Rev 3554: Several updates. in http://bzr.arbash-meinel.com/branches/bzr/1.7-dev/merge_lca_multi

John Arbash Meinel john at arbash-meinel.com
Thu Jul 31 20:13:07 BST 2008


At http://bzr.arbash-meinel.com/branches/bzr/1.7-dev/merge_lca_multi

------------------------------------------------------------
revno: 3554
revision-id: john at arbash-meinel.com-20080731191259-81qy04c3h4bxir0s
parent: john at arbash-meinel.com-20080730223829-2wd0dm1ca4tqlguw
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: merge_lca_multi
timestamp: Thu 2008-07-31 14:12:59 -0500
message:
  Several updates.
  
  Add a document describing the merge algorithm, and the rationale
  for the different steps.
  Change the sha1 algorithm to use get_file_sha1() rather than assuming the
  inventory entries text_sha1 value is correct. (It is always None for working trees.)
  For sha1-values specifically, don't allow overruling an LCA value when one tip
  seems to supersede the other's value. We will still do this for names and parent_ids.
  Add tests that we conflict if lcas disagree and tips do not have the same content,
  even if one of them is an lca value, and one is newer.
-------------- next part --------------
=== modified file 'bzrlib/merge.py'
--- a/bzrlib/merge.py	2008-07-30 22:37:49 +0000
+++ b/bzrlib/merge.py	2008-07-31 19:12:59 +0000
@@ -711,16 +711,19 @@
             if interesting_ids is not None and file_id not in interesting_ids:
                 continue
 
-            # I believe we can actually change this to see if last_rev is
-            # identical to *any* of the lca values. Though we should actually
-            # use the _lca_multi_way logic. However, it may be worthwhile to
-            # shortcut entries that are identical in all of LCA + OTHER, just
-            # to avoid the overhead of looking up information in BASE and THIS.
+            # If other_revision is found in any of the lcas, that means this
+            # node is uninteresting. This is because when merging, if there are
+            # multiple heads(), we have to create a new node. So if we didn't,
+            # we know that the ancestry is linear, and that OTHER did not
+            # modify anything
+            # See doc/developers/lca_merge_resolution.txt for details
             other_revision = other_ie.revision
+            is_unmodified = False
             for lca_path, ie in lca_values:
-                if ie is None or ie.revision != other_revision:
+                if ie is not None and ie.revision == other_revision:
+                    is_unmodified = True
                     break
-            else: # Identical in all trees
+            if is_unmodified:
                 continue
 
             lca_entries = []
@@ -744,13 +747,11 @@
             lca_parent_ids = []
             lca_names = []
             lca_executable = []
-            lca_sha1s = []
             for lca_ie in lca_entries:
                 lca_kinds.append(lca_ie.kind)
                 lca_parent_ids.append(lca_ie.parent_id)
                 lca_names.append(lca_ie.name)
                 lca_executable.append(lca_ie.executable)
-                lca_sha1s.append(lca_ie.text_sha1)
 
             kind_winner = Merge3Merger._lca_multi_way(
                 (base_ie.kind, lca_kinds),
@@ -775,9 +776,18 @@
                         continue
                     content_changed = False
                 elif other_ie.kind == 'file':
+                    def get_sha1(ie, tree):
+                        if ie.kind is None:
+                            return None
+                        return tree.get_file_sha1(file_id)
+                    base_sha1 = get_sha1(base_ie, self.base_tree)
+                    lca_sha1s = [get_sha1(ie, tree) for ie, tree
+                                 in zip(lca_entries, self._lca_trees)]
+                    this_sha1 = get_sha1(this_ie, self.this_tree)
+                    other_sha1 = get_sha1(other_ie, self.other_tree)
                     sha1_winner = Merge3Merger._lca_multi_way(
-                        (base_ie.text_sha1, lca_sha1s),
-                        other_ie.text_sha1, this_ie.text_sha1)
+                        (base_sha1, lca_sha1s), other_sha1, this_sha1,
+                        allow_overriding_lca=False)
                     exec_winner = Merge3Merger._lca_multi_way(
                         (base_ie.executable, lca_executable),
                         other_ie.executable, this_ie.executable)
