GitIdentifiers: order tree entries by UTF-8 bytes as Git does - #443
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The critical malformed-name handling issue remains unresolved, along with two documentation nits.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request updates Git tree-entry ordering to compare unsigned UTF-8 bytes, matching Git’s behavior for Unicode names.
Changes:
- Replaces UTF-16 string comparison with unsigned UTF-8 comparison.
- Adds regression coverage for supplementary Unicode characters.
- Documents the fix in
changes.xml.
File summaries
| File | Summary | Review comments |
|---|---|---|
src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java |
Implements UTF-8 byte-based ordering. | Critical (2 votes): Malformed UTF-16 can produce duplicate sort keys and cause TreeSet to drop entries; reject malformed names or enforce unique keys. Nit (3 votes): Rephrase malformed Javadoc. |
src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java |
Adds Unicode ordering regression tests. | Nit (1 vote): Correct the description distinguishing BMP and supplementary-plane characters. |
src/changes/changes.xml |
Records the bug fix. | No final comments. |
Review details
Suppressed comments (1)
src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java:228
- U+FF21 is in the Basic Multilingual Plane, so this sentence incorrectly describes both examples as names outside the BMP. The ordering difference is between a BMP character and a supplementary-plane character; please reword the test documentation to make that distinction accurate.
* Tree entry names are ordered by their UTF-8 bytes, which is not the order {@link String#compareTo(String)} gives for names outside the Basic Multilingual
* Plane: U+FF21 encodes to {@code EF BC A1} and U+1F600 to {@code F0 9F 98 80}, so Git sorts U+FF21 first, while the UTF-16 code units place the surrogate
* pair of U+1F600 first.
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| this.name = name; | ||
| this.type = Objects.requireNonNull(type, "type"); | ||
| this.sortKey = type == FileMode.DIRECTORY ? name + "/" : name; | ||
| this.sortKey = (type == FileMode.DIRECTORY ? name + "/" : name).getBytes(StandardCharsets.UTF_8); |
|
@lenamonj |
|
Nice catch! 💯 I completely overlooked that both Git and SWHID (https://www.swhid.org/) treat file names as plain byte sequences. That raises a question, though: did we miss a I'm okay with just assuming UTF-8 for file names, but we should spell that out somewhere:
|
DirectoryEntry.compareTo used String.compareTo, which orders UTF-16 code units; Git orders the raw UTF-8 bytes, and the two disagree for names outside the Basic Multilingual Plane, so treeId returned an id git write-tree does not. The sort key is now the name's UTF-8 bytes, compared unsigned. A test pins the id git write-tree produces for a two-entry tree and fails on the old comparator.
String.getBytes replaces a lone surrogate with '?', so a name holding one had the same sort key as a name holding '?', and the TreeSet in TreeIdBuilder.get kept only one of them. compareTo now falls back to the String order when the bytes tie, which keeps both, as the String comparator did. The class and test descriptions of the ordering are reworded.
Git and SWHID treat file names and symbolic link targets as bytes with no defined encoding, and this class encodes both as UTF-8, so its identifiers match theirs only when the originals were UTF-8. The paragraph is the one proposed in the review, with link targets added.
a27daa0 to
7ed2bae
Compare
|
The class Javadoc now carries that paragraph, with symbolic link targets added, since Git also stores a link target as bytes with no defined encoding. Every name and link target has been encoded with |
|
|
|
Hi @lenamonj and @ppkarwasz |
|
None, and Git's object model enforces none either: a tree object stores the name bytes it was given, so the NFC and NFD spellings of one name are two entries with two different ids. On git 2.53.0 on Linux, |
|
@ppkarwasz any thoughts on merging this PR? |
|
Let's merge it. @lenamonj, thanks again! 💯 |
DirectoryEntry.compareTousedString.compareTo, which orders UTF-16 code units. Git orders raw UTF-8 bytes; outside the Basic Multilingual Plane the two differ, sotreeIddisagreed withgit write-treefor a tree holding U+FF21 and U+1F600.The sort key is now the name's UTF-8 bytes, compared unsigned. Names below U+D800 keep their order, so the existing constants are unchanged. The new test pins the id
git write-treeproduces and fails on the old comparator.Checklist: guidelines and the ASF generative-tooling guidance read. AI was used: Claude (Anthropic) found the defect and drafted the fix and test in an audit loop I run; I reviewed it, reproduced the id with
git write-tree, and ran the default Maven goal, which passes. changes.xml updated.