diff --git a/lib/plumbing/index.dart b/lib/plumbing/index.dart index 80a3a92..b1d5086 100644 --- a/lib/plumbing/index.dart +++ b/lib/plumbing/index.dart @@ -281,7 +281,7 @@ class GitIndexEntry { mTime: stat.mTime, dev: stat.dev, ino: stat.ino, - mode: GitFileMode(stat.mode), + mode: GitFileMode.fromFileStatMode(stat.mode), uid: stat.uid, gid: stat.gid, fileSize: stat.fileSize, diff --git a/lib/plumbing/objects/tree.dart b/lib/plumbing/objects/tree.dart index 637c9f8..d0fd464 100644 --- a/lib/plumbing/objects/tree.dart +++ b/lib/plumbing/objects/tree.dart @@ -38,7 +38,8 @@ class GitTree extends GitObject { GitTree._(this.hash, this.entries); static GitTree create([Iterable? entries]) { - var t = GitTree._(GitHash.zero(), IList(entries)); + var sortedEntries = [...?entries]..sort(compareTreeEntries); + var t = GitTree._(GitHash.zero(), IList(sortedEntries)); var hash = GitHash.computeForObject(t); return GitTree._(hash, t.entries); } @@ -98,3 +99,26 @@ class GitTree extends GitObject { } } } + +int compareTreeEntries(GitTreeEntry a, GitTreeEntry b) { + var aName = utf8.encode(a.name); + var bName = utf8.encode(b.name); + var commonLength = aName.length < bName.length ? aName.length : bName.length; + + for (var i = 0; i < commonLength; i++) { + var diff = aName[i] - bName[i]; + if (diff != 0) return diff; + } + + var aTerminator = aName.length == commonLength + ? _treeSortTerminator(a) + : aName[commonLength]; + var bTerminator = bName.length == commonLength + ? _treeSortTerminator(b) + : bName[commonLength]; + return aTerminator - bTerminator; +} + +int _treeSortTerminator(GitTreeEntry entry) { + return entry.mode == GitFileMode.Dir ? $slash : 0; +} diff --git a/lib/utils/file_mode.dart b/lib/utils/file_mode.dart index b2fd598..0e00eb7 100644 --- a/lib/utils/file_mode.dart +++ b/lib/utils/file_mode.dart @@ -24,6 +24,11 @@ class GitFileMode extends Equatable { static final Symlink = GitFileMode(int.parse('120000', radix: 8)); static final Submodule = GitFileMode(int.parse('160000', radix: 8)); + static GitFileMode fromFileStatMode(int mode) { + const executableBits = 0x49; // 0111 in octal: owner/group/other execute. + return mode & executableBits == 0 ? Regular : Executable; + } + @override List get props => [val]; diff --git a/test/index_test.dart b/test/index_test.dart index 2f97e9e..9e92515 100644 --- a/test/index_test.dart +++ b/test/index_test.dart @@ -5,6 +5,7 @@ import 'package:test/test.dart'; import 'package:dart_git/plumbing/git_hash.dart'; import 'package:dart_git/plumbing/index.dart'; import 'package:dart_git/utils/file_mode.dart'; +import 'package:dart_git/utils/git_file_stat.dart'; void main() { test('Decode', () async { @@ -107,6 +108,24 @@ void main() { expect(rIndex.entries, index.entries); }); + test('normalizes filesystem file modes for git index entries', () { + var hash = GitHash('e25b29c8946e0e192fae2edc1dabf7be71e8ecf3'); + var stat = GitFileStat( + cTime: DateTime.utc(2026, 1, 1), + mTime: DateTime.utc(2026, 1, 1), + dev: 0, + ino: 0, + mode: int.parse('100600', radix: 8), + uid: 0, + gid: 0, + fileSize: 42, + ); + + var entry = GitIndexEntry.fromFS('note.md', stat, hash); + + expect(entry.mode, GitFileMode.Regular); + }); + test('Decode Merge Conflict', () { // Test adapted from go-git/plumbing/format/index/decoder_test var bytes = diff --git a/test/plumbing/objects/tree_test.dart b/test/plumbing/objects/tree_test.dart index 171033a..c65b862 100644 --- a/test/plumbing/objects/tree_test.dart +++ b/test/plumbing/objects/tree_test.dart @@ -8,6 +8,7 @@ import 'package:dart_git/plumbing/git_hash.dart'; import 'package:dart_git/plumbing/objects/object.dart'; import 'package:dart_git/plumbing/objects/tree.dart'; import 'package:dart_git/storage/object_storage_fs.dart'; +import 'package:dart_git/utils/file_mode.dart'; void main() { test('Reads the tree file correctly', () async { @@ -45,4 +46,15 @@ void main() { GitObject.envelope(data: tree.serializeData(), format: tree.format()), equals(fileBytesDefalted)); }); + + test('Sorts tree entries using git tree ordering', () { + var hash = GitHash('e25b29c8946e0e192fae2edc1dabf7be71e8ecf3'); + var tree = GitTree.create([ + GitTreeEntry(mode: GitFileMode.Regular, name: 'a0.md', hash: hash), + GitTreeEntry(mode: GitFileMode.Dir, name: 'a', hash: hash), + GitTreeEntry(mode: GitFileMode.Regular, name: 'a.md', hash: hash), + ]); + + expect(tree.entries.map((entry) => entry.name), ['a.md', 'a', 'a0.md']); + }); }