Merge lp:~jameinel/bzr/2.0-40412-show-base-weave into lp:bzr

Proposed by John A Meinel
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~jameinel/bzr/2.0-40412-show-base-weave
Merge into: lp:bzr
Diff against target: 514 lines (+341/-39)
6 files modified
bzrlib/merge.py (+36/-24)
bzrlib/tests/blackbox/test_merge.py (+16/-0)
bzrlib/tests/test_merge.py (+112/-0)
bzrlib/tests/test_merge_core.py (+46/-14)
bzrlib/versionedfile.py (+55/-1)
plan_to_base.py (+76/-0)
To merge this branch: bzr merge lp:~jameinel/bzr/2.0-40412-show-base-weave
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Fixing
Review via email: mp+15835@code.launchpad.net
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

This is a fairly small change to the merge code, such that 'bzr merge --weave' and --lca now put a .BASE file on disk along with the .THIS and .OTHER files.

The code is based off of the 2.0 branch, but for now I'm only proposing it for the 2.1 series. Adding a .BASE file on disk is a bit of a 'feature' change. Though not terribly so (given that 'bzr revert' will still clean them up, etc.) The api changes are generally internal functions with loose coupling, so should be ok.

I should also note that I don't have a NEWS entry, because it is based on the 2.0 branch (and thus I don't have a place to put NEWS for the 2.1 series.) I'll add it at merge time.

The basics here are that we walk the 'merge plan' and for each line status, we decide whether that means the content was in a hypothetical BASE. (eg, 'new-a' is newly added in one target, so obviously not in base, 'unchanged' is clearly in BASE, etc.)

In my testing, it gives the same results as a 3-way merge BASE would, except you are likely to use it in cases where 3-way merging doesn't quite cut it.

This code also suffers from:
https://bugs.edge.launchpad.net/bzr/+bug/494197

But I figure, better to land what I've got than try to track down all the edge cases and end up having a huge patch that nobody wants to review. :)

Revision history for this message
Vincent Ladeuil (vila) wrote :

Mentioning https://bugs.edge.launchpad.net/bzr/+bug/40412 and https://bugs.edge.launchpad.net/bzr/+bug/494204 here as courtesy URLs for those watching from home ;)

Do you really intend to land plan_to_base.py ?
If that's the case, contrib may be a more appropriate place no ?
Or did you just forget to delete it before submission (the code looks
like a preliminary version of your patch).

I don't know what happened here but lp is showing conflicts in the diff... which are not there when I review locally on my machine... tricked by the markers in the diff itself ? Weird.

