Skip to content

Commit

Permalink
Improve merge algorithm
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vHanda committed Aug 16, 2024
1 parent f4f08ea commit ceb04b4
Show file tree
Hide file tree
Showing 16 changed files with 123 additions and 46 deletions.
7 changes: 5 additions & 2 deletions bin/commands/merge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ class MergeCommand extends Command<int> {

var authorDate = Platform.environment['GIT_AUTHOR_DATE'];
if (authorDate != null) {
user.date = GDateTime.parse(authorDate);
var date = GDateTime.parse(authorDate);
user = GitAuthor(name: user.name, email: user.email, date: date);
}

var committer = user;
var comitterDate = Platform.environment['GIT_COMMITTER_DATE'];
if (comitterDate != null) {
committer.date = GDateTime.parse(comitterDate);
var date = GDateTime.parse(comitterDate);
committer =
GitAuthor(name: committer.name, email: committer.email, date: date);
}

var msg = argResults!['message'] ?? "Merge branch '$branchName'\n";
Expand Down
103 changes: 67 additions & 36 deletions lib/merge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extension Merge on GitRepository {
}
}

var baseTree = objStorage.readTree(bases.first.treeHash);
var headTree = objStorage.readTree(headCommit.treeHash);
var bTree = objStorage.readTree(commitB.treeHash);

Expand All @@ -70,68 +71,64 @@ extension Merge on GitRepository {
committer: committer,
parents: parents,
message: message,
treeHash: _combineTrees(headTree, bTree),
treeHash: _combineTrees(headTree, bTree, baseTree),
);
objStorage.writeObject(commit);
return resetHard(commit.hash);

// - unborn ?

// Full 3 way
// https://stackoverflow.com/questions/4129049/why-is-a-3-way-merge-advantageous-over-a-2-way-merge
}

