Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ceb04b4

Browse files
committedAug 16, 2024·
Improve merge algorithm
Handle the case where we have - base - 1.md 2.md a - 1.md 2.md 3.md b - 1.md In this case the file was deleted in 2.md and should not appear in the final merged entries. This represents a common case where the file was deleted in the remote branch but not locally. Fixes GitJournal/GitJournal#962
1 parent f4f08ea commit ceb04b4

16 files changed

+123
-46
lines changed
 

‎bin/commands/merge.dart

+5-2
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ class MergeCommand extends Command<int> {
5353

5454
var authorDate = Platform.environment['GIT_AUTHOR_DATE'];
5555
if (authorDate != null) {
56-
user.date = GDateTime.parse(authorDate);
56+
var date = GDateTime.parse(authorDate);
57+
user = GitAuthor(name: user.name, email: user.email, date: date);
5758
}
5859

5960
var committer = user;
6061
var comitterDate = Platform.environment['GIT_COMMITTER_DATE'];
6162
if (comitterDate != null) {
62-
committer.date = GDateTime.parse(comitterDate);
63+
var date = GDateTime.parse(comitterDate);
64+
committer =
65+
GitAuthor(name: committer.name, email: committer.email, date: date);
6366
}
6467

6568
var msg = argResults!['message'] ?? "Merge branch '$branchName'\n";

‎lib/merge.dart

+67-36
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ extension Merge on GitRepository {
5757
}
5858
}
5959

60+
var baseTree = objStorage.readTree(bases.first.treeHash);
6061
var headTree = objStorage.readTree(headCommit.treeHash);
6162
var bTree = objStorage.readTree(commitB.treeHash);
6263

@@ -70,68 +71,64 @@ extension Merge on GitRepository {
7071
committer: committer,
7172
parents: parents,
7273
message: message,
73-
treeHash: _combineTrees(headTree, bTree),
74+
treeHash: _combineTrees(headTree, bTree, baseTree),
7475
);
7576
objStorage.writeObject(commit);
7677
return resetHard(commit.hash);
77-
78-
// - unborn ?
79-
80-
// Full 3 way
81-
// https://stackoverflow.com/questions/4129049/why-is-a-3-way-merge-advantageous-over-a-2-way-merge
8278
}
8379

