Introduce Repository.merge_trees()

This is the function which does the work for Repository.merge_commits()
and allows us more direct control over which trees we want to merge,
including trees which do not belong to commits.
This commit is contained in:
Carlos Martín Nieto
2015-02-09 21:21:28 +01:00
parent 718a2df1d5
commit fcd4b9446b
3 changed files with 109 additions and 20 deletions

View File

@@ -661,3 +661,4 @@ typedef struct {
int git_merge_init_options(git_merge_options *opts, unsigned int version);
int git_merge_commits(git_index **out, git_repository *repo, const git_commit *our_commit, const git_commit *their_commit, const git_merge_options *opts);
int git_merge_trees(git_index **out, git_repository *repo, const git_tree *ancestor_tree, const git_tree *our_tree, const git_tree *their_tree, const git_merge_options *opts);

View File

@@ -497,6 +497,35 @@ class Repository(_Repository):
#
# Merging
#
@staticmethod
def _merge_options(favor):
"""Return a 'git_merge_opts *'
"""
def favor_to_enum(favor):
if favor == 'normal':
return C.GIT_MERGE_FILE_FAVOR_NORMAL
elif favor == 'ours':
return C.GIT_MERGE_FILE_FAVOR_OURS
elif favor == 'theirs':
return C.GIT_MERGE_FILE_FAVOR_THEIRS
elif favor == 'union':
return C.GIT_MERGE_FILE_FAVOR_UNION
else:
return None
favor_val = favor_to_enum(favor)
if favor_val is None:
raise ValueError("unkown favor value %s" % favor)
opts = ffi.new('git_merge_options *')
err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION)
check_error(err)
opts.file_favor = favor_val
return opts
def merge_commits(self, ours, theirs, favor='normal'):
"""Merge two arbitrary commits
@@ -522,21 +551,9 @@ class Repository(_Repository):
Returns an index with the result of the merge
"""
def favor_to_enum(favor):
if favor == 'normal':
return C.GIT_MERGE_FILE_FAVOR_NORMAL
elif favor == 'ours':
return C.GIT_MERGE_FILE_FAVOR_OURS
elif favor == 'theirs':
return C.GIT_MERGE_FILE_FAVOR_THEIRS
elif favor == 'union':
return C.GIT_MERGE_FILE_FAVOR_UNION
else:
return None
ours_ptr = ffi.new('git_commit **')
theirs_ptr = ffi.new('git_commit **')
opts = ffi.new('git_merge_options *')
cindex = ffi.new('git_index **')
if is_string(ours) or isinstance(ours, Oid):
@@ -547,14 +564,7 @@ class Repository(_Repository):
ours = ours.peel(Commit)
theirs = theirs.peel(Commit)
err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION)
check_error(err)
favor_val = favor_to_enum(favor)
if favor_val is None:
raise ValueError("unkown favor value %s" % favor)
opts.file_favor = favor_val
opts = self._merge_options(favor)
ffi.buffer(ours_ptr)[:] = ours._pointer[:]
ffi.buffer(theirs_ptr)[:] = theirs._pointer[:]
@@ -563,6 +573,58 @@ class Repository(_Repository):
check_error(err)
return Index.from_c(self, cindex)
def merge_trees(self, ancestor, ours, theirs, favor='normal'):
"""Merge two trees
Arguments:
ancestor
The tree which is the common ancestor between 'ours' and 'theirs'
ours
The commit to take as "ours" or base.
theirs
The commit which will be merged into "ours"
favor
How to deal with file-level conflicts. Can be one of
* normal (default). Conflicts will be preserved.
* ours. The "ours" side of the conflict region is used.
* theirs. The "theirs" side of the conflict region is used.
* union. Unique lines from each side will be used.
for all but NORMAL, the index will not record a conflict.
Returns an Index that reflects the result of the merge.
"""
ancestor_ptr = ffi.new('git_tree **')
ours_ptr = ffi.new('git_tree **')
theirs_ptr = ffi.new('git_tree **')
cindex = ffi.new('git_index **')
if is_string(ancestor) or isinstance(ancestor, Oid):
ancestor = self[ancestor]
if is_string(ours) or isinstance(ours, Oid):
ours = self[ours]
if is_string(theirs) or isinstance(theirs, Oid):
theirs = self[theirs]
ancestor = ancestor.peel(Tree)
ours = ours.peel(Tree)
theirs = theirs.peel(Tree)
opts = self._merge_options(favor)
ffi.buffer(ancestor_ptr)[:] = ancestor._pointer[:]
ffi.buffer(ours_ptr)[:] = ours._pointer[:]
ffi.buffer(theirs_ptr)[:] = theirs._pointer[:]
err = C.git_merge_trees(cindex, self._repo, ancestor_ptr[0], ours_ptr[0], theirs_ptr[0], opts)
check_error(err)
return Index.from_c(self, cindex)
#
# Utility for writing a tree into an archive
#

View File

@@ -165,3 +165,29 @@ class MergeCommitsTest(utils.RepoTestCaseForMerging):
self.assertTrue(merge_index.conflicts is None)
self.assertRaises(ValueError, self.repo.merge_commits, self.repo.head.target, branch_head_hex, favor='foo')
class MergeTreesTest(utils.RepoTestCaseForMerging):
def test_merge_trees(self):
branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
branch_id = self.repo.get(branch_head_hex).id
ancestor_id = self.repo.merge_base(self.repo.head.target, branch_id)
merge_index = self.repo.merge_trees(ancestor_id, self.repo.head.target, branch_head_hex)
self.assertTrue(merge_index.conflicts is None)
merge_commits_tree = merge_index.write_tree(self.repo)
self.repo.merge(branch_id)
index = self.repo.index
self.assertTrue(index.conflicts is None)
merge_tree = index.write_tree()
self.assertEqual(merge_tree, merge_commits_tree)
def test_merge_commits_favor(self):
branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
ancestor_id = self.repo.merge_base(self.repo.head.target, branch_head_hex)
merge_index = self.repo.merge_trees(ancestor_id, self.repo.head.target, branch_head_hex, favor='ours')
self.assertTrue(merge_index.conflicts is None)
self.assertRaises(ValueError, self.repo.merge_trees, ancestor_id, self.repo.head.target, branch_head_hex, favor='foo')