/// throws exceptions
GitHash _combineTrees(GitTree a, GitTree b) {
GitHash _combineTrees(GitTree a, GitTree b, GitTree base) {
// Get all the paths
var names = a.entries.map((e) => e.name).toSet();
names.addAll(b.entries.map((e) => e.name));

var entries = <GitTreeEntry>[];
for (var name in names) {
for (var baseEntry in base.entries) {
var name = baseEntry.name;
var aIndex = a.entries.indexWhere((e) => e.name == name);
var bIndex = b.entries.indexWhere((e) => e.name == name);

var aContains = aIndex != -1;
var bContains = bIndex != -1;

if (aContains && !bContains) {
var aEntry = a.entries[aIndex];
entries.add(aEntry);
if (!aContains && !bContains) {
// both don't contain it!
continue;
} else if (aContains && !bContains) {
// Entry deleted in 'b', but exists in 'a'
// Delete this entry in the merged result
continue;
} else if (!aContains && bContains) {
// Entry deleted in 'a', but exists in 'b'
var bEntry = b.entries[bIndex];
entries.add(bEntry);
} else {
// both contain it!
var aEntry = a.entries[aIndex];
var bEntry = b.entries[bIndex];

if (aEntry.mode == GitFileMode.Dir && bEntry.mode == GitFileMode.Dir) {
var aEntryTree = objStorage.readTree(aEntry.hash);
var bEntryTree = objStorage.readTree(bEntry.hash);

var newTreeHash = _combineTrees(
aEntryTree,
bEntryTree,
);

var entry = GitTreeEntry(
mode: GitFileMode.Dir,
name: aEntry.name,
hash: newTreeHash,
);
entries.add(entry);
continue;
} else if (aEntry.mode != GitFileMode.Dir &&
bEntry.mode != GitFileMode.Dir) {
// FIXME: Which one to pick?
var aEntry = a.entries[aIndex];
entries.add(aEntry);
continue;
}

throw GitNotImplemented();
var newEntry = _resolvConflicts(aEntry, bEntry, baseEntry);
entries.add(newEntry);
}
}

for (var entry in [...a.entries, ...b.entries]) {
var name = entry.name;

// If the entry was already in the base
var baseIndex = base.entries.indexWhere((e) => e.name == name);
if (baseIndex != -1) {
continue;
}

// If the entry was already in the merged entries
var mergedIndex = entries.indexWhere((e) => e.name == name);
if (mergedIndex != -1) {
continue;
}

entries.add(entry);
}

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

GitTreeEntry _resolvConflicts(
GitTreeEntry a, GitTreeEntry b, GitTreeEntry base) {
if (a.hash == b.hash) {
return a;
}

// Both are not Directories
if (a.mode != GitFileMode.Dir && b.mode != GitFileMode.Dir) {
return _resolveBlobConflict(a, b, base);
}

if (a.mode == GitFileMode.Dir && b.mode == GitFileMode.Dir) {
var aTree = objStorage.readTree(a.hash);
var bTree = objStorage.readTree(b.hash);
var baseTree = base.mode == GitFileMode.Dir
? objStorage.readTree(base.hash)
: GitTree.create();

var newTreeHash = _combineTrees(aTree, bTree, baseTree);
return GitTreeEntry(
mode: GitFileMode.Dir,
name: a.name,
hash: newTreeHash,
);
}

throw GitNotImplemented();
}

GitTreeEntry _resolveBlobConflict(
GitTreeEntry a, GitTreeEntry b, GitTreeEntry base) {
return a;
}

void mergeTrackingBranch({required GitAuthor author}) {
var branch = currentBranch();
var branchConfig = config.branch(branch);
Expand Down
30 changes: 30 additions & 0 deletions test/commands/merge_delete_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:test/test.dart';

import 'common.dart';

void main() {
late GitCommandSetupResult s;

setUpAll(() async {
s = await gitCommandTestFixtureSetupAll('merge-delete');
});

setUp(() async => gitCommandTestSetup(s));

var commands = [
'merge del2',
];

for (var command in commands) {
test(
command,
() async => testGitCommand(s, command, ignoreOutput: true, env: {
'GIT_AUTHOR_DATE': '2020-02-15T09:08:07.000Z',
'GIT_AUTHOR_NAME': 'Vishesh Handa',
'GIT_AUTHOR_EMAIL': '[email protected]',
'GIT_COMMITTER_DATE': '2020-02-15T09:08:07.000Z',
'GIT_COMMITTER_NAME': 'Vishesh Handa',
'GIT_COMMITTER_EMAIL': '[email protected]',
}));
}
}
6 changes: 2 additions & 4 deletions test/commands/merge_test.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// import 'package:test/test.dart';
import 'package:test/test.dart';

// import 'common.dart';
import 'common.dart';

void main() {
/*
late GitCommandSetupResult s;

setUpAll(() async {
Expand Down Expand Up @@ -31,7 +30,6 @@ void main() {
'GIT_COMMITTER_EMAIL': '[email protected]',
}));
}
*/
}

// FIXME: We aren't taking directories into account!
Expand Down
1 change: 1 addition & 0 deletions test/data/merge-delete/.gitted/HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/main
7 changes: 7 additions & 0 deletions test/data/merge-delete/.gitted/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
1 change: 1 addition & 0 deletions test/data/merge-delete/.gitted/description
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
Binary file added test/data/merge-delete/.gitted/index
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions test/data/merge-delete/.gitted/packed-refs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pack-refs with: peeled fully-peeled sorted
138451d299ab7dfdb8ad27bc37098d953cb09a3c refs/heads/del2
f6fde458426a0eb55fe19b74eb4d12f877b8f40a refs/heads/main
Empty file.
1 change: 1 addition & 0 deletions test/data/merge-delete/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions test/data/merge-delete/2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
1 change: 1 addition & 0 deletions test/data/merge-delete/3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
8 changes: 4 additions & 4 deletions test/lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ done''';
var repo2Objects =
repo2Result.stdout.split('\n').where((String e) => e.isNotEmpty).toSet();

expect(repo1Objects, repo2Objects);
expect(repo1Objects, repo2Objects, reason: 'Objects are different');

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

expect(repo1Refs, repo2Refs);
expect(repo1Refs, repo2Refs, reason: 'Refs are different');

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

expect(repo1Index, repo2Index);
expect(repo1Index, repo2Index, reason: 'Index is different');

// Test if the config is the same
var config1Data = await File(p.join(repo1, '.git', 'config')).readAsString();
Expand Down Expand Up @@ -249,7 +249,7 @@ Future<List<String>> runDartGitCommand(
reason: "Command ran with an exception. This shouldn't happen",
);
if (!shouldReturnError) {
expect(ret, 0);
expect(ret, 0, reason: 'Dart command `$command` failed in $workingDir');

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/reset_test.dart ► reset --hard HEAD^

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `reset --hard HEAD^` failed in /tmp/_git_AZNMPY/merge_dart
Raw output
dartgit>$ git reset --hard HEAD^
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/single_commands_test.dart ► write-tree

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `write-tree` failed in /tmp/_git_ZUHLTT/dart_git_dart
Raw output
dartgit>$ git write-tree
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/single_commands_test.dart ► diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522` failed in /tmp/_git_ZUHLTT/dart_git_dart
Raw output
dartgit>$ git diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/single_commands_test.dart ► diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711` failed in /tmp/_git_ZUHLTT/dart_git_dart
Raw output
dartgit>$ git diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/single_commands_test.dart ► checkout 1 file

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `checkout LICENSE` failed in /tmp/_git_ZUHLTT/dart_git_dart
Raw output
dartgit>$ git checkout LICENSE
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests ubuntu-latest stable

test/commands/single_commands_test.dart ► git checkout remote branch

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `checkout -b master origin/master` failed in /tmp/_git_ZUHLTT/dart_git_dart
Raw output
dartgit>$ git init -q .
dartgit>$ git remote add origin https://github.com/GitJournal/icloud_documents_path.git
dartgit>$ git checkout -b master origin/master
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests macOS-latest stable

test/commands/reset_test.dart ► reset --hard HEAD^

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `reset --hard HEAD^` failed in /var/folders/hw/1f0gcr8d6kn9ms0_wn0_57qc0000gn/T/_git_QLu7d8/merge_dart
Raw output
dartgit>$ git reset --hard HEAD^
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests macOS-latest stable

test/commands/single_commands_test.dart ► write-tree

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `write-tree` failed in /var/folders/hw/1f0gcr8d6kn9ms0_wn0_57qc0000gn/T/_git_PY61Ou/dart_git_dart
Raw output
dartgit>$ git write-tree
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests macOS-latest stable

test/commands/single_commands_test.dart ► diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522` failed in /var/folders/hw/1f0gcr8d6kn9ms0_wn0_57qc0000gn/T/_git_PY61Ou/dart_git_dart
Raw output
dartgit>$ git diff-tree 938c320fd826711ab4e3f5db5cf2f4557ff75522
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests macOS-latest stable

test/commands/single_commands_test.dart ► diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711` failed in /var/folders/hw/1f0gcr8d6kn9ms0_wn0_57qc0000gn/T/_git_PY61Ou/dart_git_dart
Raw output
dartgit>$ git diff-tree 6216f82ecd10cac78c2b90ddcc4d0d9dc6f3d711
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand

Check failure on line 252 in test/lib.dart

View workflow job for this annotation

GitHub Actions / Unit Tests macOS-latest stable

test/commands/single_commands_test.dart ► checkout 1 file

Failed test found in: test-results.json Error: Expected: <0> Actual: <1> Dart command `checkout LICENSE` failed in /var/folders/hw/1f0gcr8d6kn9ms0_wn0_57qc0000gn/T/_git_PY61Ou/dart_git_dart
Raw output
dartgit>$ git checkout LICENSE
package:matcher      expect
test/lib.dart 252:5  runDartGitCommand
} else {
expect(ret, isNot(0));
}
Expand Down

0 comments on commit ceb04b4

Please sign in to comment.