Skip to content

Commit 9e9f159

Browse files
codexByron
authored andcommitted
fix(tree): order cached entries by filename bytes
`TreeModifier.set_done()` sorted decoded Unicode names. A name ending in an undecodable byte could sort before a valid UTF-8 name even when Git stores them in the opposite order, changing iteration order after cache editing. Recover each name's original bytes in the existing sort key and keep its directory terminator. This matches the byte comparison in Git's `tree.c::base_name_compare()` without changing tree construction, which still belongs to Git or Gix. The regression compares cache iteration with `git ls-tree -z` for byte and emoji names. It fails before the fix and passes on both backends. Connected tree tests pass on CLI and Gix; Ruff, ShellCheck and whitespace checks pass.
1 parent fcfbaa2 commit 9e9f159

4 files changed

Lines changed: 25 additions & 3 deletions

File tree

‎doc/source/changes.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ API changes
6363
contain unquoted filename bytes.
6464
* Tree serialization now delegates construction to Git or the supported Gix
6565
backend. ``Tree.cache`` and ``TreeModifier`` edit one tree's in-memory entries;
66-
object IDs and parent trees do not change automatically. Prefer
66+
``set_done()`` orders entries by filename bytes, including undecodable names.
67+
Object IDs and parent trees do not change automatically. Prefer
6768
``IndexFile.write_tree()`` for changes to staged paths.
6869
* ``Commit.stats`` preserves Unicode line separators in filenames when
6970
``core.quotePath=false``. Git's LF-delimited output remains intact for root,

‎git/objects/tree.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import git.diff as git_diff
1212
from git.util import IterableList, join_path, to_bin_sha
13-
from git.compat import defenc, safe_decode
13+
from git.compat import defenc, safe_decode, safe_encode
1414
from git.cmd import Git
1515

1616
from . import util
@@ -91,7 +91,7 @@ def set_done(self) -> "TreeModifier":
9191
:return:
9292
self
9393
"""
94-
self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
94+
self._cache.sort(key=lambda x: safe_encode(x[2]) + (b"/" if x[1] == Tree.tree_id << 12 else b""))
9595
return self
9696

9797
# } END interface

‎test/test_tree.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from gitdb import IStream
1414

15+
from git.compat import safe_encode
1516
from git.objects import Blob, Tree
1617
from git.objects.tree import TreeModifier
1718
from test.lib import TestBase, with_rw_repo
@@ -55,6 +56,14 @@ def test_invalid_tree_does_not_retain_partial_cache(object_format, invalid_posit
5556
list(tree)
5657

5758

59+
def test_tree_modifier_orders_non_utf8_names_as_git_does():
60+
with fixture_repo("tree-byte-order.sh") as repo:
61+
expected = (read_only_fixture("tree-byte-order.sh") / "order").read_bytes().split(b"\0")[:-1]
62+
tree = repo.head.commit.tree
63+
tree.cache.set_done()
64+
assert [safe_encode(item.name) for item in tree] == expected
65+
66+
5867
@ddt.ddt
5968
class TestTree(TestBase):
6069
@with_rw_repo("HEAD")

‎tests/fixtures/tree-byte-order.sh‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init --initial-branch=main repo
4+
cd repo
5+
blob=$(printf 'content\n' | git hash-object -w --stdin)
6+
tree=$({
7+
printf '100644 blob %s\tname-\377\0' "$blob"
8+
printf '100644 blob %s\tname-\360\237\222\251\0' "$blob"
9+
} | git mktree -z)
10+
commit=$(printf 'initial\n' | git commit-tree "$tree")
11+
git update-ref refs/heads/main "$commit"
12+
git ls-tree --name-only -z HEAD >../order

0 commit comments

Comments
 (0)