Skip to content

Commit d64dd15

Browse files
committed
Merge remote-tracking branch 'carlos/merge-trees'
2 parents fa380c0 + d4da228 commit d64dd15

File tree

4 files changed

+117
-18
lines changed

4 files changed

+117
-18
lines changed

docs/merge.rst

+10
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,13 @@ can create a commit with these two parents.
3333
>>> tree = repo.index.write_tree()
3434
>>> new_commit = repo.create_commit('HEAD', user, user, tree,
3535
[repo.head.target, other_branch_tip])
36+
37+
Lower-level methods
38+
===================
39+
40+
These methods allow more direct control over how to perform the
41+
merging. They do not modify the working directory and return an
42+
in-memory Index representing the result of the merge.
43+
44+
.. automethod:: pygit2.Repository.merge_commits
45+
.. automethod:: pygit2.Repository.merge_trees

pygit2/decl.h

+1
Original file line numberDiff line numberDiff line change
@@ -661,3 +661,4 @@ typedef struct {
661661

662662
int git_merge_init_options(git_merge_options *opts, unsigned int version);
663663
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);
664+
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);

pygit2/repository.py

+80-18
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,35 @@ def index(self):
497497
#
498498
# Merging
499499
#
500+
501+
@staticmethod
502+
def _merge_options(favor):
503+
"""Return a 'git_merge_opts *'
504+
"""
505+
def favor_to_enum(favor):
506+
if favor == 'normal':
507+
return C.GIT_MERGE_FILE_FAVOR_NORMAL
508+
elif favor == 'ours':
509+
return C.GIT_MERGE_FILE_FAVOR_OURS
510+
elif favor == 'theirs':
511+
return C.GIT_MERGE_FILE_FAVOR_THEIRS
512+
elif favor == 'union':
513+
return C.GIT_MERGE_FILE_FAVOR_UNION
514+
else:
515+
return None
516+
517+
favor_val = favor_to_enum(favor)
518+
if favor_val is None:
519+
raise ValueError("unkown favor value %s" % favor)
520+
521+
opts = ffi.new('git_merge_options *')
522+
err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION)
523+
check_error(err)
524+
525+
opts.file_favor = favor_val
526+
527+
return opts
528+
500529
def merge_commits(self, ours, theirs, favor='normal'):
501530
"""Merge two arbitrary commits
502531
@@ -522,21 +551,9 @@ def merge_commits(self, ours, theirs, favor='normal'):
522551
Returns an index with the result of the merge
523552
524553
"""
525-
def favor_to_enum(favor):
526-
if favor == 'normal':
527-
return C.GIT_MERGE_FILE_FAVOR_NORMAL
528-
elif favor == 'ours':
529-
return C.GIT_MERGE_FILE_FAVOR_OURS
530-
elif favor == 'theirs':
531-
return C.GIT_MERGE_FILE_FAVOR_THEIRS
532-
elif favor == 'union':
533-
return C.GIT_MERGE_FILE_FAVOR_UNION
534-
else:
535-
return None
536554

537555
ours_ptr = ffi.new('git_commit **')
538556
theirs_ptr = ffi.new('git_commit **')
539-
opts = ffi.new('git_merge_options *')
540557
cindex = ffi.new('git_index **')
541558

542559
if is_string(ours) or isinstance(ours, Oid):
@@ -547,22 +564,67 @@ def favor_to_enum(favor):
547564
ours = ours.peel(Commit)
548565
theirs = theirs.peel(Commit)
549566

550-
err = C.git_merge_init_options(opts, C.GIT_MERGE_OPTIONS_VERSION)
567+
opts = self._merge_options(favor)
568+
569+
ffi.buffer(ours_ptr)[:] = ours._pointer[:]
570+
ffi.buffer(theirs_ptr)[:] = theirs._pointer[:]
571+
572+
err = C.git_merge_commits(cindex, self._repo, ours_ptr[0], theirs_ptr[0], opts)
551573
check_error(err)
552574