@@ -907,7 +917,7 @@
             return "other"
 
     @staticmethod
-    def _lca_multi_way(bases, other, this):
+    def _lca_multi_way(bases, other, this, allow_overriding_lca=True):
         """Consider LCAs when determining whether a change has occurred.
 
         If LCAS are all identical, this is the same as a _three_way comparison.
@@ -915,9 +925,15 @@
         :param bases: value in (BASE, [LCAS])
         :param other: value in OTHER
         :param this: value in THIS
+        :param allow_overriding_lca: If there is more than one unique lca
+            value, allow OTHER to override THIS if it has a new value, and
+            THIS only has an lca value, or vice versa. This is appropriate for
+            truly scalar values, not as much for non-scalars.
         :return: 'this', 'other', or 'conflict' depending on whether an entry
             changed or not.
         """
+        # See doc/developers/lca_merge_resolution.txt for details about this
+        # algorithm.
         if other == this:
             # Either Ambiguously clean, or nothing was actually changed. We
             # don't really care
@@ -928,25 +944,24 @@
                                       if lca_val != base_val]
         if len(filtered_lca_vals) == 0:
             return Merge3Merger._three_way(base_val, other, this)
-        else:
-            first_lca_val = filtered_lca_vals[0]
-            for lca_val in filtered_lca_vals[1:]:
-                if lca_val != first_lca_val:
-                    break
-            else: # all lcas are equal, use 3-way logic
-                return Merge3Merger._three_way(first_lca_val, other, this)
-        if other in filtered_lca_vals:
-            if this in filtered_lca_vals:
-                # Each side picked a different lca, conflict
-                return 'conflict'
-            else:
-                # This has a value which supersedes both lca values, and other
+
+        unique_lca_vals = set(filtered_lca_vals)
+        if len(unique_lca_vals) == 1:
+            return Merge3Merger._three_way(unique_lca_vals.pop(), other, this)
+
+        if allow_overriding_lca:
+            if other in unique_lca_vals:
+                if this in unique_lca_vals:
+                    # Each side picked a different lca, conflict
+                    return 'conflict'
+                else:
+                    # This has a value which supersedes both lca values, and
+                    # other only has an lca value
+                    return 'this'
+            elif this in unique_lca_vals:
+                # OTHER has a value which supersedes both lca values, and this
                 # only has an lca value
-                return 'this'
-        elif this in filtered_lca_vals:
-            # OTHER has a value which supersedes both lca values, and this only
-            # has an lca value
-            return 'other'
+                return 'other'
 
         # At this point, the lcas disagree, and the tips disagree
         return 'conflict'

=== modified file 'bzrlib/tests/test_merge.py'
--- a/bzrlib/tests/test_merge.py	2008-07-30 22:38:29 +0000
+++ b/bzrlib/tests/test_merge.py	2008-07-31 19:12:59 +0000
@@ -1238,6 +1238,44 @@
         self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
         self.assertFalse('lca_trees' in merge_obj.kwargs)
 
