Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Bug 515613 - [node] Can't merge two different histories together
When a merge is attempted, a common ancestor is required as it may
simply be a fast-forward or the active branch may already be
up-to-date depending on what the tip commit of the other branch is.
However, if the user tries to merge an unrelated history, there will
not be a common ancestor between the two branches. We need to handle
this case and immediately try to merge the two branches instead.

Signed-off-by: Remy Suen <remy.suen@gmail.com>
  • Loading branch information
rcjsuen committed Apr 22, 2017
1 parent 96336ce commit 49277e4
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 19 deletions.
77 changes: 58 additions & 19 deletions modules/orionode/lib/git/commit.js
Expand Up @@ -993,26 +993,29 @@ function merge(req, res, branchToMerge, squash) {
});
}

return repo.mergeBranches("HEAD", branchToMerge)
.then(function(_oid) {
oid = _oid;
})
.catch(function(index) {
// the merge failed due to conflicts
conflicting = true;
return git.AnnotatedCommit.lookup(repo, commit.id())
.then(function(annotated) {
var retCode = git.Merge.merge(repo, annotated, null, null);
if (retCode === git.Error.CODE.ECONFLICT) {
// checkout failed due to a conflict
return getConflictingPaths(repo, head, commit)
.then(function(conflictingPaths) {
paths = conflictingPaths;
});
} else if (retCode !== git.Error.CODE.OK) {
throw new Error("Internal merge failure (error code " + retCode + ")");
}
// try to find a common ancestor before merging
return git.Merge.base(repo, head, commit.id())
.then(function() {
return repo.mergeBranches("HEAD", branchToMerge)
.then(function(_oid) {
oid = _oid;
})
.catch(function(index) {
// the merge failed due to conflicts
conflicting = true;
return forceMerge(repo, head, commit, branchToMerge, false, function(conflictingPaths) {
paths = conflictingPaths;
});
});
})
.catch(function(err) {
if (err.message === "No merge base found") {
// no common ancestor between the two branches, force the merge
return forceMerge(repo, head, commit, branchToMerge, true, function(conflictingPaths) {
paths = conflictingPaths;
});
}
throw err;
});
})
.then(function() {
Expand All @@ -1036,6 +1039,42 @@ function merge(req, res, branchToMerge, squash) {
});
}

/**
* Force a merge from the specified comment onto HEAD. This may leave
* the repository in a conflicted state. If there are no conflits, a
* merge commit will be created to complete the merge.
*
* @param {Repository} repo the repository to perform the merge in
* @param {Commit} head the commit that HEAD is pointing at
* @param {Commit} commit the commit to merge in
* @param {String} branchToMerge the name of the other branch
* @param {boolean} createMergeCommit <tt>true</tt> if a merge commit should be created automatically,
* <tt>false</tt> otherwise
* @param {Function} conflictingPathsCallback the callback to notify if the merge fails due to
* conflicting paths in the working directory or the index
*/
function forceMerge(repo, head, commit, branchToMerge, createMergeCommit, conflictingPathsCallback) {
return git.AnnotatedCommit.lookup(repo, commit.id())
.then(function(annotated) {
var retCode = git.Merge.merge(repo, annotated, null, null);
if (retCode === git.Error.CODE.ECONFLICT) {
// checkout failed due to a conflict
return getConflictingPaths(repo, head, commit).then(conflictingPathsCallback);
} else if (retCode !== git.Error.CODE.OK) {
throw new Error("Internal merge failure (error code " + retCode + ")");
}

if (createMergeCommit) {
var signature = repo.defaultSignature();
var message = "Merged branch '" + branchToMerge + "'";
return createCommit(repo,
signature.name(), signature.email(),
signature.name(), signature.email(),
message, false, false);
}
});
}

function getConflictingPaths(repo, head, commit) {
var tree2;
var diffPaths = [];
Expand Down
178 changes: 178 additions & 0 deletions modules/orionode/test/test-git-api.js
Expand Up @@ -1384,6 +1384,184 @@ maybeDescribe("git", function() {
describe("Merge", function() {
before(setup);

/**
* Test suite for merging two branches that don't share a common ancestor.
*/
describe("Unrelated", function() {
it("identical content", function(finished) {
var testName = "merge-unrelated-identical-content";
var repository;
var initial, modify, extra, extra2;
var name = "test.txt";
var unrelated = "unrelated.txt";

var client = new GitClient(testName);
client.init();
// create a file with content "A" in it
client.setFileContents(name, "A");
client.stage(name);
client.commit();
client.start().then(function(commit) {
initial = commit.Id;
// open the repository using NodeGit
var testPath = path.join(WORKSPACE, "merge-unrelated-identical-content");
return git.Repository.open(testPath);
})
.then(function(repo) {
repository = repo;
return repository.refreshIndex();
})
.then(function(index) {
// get the oid of the current repository state
return index.writeTree();
})
.then(function(oid) {
// using that oid, create a commit in another branch with no parent commit
return repository.createCommit("refs/heads/other",
git.Signature.default(repository),
git.Signature.default(repository),
"unrelated", oid, [ ]);
})
.then(function() {
// merge in the branch with an unrelated history
client.merge("other");
return client.start();
})
.then(function(result) {
// should have succeeded, content was in fact identical
assert.equal(result.Result, "MERGED");
assert.equal(result.FailingPaths, undefined);

// check that the status is clean
client.status("SAFE");
return client.start();
})
.then(function() {
finished();
})
.catch(function(err) {
finished(err);
});
});

it("dirty working dir", function(finished) {
var testName = "merge-unrelated-dirty-working-dir";
var repository;
var initial, modify, extra, extra2;
var name = "test.txt";
var unrelated = "unrelated.txt";

var client = new GitClient(testName);
client.init();
client.setFileContents(name, "1\n2\n3");
client.stage(name);
client.commit();
client.start().then(function(commit) {
initial = commit.Id;
client.setFileContents(name, "a\nb\nc");
client.stage(name);
return client.start();
})
.then(function() {
// open the repository using NodeGit
var testPath = path.join(WORKSPACE, testName);
return git.Repository.open(testPath);
})
.then(function(repo) {
repository = repo;
return repository.refreshIndex();
})
.then(function(index) {
// get the oid of the current repository state
return index.writeTree();
})
.then(function(oid) {
// using that oid, create a commit in another branch with no parent commit
return repository.createCommit("refs/heads/other",
git.Signature.default(repository),
git.Signature.default(repository),
"unrelated", oid, [ ]);
})
.then(function() {
// reset and make the working directory dirty
client.reset("HARD", initial);
client.setFileContents(name, "B");
// merge in the branch with an unrelated history
client.merge("other");
return client.start();
})
.then(function(result) {
// should have failed because of the dirty working dir
assert.equal(result.Result, "FAILED");
assert.equal(Object.keys(result.FailingPaths).length, 1);
assert.equal(result.FailingPaths[name], "");
finished();
})
.catch(function(err) {
finished(err);
});
});

it("dirty index", function(finished) {
var testName = "merge-unrelated-dirty-index";
var repository;
var initial, modify, extra, extra2;
var name = "test.txt";
var unrelated = "unrelated.txt";

var client = new GitClient(testName);
client.init();
client.setFileContents(name, "1\n2\n3");
client.stage(name);
client.commit();
client.start().then(function(commit) {
initial = commit.Id;
client.setFileContents(name, "a\nb\nc");
client.stage(name);
return client.start();
})
.then(function() {
// open the repository using NodeGit
var testPath = path.join(WORKSPACE, testName);
return git.Repository.open(testPath);
})
.then(function(repo) {
repository = repo;
return repository.refreshIndex();
})
.then(function(index) {
// get the oid of the current repository state
return index.writeTree();
})
.then(function(oid) {
// using that oid, create a commit in another branch with no parent commit
return repository.createCommit("refs/heads/other",
git.Signature.default(repository),
git.Signature.default(repository),
"unrelated", oid, [ ]);
})
.then(function() {
// reset and make the working directory dirty
client.reset("HARD", initial);
client.setFileContents(name, "B");
client.stage(name);
// merge in the branch with an unrelated history
client.merge("other");
return client.start();
})
.then(function(result) {
// should have failed because of the dirty working dir
assert.equal(result.Result, "FAILED");
assert.equal(Object.keys(result.FailingPaths).length, 1);
assert.equal(result.FailingPaths[name], "");
finished();
})
.catch(function(err) {
finished(err);
});
});
});

describe("Conflicts", function() {
it("POST commit will resolve merge in progress", function(finished) {
var name = "conflicts.txt";
Expand Down

0 comments on commit 49277e4

Please sign in to comment.