Merge remote-tracking branch 'carlos/merge-trees'

This commit is contained in:
J. David Ibáñez 2015-02-10 09:34:49 +01:00
commit d64dd15bd2
4 changed files with 119 additions and 20 deletions

@ -33,3 +33,13 @@ can create a commit with these two parents.
>>> tree = repo.index.write_tree()
>>> new_commit = repo.create_commit('HEAD', user, user, tree,
[repo.head.target, other_branch_tip])
Lower-level methods
===================
These methods allow more direct control over how to perform the
merging. They do not modify the working directory and return an
in-memory Index representing the result of the merge.
.. automethod:: pygit2.Repository.merge_commits
.. automethod:: pygit2.Repository.merge_trees

@ -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);

@ -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
#

@ -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')