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 14:25:47 GMT 2008


At http://bzr.arbash-meinel.com/branches/bzr/1.3-dev/cherrypick_merge

------------------------------------------------------------
revno: 3250
revision-id:john at arbash-meinel.com-20080304142546-zuwwy0o9roo14928
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 14:25:46 +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 14:25:46 +0000
@@ -116,7 +116,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 +164,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 +173,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 +403,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 +411,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 +434,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 +458,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 +504,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 +873,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 +1049,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 +1191,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 14:25:46 +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 14:25:46 +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 14:25:46 +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