Rev 3250: Implement cherrypick support for Merge3 in http://bzr.arbash-meinel.com/branches/bzr/1.3-dev/cherrypick_merge
John Arbash Meinel
john at arbash-meinel.com
Tue Mar 4 13:49:46 GMT 2008
At http://bzr.arbash-meinel.com/branches/bzr/1.3-dev/cherrypick_merge
------------------------------------------------------------
revno: 3250
revision-id:john at arbash-meinel.com-20080304134944-lv23t3fmc70xtoi9
parent: pqm at pqm.ubuntu.com-20080304003709-35vh1eqa8tuuq548
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: cherrypick_merge
timestamp: Tue 2008-03-04 13:49:44 +0000
message:
Implement cherrypick support for Merge3
When merging a cherrypick, use a slightly different resolve logic.
When encountering a conflict, the new logic does not include lines that
were present in BASE that are conflicting with OTHER.
This is done since a cherrypick is (by definition) avoiding changes that
are present in the base.
(related to bug #151731)
modified:
bzrlib/merge.py merge.py-20050513021216-953b65a438527106
bzrlib/merge3.py merge3.py-20050704130834-bf0597094828a2e1
bzrlib/tests/test_merge.py testmerge.py-20050905070950-c1b5aa49ff911024
bzrlib/tests/test_merge3.py merge3.py-20050704130834-556689114c89e6f2
-------------- next part --------------
=== modified file 'bzrlib/merge.py'
--- a/bzrlib/merge.py 2008-03-02 15:06:13 +0000
+++ b/bzrlib/merge.py 2008-03-04 13:49:44 +0000
@@ -105,6 +105,10 @@
def _get_base_is_ancestor(self):
if self._base_is_ancestor is None:
+ if getattr(self, 'base_rev_id', None) is None:
+ import pdb; pdb.set_trace()
+ if getattr(self, 'this_basis', None) is None:
+ import pdb; pdb.set_trace()
self._base_is_ancestor = self.revision_graph.is_ancestor(
self.base_rev_id, self.this_basis)
return self._base_is_ancestor
@@ -116,7 +120,9 @@
def _get_base_is_other_ancestor(self):
if self._base_is_other_ancestor is None:
- self.base_is_other_ancestor = self.revision_graph.is_ancestor(
+ if self.other_basis is None:
+ return True
+ self._base_is_other_ancestor = self.revision_graph.is_ancestor(
self.base_rev_id, self.other_basis)
return self._base_is_other_ancestor
@@ -162,7 +168,7 @@
return merger, verified
@staticmethod
- def from_revision_ids(pb, this, other, base=None, other_branch=None,
+ def from_revision_ids(pb, tree, other, base=None, other_branch=None,
base_branch=None, revision_graph=None):
"""Return a Merger for revision-ids.
@@ -171,15 +177,17 @@
:param base: The revision-id to use as BASE. If not specified, will
be auto-selected.
:param other_branch: A branch containing the other revision-id. If
- not supplied, this.branch is used.
+ not supplied, tree.branch is used.
:param base_branch: A branch containing the base revision-id. If
- not supplied, other_branch or this.branch will be used.
+ not supplied, other_branch or tree.branch will be used.
+ :param revision_graph: If you have a revision_graph precomputed, pass
+ it in, otherwise it will be created for you.
:param pb: A progress indicator
"""
- merger = Merger(this.branch, this_tree=this, pb=pb,
+ merger = Merger(tree.branch, this_tree=tree, pb=pb,
revision_graph=revision_graph)
if other_branch is None:
- other_branch = this.branch
+ other_branch = tree.branch
merger.set_other_revision(other, other_branch)
if base is None:
merger.find_base()
@@ -399,7 +407,7 @@
if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
and not self.base_is_other_ancestor):
raise errors.CannotReverseCherrypick()
- if self.merge_type.history_based:
+ if self.merge_type.supports_cherrypick:
kwargs['cherrypick'] = (not self.base_is_ancestor or
not self.base_is_other_ancestor)
return self.merge_type(pb=self._pb,
@@ -407,13 +415,13 @@
**kwargs)
def do_merge(self):
- merge = self.make_merger()
self.this_tree.lock_tree_write()
if self.base_tree is not None:
self.base_tree.lock_read()
if self.other_tree is not None:
self.other_tree.lock_read()
try:
+ merge = self.make_merger()
merge.do_merge()
if self.recurse == 'down':
for path, file_id in self.this_tree.iter_references():
@@ -430,6 +438,7 @@
base_revision = self.base_tree.get_reference_revision(file_id)
sub_merge.base_tree = \
sub_tree.branch.repository.revision_tree(base_revision)
+ sub_merge.base_rev_id = base_revision
sub_merge.do_merge()
finally:
@@ -453,13 +462,15 @@
supports_reprocess = True
supports_show_base = True
history_based = False
+ supports_cherrypick = True
supports_reverse_cherrypick = True
winner_idx = {"this": 2, "other": 1, "conflict": 1}
def __init__(self, working_tree, this_tree, base_tree, other_tree,
interesting_ids=None, reprocess=False, show_base=False,
pb=DummyProgress(), pp=None, change_reporter=None,
- interesting_files=None, do_merge=True):
+ interesting_files=None, do_merge=True,
+ cherrypick=False):
"""Initialize the merger object and perform the merge.
:param working_tree: The working tree to apply the merge to
@@ -497,6 +508,7 @@
self.pb = pb
self.pp = pp
self.change_reporter = change_reporter
+ self.cherrypick = cherrypick
if self.pp is None:
self.pp = ProgressPhase("Merge phase", 3, self.pb)
if do_merge:
@@ -865,7 +877,8 @@
base_lines = []
other_lines = self.get_lines(self.other_tree, file_id)
this_lines = self.get_lines(self.this_tree, file_id)
- m3 = Merge3(base_lines, this_lines, other_lines)
+ m3 = Merge3(base_lines, this_lines, other_lines,
+ is_cherrypick=self.cherrypick)
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
if self.show_base is True:
base_marker = '|' * 7
@@ -1040,18 +1053,6 @@
supports_reverse_cherrypick = False
history_based = True
- def __init__(self, working_tree, this_tree, base_tree, other_tree,
- interesting_ids=None, pb=DummyProgress(), pp=None,
- reprocess=False, change_reporter=None,
- interesting_files=None, cherrypick=False, do_merge=True):
- self.cherrypick = cherrypick
- super(WeaveMerger, self).__init__(working_tree, this_tree,
- base_tree, other_tree,
- interesting_ids=interesting_ids,
- pb=pb, pp=pp, reprocess=reprocess,
- change_reporter=change_reporter,
- do_merge=do_merge)
-
def _merged_lines(self, file_id):
"""Generate the merged lines.
There is no distinction between lines that are meant to contain <<<<<<<
@@ -1194,6 +1195,10 @@
merger.reprocess = reprocess
merger.other_rev_id = other_rev_id
merger.other_basis = other_rev_id
+ get_revision_id = getattr(base_tree, 'get_revision_id', None)
+ if get_revision_id is None:
+ get_revision_id = base_tree.last_revision
+ merger.set_base_revision(get_revision_id(), this_branch)
return merger.do_merge()
def get_merge_type_registry():
=== modified file 'bzrlib/merge3.py'
--- a/bzrlib/merge3.py 2007-03-12 19:56:41 +0000
+++ b/bzrlib/merge3.py 2008-03-04 13:49:44 +0000
@@ -67,15 +67,14 @@
Given BASE, OTHER, THIS, tries to produce a combined text
incorporating the changes from both BASE->OTHER and BASE->THIS.
All three will typically be sequences of lines."""
- def __init__(self, base, a, b):
+ def __init__(self, base, a, b, is_cherrypick=False):
check_text_lines(base)
check_text_lines(a)
check_text_lines(b)
self.base = base
self.a = a
self.b = b
-
-
+ self.is_cherrypick = is_cherrypick
def merge_lines(self,
name_a=None,
@@ -130,10 +129,6 @@
yield end_marker + newline
else:
raise ValueError(what)
-
-
-
-
def merge_annotated(self):
"""Return merge with conflicts, showing origin of lines.
@@ -161,10 +156,6 @@
yield '>>>>\n'
else:
raise ValueError(what)
-
-
-
-
def merge_groups(self):
"""Yield sequence of line groups. Each one is a tuple:
@@ -200,7 +191,6 @@
else:
raise ValueError(what)
-
def merge_regions(self):
"""Return sequences of matching and conflicting regions.
@@ -250,23 +240,30 @@
if len_a or len_b:
# try to avoid actually slicing the lists
- equal_a = compare_range(self.a, ia, amatch,
- self.base, iz, zmatch)
- equal_b = compare_range(self.b, ib, bmatch,
- self.base, iz, zmatch)
same = compare_range(self.a, ia, amatch,
self.b, ib, bmatch)
if same:
yield 'same', ia, amatch
- elif equal_a and not equal_b:
- yield 'b', ib, bmatch
- elif equal_b and not equal_a:
- yield 'a', ia, amatch
- elif not equal_a and not equal_b:
- yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
else:
- raise AssertionError("can't handle a=b=base but unmatched")
+ equal_a = compare_range(self.a, ia, amatch,
+ self.base, iz, zmatch)
+ equal_b = compare_range(self.b, ib, bmatch,
+ self.base, iz, zmatch)
+ if equal_a and not equal_b:
+ yield 'b', ib, bmatch
+ elif equal_b and not equal_a:
+ yield 'a', ia, amatch
+ elif not equal_a and not equal_b:
+ if self.is_cherrypick:
+ for node in self._refine_cherrypick_conflict(
+ iz, zmatch, ia, amatch,
+ ib, bmatch):
+ yield node
+ else:
+ yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
+ else:
+ raise AssertionError("can't handle a=b=base but unmatched")
ia = amatch
ib = bmatch
@@ -275,7 +272,6 @@
# if the same part of the base was deleted on both sides
# that's OK, we can just skip it.
-
if matchlen > 0:
assert ia == amatch
assert ib == bmatch
@@ -285,7 +281,43 @@
iz = zend
ia = aend
ib = bend
-
+
+ def _refine_cherrypick_conflict(self, zstart, zend, astart, aend, bstart, bend):
+ """When cherrypicking b => a, ignore matches with b and base."""
+ # Do not emit regions which match, only regions which do not match
+ matches = bzrlib.patiencediff.PatienceSequenceMatcher(None,
+ self.base[zstart:zend], self.b[bstart:bend]).get_matching_blocks()
+ zzcur = 0
+ bbcur = 0
+ yielded_a = False
+ for region_iz, region_ib, region_len in matches:
+ conflict_z_len = region_iz - zzcur
+ conflict_b_len = region_ib - bbcur
+ if conflict_b_len == 0: # There are no lines in b which conflict,
+ # so skip it
+ pass
+ else:
+ if yielded_a:
+ yield ('conflict', zstart + zzcur, zstart + region_iz,
+ aend, aend, bstart + bbcur, bstart + region_ib)
+ else:
+ # The first conflict gets the a-range
+ yielded_a = True
+ yield ('conflict', zstart + zzcur, zstart + region_iz,
+ astart, aend, bstart + bbcur, bstart + region_ib)
+ zzcur = region_iz + region_len
+ bbcur = region_ib + region_len
+ if zzcur != zend - zstart or bbcur != bend - bstart:
+ if yielded_a:
+ yield ('conflict', zstart + zzcur, zstart + region_iz,
+ aend, aend, bstart + bbcur, bstart + region_ib)
+ else:
+ # The first conflict gets the a-range
+ yielded_a = True
+ yield ('conflict', zstart + zzcur, zstart + region_iz,
+ astart, aend, bstart + bbcur, bstart + region_ib)
+ if not yielded_a:
+ yield ('conflict', zstart, zend, astart, aend, bstart, bend)
def reprocess_merge_regions(self, merge_regions):
"""Where there are conflict regions, remove the agreed lines.
@@ -318,12 +350,10 @@
if reg is not None:
yield reg
-
@staticmethod
def mismatch_region(next_a, region_ia, next_b, region_ib):
if next_a < region_ia or next_b < region_ib:
return 'conflict', None, None, next_a, region_ia, next_b, region_ib
-
def find_sync_regions(self):
"""Return a list of sync regions, where both descendents match the base.
@@ -366,6 +396,8 @@
aend = asub + intlen
bend = bsub + intlen
+ # XXX: How much overhead is involved in slicing all of these
+ # and doing an extra comparison
assert self.base[intbase:intend] == self.a[asub:aend], \
(self.base[intbase:intend], self.a[asub:aend])
@@ -388,8 +420,6 @@
return sl
-
-
def find_unconflicted(self):
"""Return a list of ranges in base that are not conflicted."""
am = bzrlib.patiencediff.PatienceSequenceMatcher(
=== modified file 'bzrlib/tests/test_merge.py'
--- a/bzrlib/tests/test_merge.py 2008-03-02 15:06:13 +0000
+++ b/bzrlib/tests/test_merge.py 2008-03-04 13:49:44 +0000
@@ -352,6 +352,30 @@
merger.merge_type = _mod_merge.Merge3Merger
merger.do_merge()
+ def test_merge3_will_detect_cherrypick(self):
+ this_tree = self.make_branch_and_tree('this')
+ self.build_tree_contents([('this/file', "a\n")])
+ this_tree.add('file')
+ this_tree.commit('rev1')
+ other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
+ self.build_tree_contents([('other/file', "a\nb\n")])
+ other_tree.commit('rev2b', rev_id='rev2b')
+ self.build_tree_contents([('other/file', "a\nb\nc\n")])
+ other_tree.commit('rev3b', rev_id='rev3b')
+ this_tree.lock_write()
+ self.addCleanup(this_tree.unlock)
+
+ merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
+ this_tree, 'rev3b', 'rev2b', other_tree.branch)
+ merger.merge_type = _mod_merge.Merge3Merger
+ merger.do_merge()
+ self.assertFileEqual('a\n'
+ '<<<<<<< TREE\n'
+ '=======\n'
+ 'c\n'
+ '>>>>>>> MERGE-SOURCE\n',
+ 'this/file')
+
def test_make_merger(self):
this_tree = self.make_branch_and_tree('this')
this_tree.commit('rev1', rev_id='rev1')
=== modified file 'bzrlib/tests/test_merge3.py'
--- a/bzrlib/tests/test_merge3.py 2007-03-12 19:56:41 +0000
+++ b/bzrlib/tests/test_merge3.py 2008-03-04 13:49:44 +0000
@@ -383,3 +383,42 @@
m_lines = m3.merge_lines('OTHER', 'THIS')
self.assertEqual('<<<<<<< OTHER\rc\r=======\rb\r'
'>>>>>>> THIS\r'.splitlines(True), list(m_lines))
+
+ def test_merge3_cherrypick(self):
+ base_text = "a\nb\n"
+ this_text = "a\n"
+ other_text = "a\nb\nc\n"
+ # When cherrypicking, lines in base are not part of the conflict
+ m3 = Merge3(base_text.splitlines(True), this_text.splitlines(True),
+ other_text.splitlines(True), is_cherrypick=True)
+ m_lines = m3.merge_lines()
+ self.assertEqualDiff('a\n<<<<<<<\n=======\nc\n>>>>>>>\n',
+ ''.join(m_lines))
+
+ # This is not symmetric
+ m3 = Merge3(base_text.splitlines(True), other_text.splitlines(True),
+ this_text.splitlines(True), is_cherrypick=True)
+ m_lines = m3.merge_lines()
+ self.assertEqualDiff('a\n<<<<<<<\nb\nc\n=======\n>>>>>>>\n',
+ ''.join(m_lines))
+
+ def test_merge3_cherrypick_w_mixed(self):
+ base_text = 'a\nb\nc\nd\ne\n'
+ this_text = 'a\nb\nq\n'
+ other_text = 'a\nb\nc\nd\nf\ne\ng\n'
+ # When cherrypicking, lines in base are not part of the conflict
+ m3 = Merge3(base_text.splitlines(True), this_text.splitlines(True),
+ other_text.splitlines(True), is_cherrypick=True)
+ m_lines = m3.merge_lines()
+ self.assertEqualDiff('a\n'
+ 'b\n'
+ '<<<<<<<\n'
+ 'q\n'
+ '=======\n'
+ 'f\n'
+ '>>>>>>>\n'
+ '<<<<<<<\n'
+ '=======\n'
+ 'g\n'
+ '>>>>>>>\n',
+ ''.join(m_lines))
More information about the bazaar-commits
mailing list