+# XXX: Thing left to test:
+#       Double-criss-cross merge, the ultimate base value is different from the
+#       intermediate.
+#         A    value 'foo'
+#         |\
+#         B C  B value 'bar', C = 'foo'
+#         |X|
+#         D E  D = 'bar', E supersedes to 'bing'
+#         |X|
+#         F G  F = 'bing', G supersedes to 'barry'
+#
+#       In this case, we technically should not care about the value 'bar' for
+#       D, because it was clearly superseded by E's 'bing'. The
+#       per-file/attribute graph would actually look like:
+#         A
+#         |
+#         B
+#         |
+#         E
+#         |
+#         G
+#
+#       Because the other side of the merge never modifies the value, it just
+#       takes the value from the merge.
+#
+#       ATM I expect this to fail because we will prune 'foo' from the LCAs,
+#       and not 'bar'. I don't know if this is strictly a problem, as it is a
+#       distinctly edge case.
+#
+#       Another incorrect resolution from the same basic flaw:
+#         A    value 'foo'
+#         |\
+#         B C  B value 'bar', C = 'foo'
+#         |X|
+#         D E  D = 'bar', E reverts to 'foo'
+#         |X|
+#         F G  F = 'bar', G reverts to 'foo'
+
 
 class TestMergerEntriesLCA(TestMergerBase):
 
@@ -1383,6 +1421,32 @@
                            ((False, [False, False]), None, False)),
                          ], entries)
 
+    def test_not_in_other_or_lca(self):
+        #       A    base, introduces 'foo'
+        #       |\ 
+        #       B C  B nothing, C deletes foo
+        #       |X|
+        #       D E  D restores foo (same as B), E leaves it deleted
+        # We should emit an entry for this
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'content\n'))])
+        builder.build_snapshot('B-id', ['A-id'], [])
+        builder.build_snapshot('C-id', ['A-id'],
+            [('unversion', 'foo-id')])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
+        merge_obj = self.make_merge_obj(builder, 'E-id')
+
+        entries = list(merge_obj._entries_lca())
+        root_id = 'a-root-id'
+        self.assertEqual([('foo-id', True,
+                           ((root_id, [root_id, None]), None, root_id),
+                           ((u'foo', [u'foo', None]), None, 'foo'),
+                           ((False, [False, None]), None, False)),
+                         ], entries)
+
     def test_only_in_one_lca(self):
         builder = self.get_builder()
         builder.build_snapshot('A-id', None,
@@ -1422,6 +1486,133 @@
                            ((None, [None, None]), False, None)),
                          ], entries)
 
