Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/plumbing/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 25 additions & 1 deletion lib/plumbing/objects/tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class GitTree extends GitObject {
GitTree._(this.hash, this.entries);

static GitTree create([Iterable<GitTreeEntry>? 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);
}
Expand Down Expand Up @@ -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;
}
5 changes: 5 additions & 0 deletions lib/utils/file_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> get props => [val];

Expand Down
19 changes: 19 additions & 0 deletions test/index_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down
12 changes: 12 additions & 0 deletions test/plumbing/objects/tree_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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']);
});
}