Anyway, if plan_to_base.py is spurious, you can land without it.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/merge.py'
--- bzrlib/merge.py 2009-12-03 02:24:54 +0000
+++ bzrlib/merge.py 2009-12-08 21:10:33 +0000
@@ -1407,60 +1407,69 @@
1407 supports_reverse_cherrypick = False1407 supports_reverse_cherrypick = False
1408 history_based = True1408 history_based = True
14091409
1410 def _merged_lines(self, file_id):1410 def _generate_merge_plan(self, file_id, base):
1411 """Generate the merged lines.1411 return self.this_tree.plan_file_merge(file_id, self.other_tree,
1412 There is no distinction between lines that are meant to contain <<<<<<<
1413 and conflicts.
1414 """
1415 if self.cherrypick:
1416 base = self.base_tree
1417 else:
1418 base = None
1419 plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1420 base=base)1412 base=base)
1413
1414 def _merged_lines(self, file_id):
1415 """Generate the merged lines.
1416 There is no distinction between lines that are meant to contain <<<<<<<
1417 and conflicts.
1418 """
1419 if self.cherrypick:
1420 base = self.base_tree
1421 else:
1422 base = None
1423 plan = self._generate_merge_plan(file_id, base)
1421 if 'merge' in debug.debug_flags:1424 if 'merge' in debug.debug_flags:
1422 plan = list(plan)1425 plan = list(plan)
1423 trans_id = self.tt.trans_id_file_id(file_id)1426 trans_id = self.tt.trans_id_file_id(file_id)
1424 name = self.tt.final_name(trans_id) + '.plan'1427 name = self.tt.final_name(trans_id) + '.plan'
1425 contents = ('%10s|%s' % l for l in plan)1428 contents = ('%11s|%s' % l for l in plan)
1426 self.tt.new_file(name, self.tt.final_parent(trans_id), contents)1429 self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1430<<<<<<< TREE
1427 textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',1431 textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1428 '>>>>>>> MERGE-SOURCE\n')1432 '>>>>>>> MERGE-SOURCE\n')
1429 return textmerge.merge_lines(self.reprocess)1433 return textmerge.merge_lines(self.reprocess)
1434=======
1435 textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1436 '>>>>>>> MERGE-SOURCE\n')
1437 lines, conflicts = textmerge.merge_lines(self.reprocess)
1438 if conflicts:
1439 base_lines = textmerge.base_from_plan()
1440 else:
1441 base_lines = None
1442 return lines, base_lines
1443>>>>>>> MERGE-SOURCE
14301444
1431 def text_merge(self, file_id, trans_id):1445 def text_merge(self, file_id, trans_id):
1432 """Perform a (weave) text merge for a given file and file-id.1446 """Perform a (weave) text merge for a given file and file-id.
1433 If conflicts are encountered, .THIS and .OTHER files will be emitted,1447 If conflicts are encountered, .THIS and .OTHER files will be emitted,
1434 and a conflict will be noted.1448 and a conflict will be noted.
1435 """1449 """
1436 lines, conflicts = self._merged_lines(file_id)1450 lines, base_lines = self._merged_lines(file_id)
1437 lines = list(lines)1451 lines = list(lines)
1438 # Note we're checking whether the OUTPUT is binary in this case,1452 # Note we're checking whether the OUTPUT is binary in this case,
1439 # because we don't want to get into weave merge guts.1453 # because we don't want to get into weave merge guts.
1440 textfile.check_text_lines(lines)1454 textfile.check_text_lines(lines)
1441 self.tt.create_file(lines, trans_id)1455 self.tt.create_file(lines, trans_id)
1442 if conflicts:1456 if base_lines is not None:
1457 # Conflict
1443 self._raw_conflicts.append(('text conflict', trans_id))1458 self._raw_conflicts.append(('text conflict', trans_id))
1444 name = self.tt.final_name(trans_id)1459 name = self.tt.final_name(trans_id)
1445 parent_id = self.tt.final_parent(trans_id)1460 parent_id = self.tt.final_parent(trans_id)
1446 file_group = self._dump_conflicts(name, parent_id, file_id,1461 file_group = self._dump_conflicts(name, parent_id, file_id,
1447 no_base=True)1462 no_base=False,
1463 base_lines=base_lines)
1448 file_group.append(trans_id)1464 file_group.append(trans_id)
14491465
14501466
1451class LCAMerger(WeaveMerger):1467class LCAMerger(WeaveMerger):
14521468
1453 def _merged_lines(self, file_id):1469 def _generate_merge_plan(self, file_id, base):
1454 """Generate the merged lines.1470 return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1455 There is no distinction between lines that are meant to contain <<<<<<<
1456 and conflicts.
1457 """
1458 if self.cherrypick:
1459 base = self.base_tree
1460 else:
1461 base = None
1462 plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1463 base=base)1471 base=base)
1472<<<<<<< TREE
1464 if 'merge' in debug.debug_flags:1473 if 'merge' in debug.debug_flags:
1465 plan = list(plan)1474 plan = list(plan)
1466 trans_id = self.tt.trans_id_file_id(file_id)1475 trans_id = self.tt.trans_id_file_id(file_id)
@@ -1471,6 +1480,9 @@
1471 '>>>>>>> MERGE-SOURCE\n')1480 '>>>>>>> MERGE-SOURCE\n')
1472 return textmerge.merge_lines(self.reprocess)1481 return textmerge.merge_lines(self.reprocess)
14731482
1483=======
1484
1485>>>>>>> MERGE-SOURCE
14741486
1475class Diff3Merger(Merge3Merger):1487class Diff3Merger(Merge3Merger):
1476 """Three-way merger using external diff3 for text merging"""1488 """Three-way merger using external diff3 for text merging"""
14771489
=== modified file 'bzrlib/tests/blackbox/test_merge.py'
--- bzrlib/tests/blackbox/test_merge.py 2009-12-07 21:46:28 +0000
+++ bzrlib/tests/blackbox/test_merge.py 2009-12-08 21:10:33 +0000
@@ -211,6 +211,22 @@
211 self.failUnlessExists('sub/a.txt.OTHER')211 self.failUnlessExists('sub/a.txt.OTHER')
212 self.failUnlessExists('sub/a.txt.BASE')212 self.failUnlessExists('sub/a.txt.BASE')
213213
214 def test_conflict_leaves_base_this_other_files(self):
215 tree, other = self.create_conflicting_branches()
216 self.run_bzr('merge ../other', working_dir='tree',
217 retcode=1)
218 self.assertFileEqual('a\nb\nc\n', 'tree/fname.BASE')
219 self.assertFileEqual('a\nB\nD\n', 'tree/fname.OTHER')
220 self.assertFileEqual('a\nB\nC\n', 'tree/fname.THIS')
221
222 def test_weave_conflict_leaves_base_this_other_files(self):
223 tree, other = self.create_conflicting_branches()
224 self.run_bzr('merge ../other --weave', working_dir='tree',
225 retcode=1)
226 self.assertFileEqual('a\nb\nc\n', 'tree/fname.BASE')
227 self.assertFileEqual('a\nB\nD\n', 'tree/fname.OTHER')
228 self.assertFileEqual('a\nB\nC\n', 'tree/fname.THIS')
229
214 def test_merge_remember(self):230 def test_merge_remember(self):
215 """Merge changes from one branch to another, test submit location."""231 """Merge changes from one branch to another, test submit location."""
216 tree_a = self.make_branch_and_tree('branch_a')232 tree_a = self.make_branch_and_tree('branch_a')
217233
=== modified file 'bzrlib/tests/test_merge.py'
--- bzrlib/tests/test_merge.py 2009-09-19 00:32:14 +0000
+++ bzrlib/tests/test_merge.py 2009-12-08 21:10:33 +0000
@@ -38,6 +38,7 @@
38from bzrlib.workingtree import WorkingTree38from bzrlib.workingtree import WorkingTree
39from bzrlib.transform import TreeTransform39from bzrlib.transform import TreeTransform
4040
41
41class TestMerge(TestCaseWithTransport):42class TestMerge(TestCaseWithTransport):
42 """Test appending more than one revision"""43 """Test appending more than one revision"""
4344
@@ -524,6 +525,12 @@
524 self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')525 self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')
525 return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))526 return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))
526527
528 def test_base_from_plan(self):
529 self.setup_plan_merge()
530 plan = self.plan_merge_vf.plan_merge('B', 'C')
531 pwm = versionedfile.PlanWeaveMerge(plan)
532 self.assertEqual(['a\n', 'b\n', 'c\n'], pwm.base_from_plan())
533
527 def test_unique_lines(self):534 def test_unique_lines(self):
528 plan = self.setup_plan_merge()535 plan = self.setup_plan_merge()
529 self.assertEqual(plan._unique_lines(536 self.assertEqual(plan._unique_lines(
@@ -827,6 +834,111 @@
827 ('unchanged', 'f\n'),834 ('unchanged', 'f\n'),
828 ('unchanged', 'g\n')],835 ('unchanged', 'g\n')],
829 list(plan))836 list(plan))
837 plan = self.plan_merge_vf.plan_lca_merge('F', 'G')
838 # This is one of the main differences between plan_merge and
839 # plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
840 # because 'x' was not present in one of the bases. However, in this
841 # case it is spurious because 'x' does not exist in the global base A.
842 self.assertEqual([
843 ('unchanged', 'h\n'),
844 ('unchanged', 'a\n'),
845 ('conflicted-a', 'x\n'),
846 ('new-b', 'z\n'),
847 ('unchanged', 'c\n'),
848 ('unchanged', 'd\n'),
849 ('unchanged', 'y\n'),
850 ('unchanged', 'f\n'),
851 ('unchanged', 'g\n')],
852 list(plan))
853
854 def test_criss_cross_flip_flop(self):
855 # This is specificly trying to trigger problems when using limited
856 # ancestry and weaves. The ancestry graph looks like:
857 # XX unused ancestor, should not show up in the weave
858 # |
859 # A Unique LCA
860 # / \
861 # B C B & C both introduce a new line
862 # |\ /|
863 # | X |
864 # |/ \|
865 # D E B & C are both merged, so both are common ancestors
866 # In the process of merging, both sides order the new
867 # lines differently
868 #
869 self.add_rev('root', 'XX', [], 'qrs')
870 self.add_rev('root', 'A', ['XX'], 'abcdef')
871 self.add_rev('root', 'B', ['A'], 'abcdgef')
872 self.add_rev('root', 'C', ['A'], 'abcdhef')
873 self.add_rev('root', 'D', ['B', 'C'], 'abcdghef')
874 self.add_rev('root', 'E', ['C', 'B'], 'abcdhgef')
875 plan = list(self.plan_merge_vf.plan_merge('D', 'E'))
876 self.assertEqual([
877 ('unchanged', 'a\n'),
878 ('unchanged', 'b\n'),
879 ('unchanged', 'c\n'),
880 ('unchanged', 'd\n'),
881 ('new-b', 'h\n'),
882 ('unchanged', 'g\n'),
883 ('killed-b', 'h\n'),
884 ('unchanged', 'e\n'),
885 ('unchanged', 'f\n'),
886 ], plan)
887 pwm = versionedfile.PlanWeaveMerge(plan)
888 self.assertEqualDiff('\n'.join('abcdghef') + '\n',
889 ''.join(pwm.base_from_plan()))
890 # Reversing the order reverses the merge plan, and final order of 'hg'
891 # => 'gh'
892 plan = list(self.plan_merge_vf.plan_merge('E', 'D'))
893 self.assertEqual([
894 ('unchanged', 'a\n'),
895 ('unchanged', 'b\n'),
896 ('unchanged', 'c\n'),
897 ('unchanged', 'd\n'),
898 ('new-b', 'g\n'),
899 ('unchanged', 'h\n'),
900 ('killed-b', 'g\n'),
901 ('unchanged', 'e\n'),
902 ('unchanged', 'f\n'),
903 ], plan)
904 pwm = versionedfile.PlanWeaveMerge(plan)
905 self.assertEqualDiff('\n'.join('abcdhgef') + '\n',
906 ''.join(pwm.base_from_plan()))
907 # This is where lca differs, in that it (fairly correctly) determines
908 # that there is a conflict because both sides resolved the merge
909 # differently
910 plan = list(self.plan_merge_vf.plan_lca_merge('D', 'E'))
911 self.assertEqual([
912 ('unchanged', 'a\n'),
913 ('unchanged', 'b\n'),
914 ('unchanged', 'c\n'),
915 ('unchanged', 'd\n'),
916 ('conflicted-b', 'h\n'),
917 ('unchanged', 'g\n'),
918 ('conflicted-a', 'h\n'),
919 ('unchanged', 'e\n'),
920 ('unchanged', 'f\n'),
921 ], plan)
922 pwm = versionedfile.PlanWeaveMerge(plan)
923 self.assertEqualDiff('\n'.join('abcdgef') + '\n',
924 ''.join(pwm.base_from_plan()))
925 # Reversing it changes what line is doubled, but still gives a
926 # double-conflict
927 plan = list(self.plan_merge_vf.plan_lca_merge('E', 'D'))
928 self.assertEqual([
929 ('unchanged', 'a\n'),
930 ('unchanged', 'b\n'),
931 ('unchanged', 'c\n'),
932 ('unchanged', 'd\n'),
933 ('conflicted-b', 'g\n'),
934 ('unchanged', 'h\n'),
935 ('conflicted-a', 'g\n'),
936 ('unchanged', 'e\n'),
937 ('unchanged', 'f\n'),
938 ], plan)
939 pwm = versionedfile.PlanWeaveMerge(plan)
940 self.assertEqualDiff('\n'.join('abcdhef') + '\n',
941 ''.join(pwm.base_from_plan()))
830942
831 def assertRemoveExternalReferences(self, filtered_parent_map,943 def assertRemoveExternalReferences(self, filtered_parent_map,
832 child_map, tails, parent_map):944 child_map, tails, parent_map):
833945
=== modified file 'bzrlib/tests/test_merge_core.py'
--- bzrlib/tests/test_merge_core.py 2009-08-20 04:09:58 +0000
+++ bzrlib/tests/test_merge_core.py 2009-12-08 21:10:33 +0000
@@ -434,6 +434,7 @@
434 self.assertEqual('text2', builder.this.get_file('1').read())434 self.assertEqual('text2', builder.this.get_file('1').read())
435 builder.cleanup()435 builder.cleanup()
436436
437
437class FunctionalMergeTest(TestCaseWithTransport):438class FunctionalMergeTest(TestCaseWithTransport):
438439
439 def test_trivial_star_merge(self):440 def test_trivial_star_merge(self):
@@ -467,30 +468,61 @@
467 self.assertEqual("Mary\n", open("original/file2", "rt").read())468 self.assertEqual("Mary\n", open("original/file2", "rt").read())
468469
469 def test_conflicts(self):470 def test_conflicts(self):
470 os.mkdir('a')
471 wta = self.make_branch_and_tree('a')471 wta = self.make_branch_and_tree('a')
472 a = wta.branch472 self.build_tree_contents([('a/file', 'contents\n')])
473 file('a/file', 'wb').write('contents\n')
474 wta.add('file')473 wta.add('file')
475 wta.commit('base revision', allow_pointless=False)474 wta.commit('base revision', allow_pointless=False)
476 d_b = a.bzrdir.clone('b')475 d_b = wta.branch.bzrdir.clone('b')
477 b = d_b.open_branch()476 self.build_tree_contents([('a/file', 'other contents\n')])
478 file('a/file', 'wb').write('other contents\n')
479 wta.commit('other revision', allow_pointless=False)477 wta.commit('other revision', allow_pointless=False)
480 file('b/file', 'wb').write('this contents contents\n')478 self.build_tree_contents([('b/file', 'this contents contents\n')])
481 wtb = d_b.open_workingtree()479 wtb = d_b.open_workingtree()
482 wtb.commit('this revision', allow_pointless=False)480 wtb.commit('this revision', allow_pointless=False)
483 self.assertEqual(1, wtb.merge_from_branch(wta.branch))481 self.assertEqual(1, wtb.merge_from_branch(wta.branch))
484 self.assert_(os.path.lexists('b/file.THIS'))482 self.failUnlessExists('b/file.THIS')
485 self.assert_(os.path.lexists('b/file.BASE'))483 self.failUnlessExists('b/file.BASE')
486 self.assert_(os.path.lexists('b/file.OTHER'))484 self.failUnlessExists('b/file.OTHER')
487 wtb.revert()485 wtb.revert()
488 self.assertEqual(1, wtb.merge_from_branch(wta.branch,486 self.assertEqual(1, wtb.merge_from_branch(wta.branch,
489 merge_type=WeaveMerger))487 merge_type=WeaveMerger))
490 self.assert_(os.path.lexists('b/file'))488 self.failUnlessExists('b/file')
491 self.assert_(os.path.lexists('b/file.THIS'))489 self.failUnlessExists('b/file.THIS')
492 self.assert_(not os.path.lexists('b/file.BASE'))490 self.failUnlessExists('b/file.BASE')
493 self.assert_(os.path.lexists('b/file.OTHER'))491 self.failUnlessExists('b/file.OTHER')
492
493 def test_weave_conflicts_not_in_base(self):
494 builder = self.make_branch_builder('source')
495 builder.start_series()
496 # See bug #494197
497 # A base revision (before criss-cross)
498 # |\
499 # B C B does nothing, C adds 'foo'
500 # |X|
501 # D E D and E modify foo in incompatible ways
502 #
503 # Merging will conflict, with C as a clean base text. However, the
504 # current code uses A as the global base and 'foo' doesn't exist there.
505 # It isn't trivial to create foo.BASE because it tries to look up
506 # attributes like 'executable' in A.
507 builder.build_snapshot('A-id', None, [
508 ('add', ('', 'TREE_ROOT', 'directory', None))])
509 builder.build_snapshot('B-id', ['A-id'], [])
510 builder.build_snapshot('C-id', ['A-id'], [
511 ('add', ('foo', 'foo-id', 'file', 'orig\ncontents\n'))])
512 builder.build_snapshot('D-id', ['B-id', 'C-id'], [
513 ('add', ('foo', 'foo-id', 'file', 'orig\ncontents\nand D\n'))])
514 builder.build_snapshot('E-id', ['C-id', 'B-id'], [
515 ('modify', ('foo-id', 'orig\ncontents\nand E\n'))])
516 builder.finish_series()
517 tree = builder.get_branch().create_checkout('tree', lightweight=True)
518 self.assertEqual(1, tree.merge_from_branch(tree.branch,
519 to_revision='D-id',
520 merge_type=WeaveMerger))
521 self.failUnlessExists('tree/foo.THIS')
522 self.failUnlessExists('tree/foo.OTHER')
523 self.expectFailure('fail to create .BASE in some criss-cross merges',
524 self.failUnlessExists, 'tree/foo.BASE')
525 self.failUnlessExists('tree/foo.BASE')
494526
495 def test_merge_unrelated(self):527 def test_merge_unrelated(self):
496 """Sucessfully merges unrelated branches with no common names"""528 """Sucessfully merges unrelated branches with no common names"""
497529
=== modified file 'bzrlib/versionedfile.py'
--- bzrlib/versionedfile.py 2009-10-19 15:06:58 +0000
+++ bzrlib/versionedfile.py 2009-12-08 21:10:33 +0000
@@ -1426,7 +1426,7 @@
1426 def __init__(self, plan, a_marker=TextMerge.A_MARKER,1426 def __init__(self, plan, a_marker=TextMerge.A_MARKER,
1427 b_marker=TextMerge.B_MARKER):1427 b_marker=TextMerge.B_MARKER):
1428 TextMerge.__init__(self, a_marker, b_marker)1428 TextMerge.__init__(self, a_marker, b_marker)
1429 self.plan = plan1429 self.plan = list(plan)
14301430
1431 def _merge_struct(self):1431 def _merge_struct(self):
1432 lines_a = []1432 lines_a = []
@@ -1490,6 +1490,60 @@
1490 for struct in outstanding_struct():1490 for struct in outstanding_struct():
1491 yield struct1491 yield struct
14921492
1493 def base_from_plan(self):
1494 """Construct a BASE file from the plan text."""
1495 base_lines = []
1496 for state, line in self.plan:
1497 if state in ('killed-a', 'killed-b', 'killed-both', 'unchanged'):
1498 # If unchanged, then this line is straight from base. If a or b
1499 # or both killed the line, then it *used* to be in base.
1500 base_lines.append(line)
1501 else:
1502 if state not in ('killed-base', 'irrelevant',
1503 'ghost-a', 'ghost-b',
1504 'new-a', 'new-b',
1505 'conflicted-a', 'conflicted-b'):
1506 # killed-base, irrelevant means it doesn't apply
1507 # ghost-a/ghost-b are harder to say for sure, but they
1508 # aren't in the 'inc_c' which means they aren't in the
1509 # shared base of a & b. So we don't include them. And
1510 # obviously if the line is newly inserted, it isn't in base
1511
1512 # If 'conflicted-a' or b, then it is new vs one base, but
1513 # old versus another base. However, if we make it present
1514 # in the base, it will be deleted from the target, and it
1515 # seems better to get a line doubled in the merge result,
1516 # rather than have it deleted entirely.
1517 # Example, each node is the 'text' at that point:
1518 # MN
1519 # / \
1520 # MaN MbN
1521 # | X |
1522 # MabN MbaN
1523 # \ /
1524 # ???
1525 # There was a criss-cross conflict merge. Both sides
1526 # include the other, but put themselves first.
1527 # Weave marks this as a 'clean' merge, picking OTHER over
1528 # THIS. (Though the details depend on order inserted into
1529 # weave, etc.)
1530 # LCA generates a plan:
1531 # [('unchanged', M),
1532 # ('conflicted-b', b),
1533 # ('unchanged', a),
1534 # ('conflicted-a', b),
1535 # ('unchanged', N)]
1536 # If you mark 'conflicted-*' as part of BASE, then a 3-way
1537 # merge tool will cleanly generate "MaN" (as BASE vs THIS
1538 # removes one 'b', and BASE vs OTHER removes the other)
1539 # If you include neither, 3-way creates a clean "MbabN" as
1540 # THIS adds one 'b', and OTHER does too.
1541 # It seems that having the line 2 times is better than
1542 # having it omitted. (Easier to manually delete than notice
1543 # it needs to be added.)
1544 raise AssertionError('Unknown state: %s' % (state,))
1545 return base_lines
1546
14931547
1494class WeaveMerge(PlanWeaveMerge):1548class WeaveMerge(PlanWeaveMerge):
1495 """Weave merge that takes a VersionedFile and two versions as its input."""1549 """Weave merge that takes a VersionedFile and two versions as its input."""
14961550
=== added file 'plan_to_base.py'
--- plan_to_base.py 1970-01-01 00:00:00 +0000
+++ plan_to_base.py 2009-12-08 21:10:33 +0000
@@ -0,0 +1,76 @@
1#!/usr/bin/env python
2# Copyright (C) 2009 Canonical Ltd
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18"""A script that converts a bzr merge .plan file into a .BASE file."""
19
20import sys
21
22
23def plan_lines_to_base_lines(plan_lines):
24 _ghost_warning = False
25 base_lines = []
26 for line in plan_lines:
27 action, content = line.split('|', 1)
28 action = action.strip()
29 if action in ('killed-a', 'killed-b', 'killed-both', 'unchanged'):
30 # If lines were removed by a or b or both, then they must have been
31 # in the base. if unchanged, then they are copied from the base
32 base_lines.append(content)
33 elif action in ('killed-base', 'irrelevant', 'ghost-a', 'ghost-b',
34 'new-a', 'new-b'):
35 # The first 4 are ones that are always suppressed
36 # the last 2 are lines that are in A or B, but *not* in BASE, so we
37 # ignore them
38 continue
39 else:
40 sys.stderr.write('Unknown action: %s\n' % (action,))
41 return base_lines
42
43
44def plan_file_to_base_file(plan_filename):
45 if not plan_filename.endswith('.plan'):
46 sys.stderr.write('"%s" does not look like a .plan file\n'
47 % (plan_filename,))
48 return
49 plan_file = open(plan_filename, 'rb')
50 try:
51 plan_lines = plan_file.readlines()
52 finally:
53 plan_file.close()
54 base_filename = plan_filename[:-4] + 'BASE'
55 base_lines = plan_lines_to_base_lines(plan_lines)
56 f = open(base_filename, 'wb')
57 try:
58 f.writelines(base_lines)
59 finally:
60 f.close()
61
62
63def main(args):
64 import optparse
65 p = optparse.OptionParser('%prog foo.plan*')
66
67 opts, args = p.parse_args(args)
68 if len(args) < 1:
69 sys.stderr.write('You must supply exactly a .plan file.\n')
70 return 1
71 for arg in args:
72 plan_file_to_base_file(arg)
73
74
75if __name__ == '__main__':
76 sys.exit(main(sys.argv[1:]))