+    def test_one_lca_supersedes(self):
+        # One LCA supersede's the other LCA's last modified value, but the
+        # value is not the same as BASE.
+        #       A    base, introduces 'foo', last mod A
+        #       |\ 
+        #       B C  B modifies 'foo' (mod B), C does nothing (mod A)
+        #       |X|
+        #       D E  D does nothing (mod B), E updates 'foo' (mod E)
+        #       |X|
+        #       F G  F updates 'foo' (mod F). G does nothing (mod E)
+        #
+        #   At this point, G should not be considered to modify 'foo', even
+        #   though its LCAs disagree. This is because the modification in E
+        #   completely supersedes the value in D.
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
+        builder.build_snapshot('C-id', ['A-id'], [])
+        builder.build_snapshot('B-id', ['A-id'],
+            [('modify', ('foo-id', 'B content\n'))])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'],
+            [('modify', ('foo-id', 'E content\n'))])
+        builder.build_snapshot('G-id', ['E-id', 'D-id'], [])
+        builder.build_snapshot('F-id', ['D-id', 'E-id'],
+            [('modify', ('foo-id', 'F content\n'))])
+        merge_obj = self.make_merge_obj(builder, 'G-id')
+
+        self.assertEqual([], list(merge_obj._entries_lca()))
+
+    def test_both_sides_revert(self):
+        # Both sides of a criss-cross revert the text to the lca
+        #       A    base, introduces 'foo'
+        #       |\ 
+        #       B C  B modifies 'foo', C modifies 'foo'
+        #       |X|
+        #       D E  D reverts to B, E reverts to C
+        # This should conflict
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
+        builder.build_snapshot('B-id', ['A-id'],
+            [('modify', ('foo-id', 'B content\n'))])
+        builder.build_snapshot('C-id', ['A-id'],
+            [('modify', ('foo-id', 'C content\n'))])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
+        merge_obj = self.make_merge_obj(builder, 'E-id')
+
+        entries = list(merge_obj._entries_lca())
+        root_id = 'a-root-id'
+        self.assertEqual([('foo-id', True,
+                           ((root_id, [root_id, root_id]), root_id, root_id),
+                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
+                           ((False, [False, False]), False, False)),
+                         ], entries)
+
+    def test_different_lca_resolve_one_side_updates_content(self):
+        # Both sides converge, but then one side updates the text.
+        #       A    base, introduces 'foo'
+        #       |\ 
+        #       B C  B modifies 'foo', C modifies 'foo'
+        #       |X|
+        #       D E  D reverts to B, E reverts to C
+        #       |
+        #       F    F updates to a new value
+        # We need to emit an entry for 'foo', because D & E differed on the
+        # merge resolution
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
+        builder.build_snapshot('B-id', ['A-id'],
+            [('modify', ('foo-id', 'B content\n'))])
+        builder.build_snapshot('C-id', ['A-id'],
+            [('modify', ('foo-id', 'C content\n'))])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
+        builder.build_snapshot('F-id', ['D-id'],
+            [('modify', ('foo-id', 'F content\n'))])
+        merge_obj = self.make_merge_obj(builder, 'E-id')
+
+        entries = list(merge_obj._entries_lca())
+        root_id = 'a-root-id'
+        self.assertEqual([('foo-id', True,
+                           ((root_id, [root_id, root_id]), root_id, root_id),
+                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
+                           ((False, [False, False]), False, False)),
+                         ], entries)
+
+    def test_same_lca_resolution_one_side_updates_content(self):
+        # Both sides converge, but then one side updates the text.
+        #       A    base, introduces 'foo'
+        #       |\ 
+        #       B C  B modifies 'foo', C modifies 'foo'
+        #       |X|
+        #       D E  D and E use C's value
+        #       |
+        #       F    F updates to a new value
+        # I think it is a bug that this conflicts, but we don't have a way to
+        # detect otherwise. And because of:
+        #   test_different_lca_resolve_one_side_updates_content
+        # We need to conflict.
+
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
+        builder.build_snapshot('B-id', ['A-id'],
+            [('modify', ('foo-id', 'B content\n'))])
+        builder.build_snapshot('C-id', ['A-id'],
+            [('modify', ('foo-id', 'C content\n'))])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'],
+            [('modify', ('foo-id', 'C content\n'))]) # Same as E
+        builder.build_snapshot('F-id', ['D-id'],
+            [('modify', ('foo-id', 'F content\n'))])
+        merge_obj = self.make_merge_obj(builder, 'E-id')
+
+        entries = list(merge_obj._entries_lca())
+        root_id = 'a-root-id'
+        self.expectFailure("We don't detect that LCA resolution was the"
+                           " same on both sides",
+            self.assertEqual, [], entries)
+
     def test_only_path_changed(self):
         builder = self.get_builder()
         builder.build_snapshot('A-id', None,
@@ -1761,6 +1952,35 @@
         self.assertEqual('foo-id', wt.path2id('foo'))
         self.assertEqual('bar', wt.get_symlink_target('foo-id'))
 
+    def test_both_sides_revert(self):
+        # Both sides of a criss-cross revert the text to the lca
+        #       A    base, introduces 'foo'
+        #       |\ 
+        #       B C  B modifies 'foo', C modifies 'foo'
+        #       |X|
+        #       D E  D reverts to B, E reverts to C
+        # This should conflict
+        # This must be done with a real WorkingTree, because normally their
+        # inventory contains "None" rather than a real sha1
+        builder = self.get_builder()
+        builder.build_snapshot('A-id', None,
+            [('add', (u'', 'a-root-id', 'directory', None)),
+             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
+        builder.build_snapshot('B-id', ['A-id'],
+            [('modify', ('foo-id', 'B content\n'))])
+        builder.build_snapshot('C-id', ['A-id'],
+            [('modify', ('foo-id', 'C content\n'))])
+        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
+        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
+        wt, conflicts = self.do_merge(builder, 'E-id')
+        self.assertEqual(1, conflicts)
+        self.assertEqualDiff('<<<<<<< TREE\n'
+                             'B content\n'
+                             '=======\n'
+                             'C content\n'
+                             '>>>>>>> MERGE-SOURCE\n',
+                             wt.get_file_text('foo-id'))
+
     def test_modified_symlink(self):
         self.requireFeature(tests.SymlinkFeature)
         #   A       Create symlink foo => bar
@@ -1983,9 +2203,11 @@
 
 class TestLCAMultiWay(tests.TestCase):
 
-    def assertLCAMultiWay(self, expected, base, lcas, other, this):
+    def assertLCAMultiWay(self, expected, base, lcas, other, this,
+                          allow_overriding_lca=True):
         self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
-                                        (base, lcas), other, this))
+                                (base, lcas), other, this,
+                                allow_overriding_lca=allow_overriding_lca))
 
     def test_other_equal_equal_lcas(self):
         """Test when OTHER=LCA and all LCAs are identical."""
