Merge lp:~garyvdm/bzr/bug588698-criss-cross-null-base into lp:bzr

Proposed by Gary van der Merwe
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 5543
Proposed branch: lp:~garyvdm/bzr/bug588698-criss-cross-null-base
Merge into: lp:bzr
Diff against target: 152 lines (+61/-7)
5 files modified
bzrlib/branchbuilder.py (+5/-1)
bzrlib/merge.py (+15/-6)
bzrlib/tests/test_branchbuilder.py (+15/-0)
bzrlib/tests/test_merge.py (+20/-0)
doc/en/release-notes/bzr-2.3.txt (+6/-0)
To merge this branch: bzr merge lp:~garyvdm/bzr/bug588698-criss-cross-null-base
Reviewer Review Type Date Requested Status
John A Meinel Needs Information
Review via email: mp+41047@code.launchpad.net

Description of the change

This makes merge choose one of the lcas as the base revision when it cannot locate a unique lca, for example where there is a criss-cross merge of a new root.

It chooses the lca by taking the first lca from find_merge_order(lcas). This seems to give the most sensible for all the user tests I have run.

This has a drive-by fix to make BranchBuilder.build_snapshot accept parent_ids == ['null:']. This was done to make it easier to write a test using BranchBuilder that introduces a new root into the graph.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (3.3 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/17/2010 7:44 AM, Gary van der Merwe wrote:
> Gary van der Merwe has proposed merging lp:~garyvdm/bzr/bug588698-criss-cross-null-base into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
> Related bugs:
> #588698 "bzr merge" fails "Branches have no common ancestor"
> https://bugs.launchpad.net/bugs/588698
>
>
> This makes merge choose one of the lcas as the base revision when it cannot locate a unique lca, for example where there is a criss-cross merge of a new root.
>
> It chooses the lca by taking the first lca from find_merge_order(lcas). This seems to give the most sensible for all the user tests I have run.
>
> This has a drive-by fix to make BranchBuilder.build_snapshot accept parent_ids == ['null:']. This was done to make it easier to write a test using BranchBuilder that introduces a new root into the graph.

- - if parent_ids is not None:
+ if parent_ids == [revision.NULL_REVISION]:

^- Are we sure parent_ids is always a list? Could it be a tuple:
(revision.NULL_REVISION,).

I think it usually is. However, you shouldn't need to pass
[NULL_REVISION] either. Just pass the empty list (I think).

+ builder.build_snapshot('B-id', [],
+ [('add', ('', None, 'directory', None))])

I'm not positive if that works, but it does actually match certain bits
of code better. ('null:' isn't saved to the index files, for example).

I'd have to dig a bit deeper to sort out the rest. However, this snippet
looks worrying:

- - self._is_criss_cross = True
+ sorted_lca_keys = self.revision_graph.find_merge_order(

+ revisions[0], lcas)
+ if self.base_rev_id == _mod_revision.NULL_REVISION:
+ self.base_rev_id = sorted_lca_keys[0]
+

graph.find_merge_order is not a cheap operation, because it has to walk
the ancestry. Could we do it only in the 'if' block:

if self.base_rev_id == _mod_revision.NULL_REVISION:
    trace.mutter("hit the NULL_REVISION as the unique LCA."
                 " Picking the 'first' lca as BASE")
    sorted_lca_keys = self.revision_graph.find_merge_order(revisions[0],
lcas)
    self.base_rev_id = sorted_lca_keys[0]

...

                 interesting_revision_ids = [self.base_rev_id]
                 interesting_revision_ids.extend(lcas)
+ interesting_revision_ids =
list(set(interesting_revision_ids))

^- This doesn't preserve the ordering of interesting_revision_ids. But
since it looks like we don't need the specific order, why not just use a
set and not cast to a list in the first place?

I see that 'sorted_lca_keys' is used again right after this point. but
is it used in all possible cases?

Anyway, the basic concept seems fine. Failing down to NULL_REVISION is
bad, picking a 'best guess' LCA seems reasonable. We may want to inform
the user that we did this, in case it would be confusing why X or Y
didn't happen. At least a mutter() helps us debug it if it is happening.

John
=:->

 review: needs_information
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev....

Read more...

review: Needs Information
Revision history for this message
Gary van der Merwe (garyvdm) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 17/11/2010 23:05, John Arbash Meinel wrote:
> I think it usually is. However, you shouldn't need to pass
> [NULL_REVISION] either. Just pass the empty list (I think).
>
> + builder.build_snapshot('B-id', [],
> + [('add', ('', None, 'directory', None))])
>
> I'm not positive if that works, but it does actually match certain bits
> of code better. ('null:' isn't saved to the index files, for example).

Ok. I've updated the code to make it possible to pass [] to get a null
parent.

> graph.find_merge_order is not a cheap operation, because it has to walk
> the ancestry. Could we do it only in the 'if' block:
....
> I see that 'sorted_lca_keys' is used again right after this point. but
> is it used in all possible cases?

Previously, we always call graph.find_merge_order if we encounter a
criss-cross merge. It is used later to sort interesting_trees. I have
just moved it's execution earlier.

>
> interesting_revision_ids = [self.base_rev_id]
> interesting_revision_ids.extend(lcas)
> + interesting_revision_ids =
> list(set(interesting_revision_ids))
>
> ^- This doesn't preserve the ordering of interesting_revision_ids. But
> since it looks like we don't need the specific order, why not just use a
> set and not cast to a list in the first place?

Ah yes, when I wrote it that line, I meant to go back and change it, but
I forgot to. Fixed now.

> Anyway, the basic concept seems fine. Failing down to NULL_REVISION is
> bad, picking a 'best guess' LCA seems reasonable.

Great.

> We may want to inform the user that we did this, in case it would be
> confusing why X or Y didn't happen. At least a mutter() helps us
> debug it if it is happening.

I don't think that the behavior changes much compared to a criss-cross
merge, and so I don't think a warning is appropriate. I have added a mutter.

Regards,

Gary
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkzk5pYACgkQd/3EdwGKOh1IfACgnKiuHCi2+ra9ffOSiNgxus31
S9wAn1GtkQoM6nUyxMcGV7vbbUEKmhS/
=PvTF
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/branchbuilder.py'
2--- bzrlib/branchbuilder.py 2010-02-27 12:27:33 +0000
3+++ bzrlib/branchbuilder.py 2010-11-18 08:27:12 +0000
4@@ -21,6 +21,7 @@
5 commit,
6 errors,
7 memorytree,
8+ revision,
9 )
10
11
12@@ -186,7 +187,10 @@
13 :return: The revision_id of the new commit
14 """
15 if parent_ids is not None:
16- base_id = parent_ids[0]
17+ if len(parent_ids) == 0:
18+ base_id = revision.NULL_REVISION
19+ else:
20+ base_id = parent_ids[0]
21 if base_id != self._branch.last_revision():
22 self._move_branch_pointer(base_id,
23 allow_leftmost_as_ghost=allow_leftmost_as_ghost)
24
25=== modified file 'bzrlib/merge.py'
26--- bzrlib/merge.py 2010-08-25 13:02:32 +0000
27+++ bzrlib/merge.py 2010-11-18 08:27:12 +0000
28@@ -582,6 +582,7 @@
29 elif len(lcas) == 1:
30 self.base_rev_id = list(lcas)[0]
31 else: # len(lcas) > 1
32+ self._is_criss_cross = True
33 if len(lcas) > 2:
34 # find_unique_lca can only handle 2 nodes, so we have to
35 # start back at the beginning. It is a shame to traverse
36@@ -592,22 +593,30 @@
37 else:
38 self.base_rev_id = self.revision_graph.find_unique_lca(
39 *lcas)
40- self._is_criss_cross = True
41+ sorted_lca_keys = self.revision_graph.find_merge_order(
42+ revisions[0], lcas)
43+ if self.base_rev_id == _mod_revision.NULL_REVISION:
44+ self.base_rev_id = sorted_lca_keys[0]
45+
46 if self.base_rev_id == _mod_revision.NULL_REVISION:
47 raise errors.UnrelatedBranches()
48 if self._is_criss_cross:
49 trace.warning('Warning: criss-cross merge encountered. See bzr'
50 ' help criss-cross.')
51 trace.mutter('Criss-cross lcas: %r' % lcas)
52- interesting_revision_ids = [self.base_rev_id]
53- interesting_revision_ids.extend(lcas)
54+ if self.base_rev_id in lcas:
55+ trace.mutter('Unable to find unique lca. '
56+ 'Fallback %r as best option.' % self.base_rev_id)
57+ interesting_revision_ids = set(lcas)
58+ interesting_revision_ids.add(self.base_rev_id)
59 interesting_trees = dict((t.get_revision_id(), t)
60 for t in self.this_branch.repository.revision_trees(
61 interesting_revision_ids))
62 self._cached_trees.update(interesting_trees)
63- self.base_tree = interesting_trees.pop(self.base_rev_id)
64- sorted_lca_keys = self.revision_graph.find_merge_order(
65- revisions[0], lcas)
66+ if self.base_rev_id in lcas:
67+ self.base_tree = interesting_trees[self.base_rev_id]
68+ else:
69+ self.base_tree = interesting_trees.pop(self.base_rev_id)
70 self._lca_trees = [interesting_trees[key]
71 for key in sorted_lca_keys]
72 else:
73
74=== modified file 'bzrlib/tests/test_branchbuilder.py'
75--- bzrlib/tests/test_branchbuilder.py 2010-02-27 12:27:33 +0000
76+++ bzrlib/tests/test_branchbuilder.py 2010-11-18 08:27:12 +0000
77@@ -324,6 +324,21 @@
78 # should look like it was not modified in the merge
79 self.assertEqual('C-id', d_tree.inventory['c-id'].revision)
80
81+ def test_set_parent_to_null(self):
82+ builder = self.build_a_rev()
83+ builder.start_series()
84+ self.addCleanup(builder.finish_series)
85+ builder.build_snapshot('B-id', [],
86+ [('add', ('', None, 'directory', None))])
87+ # We should now have a graph:
88+ # A B
89+ # And not A => B
90+ repo = builder.get_branch().repository
91+ self.assertEqual({'A-id': (_mod_revision.NULL_REVISION,),
92+ 'B-id': (_mod_revision.NULL_REVISION,),},
93+ repo.get_parent_map(['A-id', 'B-id']))
94+
95+
96 def test_start_finish_series(self):
97 builder = BranchBuilder(self.get_transport().clone('foo'))
98 builder.start_series()
99
100=== modified file 'bzrlib/tests/test_merge.py'
101--- bzrlib/tests/test_merge.py 2010-07-29 04:07:27 +0000
102+++ bzrlib/tests/test_merge.py 2010-11-18 08:27:12 +0000
103@@ -1270,6 +1270,26 @@
104 self.assertEqual(['B-id', 'C-id', 'F-id'],
105 [t.get_revision_id() for t in merger._lca_trees])
106
107+ def test_find_base_new_root_criss_cross(self):
108+ # A B
109+ # |\ /|
110+ # | X |
111+ # |/ \|
112+ # C D
113+
114+ builder = self.get_builder()
115+ builder.build_snapshot('A-id', None,
116+ [('add', ('', None, 'directory', None))])
117+ builder.build_snapshot('B-id', [],
118+ [('add', ('', None, 'directory', None))])
119+ builder.build_snapshot('D-id', ['A-id', 'B-id'], [])
120+ builder.build_snapshot('C-id', ['A-id', 'B-id'], [])
121+ merger = self.make_Merger(builder, 'D-id')
122+ self.assertEqual('A-id', merger.base_rev_id)
123+ self.assertTrue(merger._is_criss_cross)
124+ self.assertEqual(['A-id', 'B-id'], [t.get_revision_id()
125+ for t in merger._lca_trees])
126+
127 def test_no_criss_cross_passed_to_merge_type(self):
128 class LCATreesMerger(LoggingMerger):
129 supports_lca_trees = True
130
131=== modified file 'doc/en/release-notes/bzr-2.3.txt'
132--- doc/en/release-notes/bzr-2.3.txt 2010-11-11 23:48:07 +0000
133+++ doc/en/release-notes/bzr-2.3.txt 2010-11-18 08:27:12 +0000
134@@ -52,6 +52,9 @@
135 is involved in a text conflict (but the conflict is still not
136 resolved). (Vincent Ladeuil, #646961)
137
138+* Merge will now correctly locate a lca where there is a criss-cross merge
139+ of a new root. (Gary van der Merwe, #588698)
140+
141 * Report error if non-ASCII command option given. (Rory Yorke, #140563)
142
143 Documentation
144@@ -71,6 +74,9 @@
145 .. Major internal changes, unlikely to be visible to users or plugin
146 developers, but interesting for bzr developers.
147
148+* ``BranchBuilder.build_snapshot`` now accepts parent_ids == [].
149+ This can be used to create a new root in the graph. (Gary van der Merwe)
150+
151 Testing
152 *******
153