8480
/// throws exceptions
85-
GitHash _combineTrees(GitTree a, GitTree b) {
81+
GitHash _combineTrees(GitTree a, GitTree b, GitTree base) {
8682
// Get all the paths
8783
var names = a.entries.map((e) => e.name).toSet();
8884
names.addAll(b.entries.map((e) => e.name));
8985

9086
var entries = <GitTreeEntry>[];
91-
for (var name in names) {
87+
for (var baseEntry in base.entries) {
88+
var name = baseEntry.name;
9289
var aIndex = a.entries.indexWhere((e) => e.name == name);
9390
var bIndex = b.entries.indexWhere((e) => e.name == name);
9491

9592
var aContains = aIndex != -1;
9693
var bContains = bIndex != -1;
9794

98-
if (aContains && !bContains) {
99-
var aEntry = a.entries[aIndex];
100-
entries.add(aEntry);
95+
if (!aContains && !bContains) {
96+
// both don't contain it!
97+
continue;
98+
} else if (aContains && !bContains) {
99+
// Entry deleted in 'b', but exists in 'a'
100+
// Delete this entry in the merged result
101+
continue;
101102
} else if (!aContains && bContains) {
103+
// Entry deleted in 'a', but exists in 'b'
102104
var bEntry = b.entries[bIndex];
103105
entries.add(bEntry);
104106
} else {
105107
// both contain it!
106108
var aEntry = a.entries[aIndex];
107109
var bEntry = b.entries[bIndex];
108110

109-
if (aEntry.mode == GitFileMode.Dir && bEntry.mode == GitFileMode.Dir) {
110-
var aEntryTree = objStorage.readTree(aEntry.hash);
111-
var bEntryTree = objStorage.readTree(bEntry.hash);
112-
113-
var newTreeHash = _combineTrees(
114-
aEntryTree,
115-
bEntryTree,
116-
);
117-
118-
var entry = GitTreeEntry(
119-
mode: GitFileMode.Dir,
120-
name: aEntry.name,
121-
hash: newTreeHash,
122-
);
123-
entries.add(entry);
124-
continue;
125-
} else if (aEntry.mode != GitFileMode.Dir &&
126-
bEntry.mode != GitFileMode.Dir) {
127-
// FIXME: Which one to pick?
128-
var aEntry = a.entries[aIndex];
129-
entries.add(aEntry);
130-
continue;
131-
}
132-
133-
throw GitNotImplemented();
111+
var newEntry = _resolvConflicts(aEntry, bEntry, baseEntry);
112+
entries.add(newEntry);
113+
}
114+
}
115+
116+
for (var entry in [...a.entries, ...b.entries]) {
117+
var name = entry.name;
118+
119+
// If the entry was already in the base
120+
var baseIndex = base.entries.indexWhere((e) => e.name == name);
121+
if (baseIndex != -1) {
122+
continue;
134123
}
124+
125+
// If the entry was already in the merged entries
126+
var mergedIndex = entries.indexWhere((e) => e.name == name);
127+
if (mergedIndex != -1) {
128+
continue;
129+
}
130+
131+
entries.add(entry);
135132
}
136133

137134
var newTree = GitTree.create(entries);
@@ -140,6 +137,40 @@ extension Merge on GitRepository {
140137
return newTree.hash;
141138
}
142139

140+
GitTreeEntry _resolvConflicts(
141+
GitTreeEntry a, GitTreeEntry b, GitTreeEntry base) {
142+
if (a.hash == b.hash) {
143+
return a;
144+
}
145+
146+
// Both are not Directories
147+
if (a.mode != GitFileMode.Dir && b.mode != GitFileMode.Dir) {
148+
return _resolveBlobConflict(a, b, base);
149+
}
150+
151+
if (a.mode == GitFileMode.Dir && b.mode == GitFileMode.Dir) {
152+
var aTree = objStorage.readTree(a.hash);
153+
var bTree = objStorage.readTree(b.hash);
154+
var baseTree = base.mode == GitFileMode.Dir
155+
? objStorage.readTree(base.hash)
156+
: GitTree.create();
157+
158+
var newTreeHash = _combineTrees(aTree, bTree, baseTree);
159+
return GitTreeEntry(
160+
mode: GitFileMode.Dir,
161+
name: a.name,
162+
hash: newTreeHash,
163+
);
164+
}
165+
166+
throw GitNotImplemented();
167+
}
168+
169+
GitTreeEntry _resolveBlobConflict(
170+
GitTreeEntry a, GitTreeEntry b, GitTreeEntry base) {
171+
return a;
172+
}
173+
143174
void mergeTrackingBranch({required GitAuthor author}) {
144175
var branch = currentBranch();
145176
var branchConfig = config.branch(branch);

‎test/commands/merge_delete_test.dart

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:test/test.dart';
2+
3+
import 'common.dart';
4+
5+
void main() {
6+
late GitCommandSetupResult s;
7+
8+
setUpAll(() async {
9+
s = await gitCommandTestFixtureSetupAll('merge-delete');
10+
});
11+
12+
setUp(() async => gitCommandTestSetup(s));
13+
14+
var commands = [
15+
'merge del2',
16+
];
17+
18+
for (var command in commands) {
19+
test(
20+
command,
21+
() async => testGitCommand(s, command, ignoreOutput: true, env: {
22+
'GIT_AUTHOR_DATE': '2020-02-15T09:08:07.000Z',
23+
'GIT_AUTHOR_NAME': 'Vishesh Handa',
24+
'GIT_AUTHOR_EMAIL': 'me@vhanda.in',
25+
'GIT_COMMITTER_DATE': '2020-02-15T09:08:07.000Z',
26+
'GIT_COMMITTER_NAME': 'Vishesh Handa',
27+
'GIT_COMMITTER_EMAIL': 'me@vhanda.in',
28+
}));
29+
}
30+
}

‎test/commands/merge_test.dart

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
// import 'package:test/test.dart';
1+
import 'package:test/test.dart';
22

3-
// import 'common.dart';
3+
import 'common.dart';
44

55
void main() {
6-
/*
76
late GitCommandSetupResult s;
87

98
setUpAll(() async {
@@ -31,7 +30,6 @@ void main() {
3130
'GIT_COMMITTER_EMAIL': 'me@vhanda.in',
3231
}));
3332
}
34-
*/
3533
}
3634

3735
// FIXME: We aren't taking directories into account!

‎test/data/merge-delete/.gitted/HEAD

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ref: refs/heads/main

‎test/data/merge-delete/.gitted/config

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[core]
2+
repositoryformatversion = 0
3+
filemode = true
4+
bare = false
5+
logallrefupdates = true
6+
ignorecase = true
7+
precomposeunicode = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Unnamed repository; edit this file 'description' to name the repository.

‎test/data/merge-delete/.gitted/index

281 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# pack-refs with: peeled fully-peeled sorted
2+
138451d299ab7dfdb8ad27bc37098d953cb09a3c refs/heads/del2
3+
f6fde458426a0eb55fe19b74eb4d12f877b8f40a refs/heads/main

‎test/data/merge-delete/.gitted/refs/heads/dummy-marker.txt

Whitespace-only changes.

‎test/data/merge-delete/1.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1

‎test/data/merge-delete/2.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

‎test/data/merge-delete/3.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3

‎test/lib.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ done''';
9898
var repo2Objects =
9999
repo2Result.stdout.split('\n').where((String e) => e.isNotEmpty).toSet();
100100

101-
expect(repo1Objects, repo2Objects);
101+
expect(repo1Objects, repo2Objects, reason: 'Objects are different');
102102

103103
// Test if all the references are the same
104104
var listRefScript = 'git show-ref --head';
@@ -115,7 +115,7 @@ done''';
115115
var repo2Refs =
116116
repo2Result.stdout.split('\n').where((String e) => e.isNotEmpty).toSet();
117117

118-
expect(repo1Refs, repo2Refs);
118+
expect(repo1Refs, repo2Refs, reason: 'Refs are different');
119119

120120
// Test if the index is the same
121121
var listIndexScript = 'git ls-files --stage';
@@ -136,7 +136,7 @@ done''';
136136
.where((String e) => e.isNotEmpty)
137137
.toSet() as Set<String>?;
138138

139-
expect(repo1Index, repo2Index);
139+
expect(repo1Index, repo2Index, reason: 'Index is different');
140140

141141
// Test if the config is the same
142142
var config1Data = await File(p.join(repo1, '.git', 'config')).readAsString();
@@ -249,7 +249,7 @@ Future<List<String>> runDartGitCommand(
249249
reason: "Command ran with an exception. This shouldn't happen",
250250
);
251251
if (!shouldReturnError) {
252-
expect(ret, 0);
252+
expect(ret, 0, reason: 'Dart command `$command` failed in $workingDir');
253253
} else {
254254
expect(ret, isNot(0));
255255
}

0 commit comments

Comments
 (0)
Please sign in to comment.