@@ -2061,12 +2283,24 @@
             'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
         self.assertLCAMultiWay('this',
             'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval')
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval',
+            allow_overriding_lca=False)
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval',
+            allow_overriding_lca=False)
         # THIS reverted back to BASE, but that is an explicit supersede of all
         # LCAs
         self.assertLCAMultiWay('this',
             'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval')
         self.assertLCAMultiWay('this',
             'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval',
+            allow_overriding_lca=False)
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval',
+            allow_overriding_lca=False)
 
     def test_this_in_lca(self):
         # THIS takes a value of one of the LCAs, OTHER takes a new value, which
@@ -2075,10 +2309,19 @@
             'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
         self.assertLCAMultiWay('other',
             'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val',
+            allow_overriding_lca=False)
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val',
+            allow_overriding_lca=False)
         # OTHER reverted back to BASE, but that is an explicit supersede of all
         # LCAs
         self.assertLCAMultiWay('other',
             'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val')
+        self.assertLCAMultiWay('conflict',
+            'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val',
+            allow_overriding_lca=False)
 
     def test_all_differ(self):
         self.assertLCAMultiWay('conflict',

=== added file 'doc/developers/lca_merge_resolution.txt'
--- a/doc/developers/lca_merge_resolution.txt	1970-01-01 00:00:00 +0000
+++ b/doc/developers/lca_merge_resolution.txt	2008-07-31 19:12:59 +0000
@@ -0,0 +1,127 @@
+====================
+LCA Merge Resolution
+====================
+
+There are 2 ways that you get LCA merge resolution in bzr. First, if you use
+``bzr merge --lca``, the content of files will be resolved using a Least Common
+Ancestors algorithm. That is not being described here.
+
+This is describing how we handle merging tree-shape when there is not a single
+unique ancestor (criss-cross merge). With a single LCA, we use simple
+3-way-merge logic.
+
+When there are multiple possible LCAs, we use a different algorithm for
+handling tree-shape merging. Described here.
+
+As a simple example, here is a revision graph which we will refer to often::
+
+  .    BASE
+  .  /      \
+  . LCA1   LCA2
+  . |   \ /   |
+  . |    X    |
+  . |   / \   |
+  . THIS  OTHER
+
+In this graph, ``THIS`` and ``OTHER`` both have ``LCA1`` and ``LCA2`` in their
+ancestry but neither is an ancestor of the other, so we have 2 least common
+ancestors. The unique common ancestor is ``BASE``. (It should be noted that in
+this text we will talk directly about ``LCA1`` and ``LCA2``, but the algorithms
+are designed to cope with more than 2 LCAs.)
+
+
+Scalars
+=======
+
+Definition
+----------
+
+I'm defining scalar values as ones that cannot be 'merged' on their own. For
+example, the name of a file is "scalar". If one person changes "foo.txt" to
+"foo.c" and someone else changes "foo.txt" to "bar.txt" we don't merge the
+changes to be "bar.c", we simply conflict and expect the user to sort it out.
+
+We use a slightly different algorithm for scalars.
+
+
+Resolution Algorithm
+--------------------
+
+(This can be seen as ``bzrlib.merge.Merge3Merger._lca_multi_way```
+
+1. If ``THIS`` and ``OTHER`` have the same value, use it. There is no need to
+   inspect any other values in this case. Either nothing was changed (all
+   interesting nodes would have the same value), or we have "accidental
+   convergence" (both sides made the same change.).
+
+2. Find the values from ``LCA1`` and ``LCA2`` which are not the same as
+   ``BASE``. The idea here is to provide a rudimentary "heads" comparison.
+   Often, the whole tree graph will have a criss-cross, but the per-file
+   (per-scalar) graph would be linear. And the value in one LCA strictly
+   dominates the other. It is possible to construct a scenario where one side
+   dominates the other, but the dominated value is not ``BASE``, but a second
+   intermediate value. Most scalars are rarely changed, so this is unlikely to
+   be an issue. And the trade-off is having to generate and inspect the
+   per-scalar graph.
+
+   If there are no LCA values that are different from ``BASE``, we use a simple
+   3-way merge with ``BASE`` as the base value.
+
+3. Find the unique set of LCA values that do not include the ``BASE`` value.
+   If there is only one unique LCA value, we again use three-way merge logic
+   using that unique value as the base.
+
+4. At this point we have determined that we have at least 2 unique values in
+   our LCAs which means that ``THIS`` and ``OTHER`` would both have to resolve
+   the conflict. If they resolved it in the same way, we would have caught that
+   in step 1. So they either both picked a different LCA value, or one (or
+   both) chose a new value to use.
+
+   So at this point, if ``OTHER`` and ``THIS`` both picked a different LCA
+   value, we conflict.
+
+   If ``OTHER`` and ``THIS`` both have values that are not LCA values, we also
+   conflict. (Same as 3-way, both sides modified a value in different ways.)
+
+5. (optional) The only tricky part is this, if ``OTHER`` has a LCA value, but
+   ``THIS`` does not, then we go with ``THIS``, and conversely if ``THIS`` has
+   an LCA value, but ``OTHER`` does not, then we go with ``OTHER``. The idea is
+   that ``THIS`` and ``OTHER`` may have resolved things in the same way, and
+   then later changed the value to something newer. (They could have also
+   resolved it differently, and then one side updated again.)
+
+
+``InventoryEntry.revision``
+---------------------------
+
+The last-modified revision for an entry gets treated differently. This is
+because how it is generated allows us to infer more information. Specifically,
+any time there is a change to an entry (rename, or content change) the last
+modified revision is updated. Further, if we are merging, and both sides
+updated the entry, then we update the last-modified revision at the merge
+point.
+
+For a picture example::
+
+    .   A
+    .  / \
+    . B   C
+    .  \ /
+    .   D
+
+
+For a single entry, the last modified revision in ``D`` is:
+
+1) ``A`` if neither ``B`` or ``C`` modified it
+2) ``B`` if ``B`` modified and ``C`` did not
+3) ``C`` if ``C`` modified and ``B`` did not
+4) ``D`` if ``B`` and ``C`` modified it
+
+This means that if the last modified revision is the same, there have been no
+changes in the intermediate time. And if ``OTHER`` has the same last modified
+revision as *any* LCA, then we know that all other LCAs' last-modified
+revisions are in the ancestry of that value. (Otherwise, when ``OTHER`` would
+need to create a new last modified revision as part of the merge.)
+
+..
+   vim: ft=rst tw=74 ai



More information about the bazaar-commits mailing list