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
=== modified file 'bzrlib/branchbuilder.py'
--- bzrlib/branchbuilder.py 2010-02-27 12:27:33 +0000
+++ bzrlib/branchbuilder.py 2010-11-18 08:27:12 +0000
@@ -21,6 +21,7 @@
21 commit,21 commit,
22 errors,22 errors,
23 memorytree,23 memorytree,
24 revision,
24 )25 )
2526
2627
@@ -186,7 +187,10 @@
186 :return: The revision_id of the new commit187 :return: The revision_id of the new commit
187 """188 """
188 if parent_ids is not None:189 if parent_ids is not None:
189 base_id = parent_ids[0]190 if len(parent_ids) == 0:
191 base_id = revision.NULL_REVISION
192 else:
193 base_id = parent_ids[0]
190 if base_id != self._branch.last_revision():194 if base_id != self._branch.last_revision():
191 self._move_branch_pointer(base_id,195 self._move_branch_pointer(base_id,
192 allow_leftmost_as_ghost=allow_leftmost_as_ghost)196 allow_leftmost_as_ghost=allow_leftmost_as_ghost)
193197
=== modified file 'bzrlib/merge.py'
--- bzrlib/merge.py 2010-08-25 13:02:32 +0000
+++ bzrlib/merge.py 2010-11-18 08:27:12 +0000
@@ -582,6 +582,7 @@
582 elif len(lcas) == 1:582 elif len(lcas) == 1:
583 self.base_rev_id = list(lcas)[0]583 self.base_rev_id = list(lcas)[0]
584 else: # len(lcas) > 1584 else: # len(lcas) > 1
585 self._is_criss_cross = True
585 if len(lcas) > 2:586 if len(lcas) > 2:
586 # find_unique_lca can only handle 2 nodes, so we have to587 # find_unique_lca can only handle 2 nodes, so we have to
587 # start back at the beginning. It is a shame to traverse588 # start back at the beginning. It is a shame to traverse
@@ -592,22 +593,30 @@
592 else:593 else:
593 self.base_rev_id = self.revision_graph.find_unique_lca(594 self.base_rev_id = self.revision_graph.find_unique_lca(
594 *lcas)595 *lcas)
595 self._is_criss_cross = True596 sorted_lca_keys = self.revision_graph.find_merge_order(
597 revisions[0], lcas)
598 if self.base_rev_id == _mod_revision.NULL_REVISION:
599 self.base_rev_id = sorted_lca_keys[0]
600
596 if self.base_rev_id == _mod_revision.NULL_REVISION:601 if self.base_rev_id == _mod_revision.NULL_REVISION:
597 raise errors.UnrelatedBranches()602 raise errors.UnrelatedBranches()
598 if self._is_criss_cross:603 if self._is_criss_cross:
599 trace.warning('Warning: criss-cross merge encountered. See bzr'604 trace.warning('Warning: criss-cross merge encountered. See bzr'
600 ' help criss-cross.')605 ' help criss-cross.')
601 trace.mutter('Criss-cross lcas: %r' % lcas)606 trace.mutter('Criss-cross lcas: %r' % lcas)
602 interesting_revision_ids = [self.base_rev_id]607 if self.base_rev_id in lcas:
603 interesting_revision_ids.extend(lcas)608 trace.mutter('Unable to find unique lca. '
609 'Fallback %r as best option.' % self.base_rev_id)
610 interesting_revision_ids = set(lcas)
611 interesting_revision_ids.add(self.base_rev_id)
604 interesting_trees = dict((t.get_revision_id(), t)612 interesting_trees = dict((t.get_revision_id(), t)
605 for t in self.this_branch.repository.revision_trees(613 for t in self.this_branch.repository.revision_trees(
606 interesting_revision_ids))614 interesting_revision_ids))
607 self._cached_trees.update(interesting_trees)615 self._cached_trees.update(interesting_trees)
608 self.base_tree = interesting_trees.pop(self.base_rev_id)616 if self.base_rev_id in lcas:
609 sorted_lca_keys = self.revision_graph.find_merge_order(617 self.base_tree = interesting_trees[self.base_rev_id]
610 revisions[0], lcas)618 else:
619 self.base_tree = interesting_trees.pop(self.base_rev_id)
611 self._lca_trees = [interesting_trees[key]620 self._lca_trees = [interesting_trees[key]
612 for key in sorted_lca_keys]621 for key in sorted_lca_keys]
613 else:622 else:
614623
=== modified file 'bzrlib/tests/test_branchbuilder.py'
--- bzrlib/tests/test_branchbuilder.py 2010-02-27 12:27:33 +0000
+++ bzrlib/tests/test_branchbuilder.py 2010-11-18 08:27:12 +0000
@@ -324,6 +324,21 @@
324 # should look like it was not modified in the merge324 # should look like it was not modified in the merge
325 self.assertEqual('C-id', d_tree.inventory['c-id'].revision)325 self.assertEqual('C-id', d_tree.inventory['c-id'].revision)
326326
327 def test_set_parent_to_null(self):
328 builder = self.build_a_rev()
329 builder.start_series()
330 self.addCleanup(builder.finish_series)
331 builder.build_snapshot('B-id', [],
332 [('add', ('', None, 'directory', None))])
333 # We should now have a graph:
334 # A B
335 # And not A => B
336 repo = builder.get_branch().repository
337 self.assertEqual({'A-id': (_mod_revision.NULL_REVISION,),
338 'B-id': (_mod_revision.NULL_REVISION,),},
339 repo.get_parent_map(['A-id', 'B-id']))
340
341
327 def test_start_finish_series(self):342 def test_start_finish_series(self):
328 builder = BranchBuilder(self.get_transport().clone('foo'))343 builder = BranchBuilder(self.get_transport().clone('foo'))
329 builder.start_series()344 builder.start_series()
330345
=== modified file 'bzrlib/tests/test_merge.py'
--- bzrlib/tests/test_merge.py 2010-07-29 04:07:27 +0000
+++ bzrlib/tests/test_merge.py 2010-11-18 08:27:12 +0000
@@ -1270,6 +1270,26 @@
1270 self.assertEqual(['B-id', 'C-id', 'F-id'],1270 self.assertEqual(['B-id', 'C-id', 'F-id'],
1271 [t.get_revision_id() for t in merger._lca_trees])1271 [t.get_revision_id() for t in merger._lca_trees])
12721272
1273 def test_find_base_new_root_criss_cross(self):
1274 # A B
1275 # |\ /|
1276 # | X |
1277 # |/ \|
1278 # C D
1279
1280 builder = self.get_builder()
1281 builder.build_snapshot('A-id', None,
1282 [('add', ('', None, 'directory', None))])
1283 builder.build_snapshot('B-id', [],
1284 [('add', ('', None, 'directory', None))])
1285 builder.build_snapshot('D-id', ['A-id', 'B-id'], [])
1286 builder.build_snapshot('C-id', ['A-id', 'B-id'], [])
1287 merger = self.make_Merger(builder, 'D-id')
1288 self.assertEqual('A-id', merger.base_rev_id)
1289 self.assertTrue(merger._is_criss_cross)
1290 self.assertEqual(['A-id', 'B-id'], [t.get_revision_id()
1291 for t in merger._lca_trees])
1292
1273 def test_no_criss_cross_passed_to_merge_type(self):1293 def test_no_criss_cross_passed_to_merge_type(self):
1274 class LCATreesMerger(LoggingMerger):1294 class LCATreesMerger(LoggingMerger):
1275 supports_lca_trees = True1295 supports_lca_trees = True
12761296
=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- doc/en/release-notes/bzr-2.3.txt 2010-11-11 23:48:07 +0000
+++ doc/en/release-notes/bzr-2.3.txt 2010-11-18 08:27:12 +0000
@@ -52,6 +52,9 @@
52 is involved in a text conflict (but the conflict is still not52 is involved in a text conflict (but the conflict is still not
53 resolved). (Vincent Ladeuil, #646961)53 resolved). (Vincent Ladeuil, #646961)
5454
55* Merge will now correctly locate a lca where there is a criss-cross merge
56 of a new root. (Gary van der Merwe, #588698)
57
55* Report error if non-ASCII command option given. (Rory Yorke, #140563)58* Report error if non-ASCII command option given. (Rory Yorke, #140563)
5659
57Documentation60Documentation
@@ -71,6 +74,9 @@
71.. Major internal changes, unlikely to be visible to users or plugin 74.. Major internal changes, unlikely to be visible to users or plugin
72 developers, but interesting for bzr developers.75 developers, but interesting for bzr developers.
7376
77* ``BranchBuilder.build_snapshot`` now accepts parent_ids == [].
78 This can be used to create a new root in the graph. (Gary van der Merwe)
79
74Testing80Testing
75*******81*******
7682