553-
favor_val = favor_to_enum(favor)
554-
if favor_val is None:
555-
raise ValueError("unkown favor value %s" % favor)
575+
return Index.from_c(self, cindex)
556576

557-
opts.file_favor = favor_val
577+
def merge_trees(self, ancestor, ours, theirs, favor='normal'):
578+
"""Merge two trees
558579
580+
Arguments:
581+
582+
ancestor
583+
The tree which is the common ancestor between 'ours' and 'theirs'
584+
ours
585+
The commit to take as "ours" or base.
586+
theirs
587+
The commit which will be merged into "ours"
588+
favor
589+
How to deal with file-level conflicts. Can be one of
590+
591+
* normal (default). Conflicts will be preserved.
592+
* ours. The "ours" side of the conflict region is used.
593+
* theirs. The "theirs" side of the conflict region is used.
594+
* union. Unique lines from each side will be used.
595+
596+
for all but NORMAL, the index will not record a conflict.
597+
598+
Returns an Index that reflects the result of the merge.
599+
"""
600+
601+
ancestor_ptr = ffi.new('git_tree **')
602+
ours_ptr = ffi.new('git_tree **')
603+
theirs_ptr = ffi.new('git_tree **')
604+
cindex = ffi.new('git_index **')
605+
606+
if is_string(ancestor) or isinstance(ancestor, Oid):
607+
ancestor = self[ancestor]
608+
if is_string(ours) or isinstance(ours, Oid):
609+
ours = self[ours]
610+
if is_string(theirs) or isinstance(theirs, Oid):
611+
theirs = self[theirs]
612+
613+
ancestor = ancestor.peel(Tree)
614+
ours = ours.peel(Tree)
615+
theirs = theirs.peel(Tree)
616+
617+
opts = self._merge_options(favor)
618+
619+
ffi.buffer(ancestor_ptr)[:] = ancestor._pointer[:]
559620
ffi.buffer(ours_ptr)[:] = ours._pointer[:]
560621
ffi.buffer(theirs_ptr)[:] = theirs._pointer[:]
561622

562-
err = C.git_merge_commits(cindex, self._repo, ours_ptr[0], theirs_ptr[0], opts)
623+
err = C.git_merge_trees(cindex, self._repo, ancestor_ptr[0], ours_ptr[0], theirs_ptr[0], opts)
563624
check_error(err)
564625

565626
return Index.from_c(self, cindex)
627+
566628
#
567629
# Utility for writing a tree into an archive
568630
#

test/test_merge.py

+26
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,29 @@ def test_merge_commits_favor(self):
165165
self.assertTrue(merge_index.conflicts is None)
166166

167167
self.assertRaises(ValueError, self.repo.merge_commits, self.repo.head.target, branch_head_hex, favor='foo')
168+
169+
class MergeTreesTest(utils.RepoTestCaseForMerging):
170+
171+
def test_merge_trees(self):
172+
branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
173+
branch_id = self.repo.get(branch_head_hex).id
174+
ancestor_id = self.repo.merge_base(self.repo.head.target, branch_id)
175+
176+
merge_index = self.repo.merge_trees(ancestor_id, self.repo.head.target, branch_head_hex)
177+
self.assertTrue(merge_index.conflicts is None)
178+
merge_commits_tree = merge_index.write_tree(self.repo)
179+
180+
self.repo.merge(branch_id)
181+
index = self.repo.index
182+
self.assertTrue(index.conflicts is None)
183+
merge_tree = index.write_tree()
184+
185+
self.assertEqual(merge_tree, merge_commits_tree)
186+
187+
def test_merge_commits_favor(self):
188+
branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
189+
ancestor_id = self.repo.merge_base(self.repo.head.target, branch_head_hex)
190+
merge_index = self.repo.merge_trees(ancestor_id, self.repo.head.target, branch_head_hex, favor='ours')
191+
self.assertTrue(merge_index.conflicts is None)
192+
193+
self.assertRaises(ValueError, self.repo.merge_trees, ancestor_id, self.repo.head.target, branch_head_hex, favor='foo')

0 commit comments

Comments
 (0)