diff --git a/pygit2/decl.h b/pygit2/decl.h index 5c0dda4..f3ec080 100644 --- a/pygit2/decl.h +++ b/pygit2/decl.h @@ -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); diff --git a/pygit2/repository.py b/pygit2/repository.py index 064ebb9..d6d800f 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -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 # diff --git a/test/test_merge.py b/test/test_merge.py index 10d1c54..7719d6d 100644 --- a/test/test_merge.py +++ b/test/test_merge.py @@ -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')