Merge lp:~jameinel/bzr/2.0-40412-show-base-weave into lp:bzr
- 2.0-40412-show-base-weave
- Merge into bzr.dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincent Ladeuil | Needs Fixing | ||
Review via email: mp+15835@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote : | # |
Revision history for this message
Vincent Ladeuil (vila) wrote : | # |
Mentioning https:/
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
1 | === modified file 'bzrlib/merge.py' |
2 | --- bzrlib/merge.py 2009-12-03 02:24:54 +0000 |
3 | +++ bzrlib/merge.py 2009-12-08 21:10:33 +0000 |
4 | @@ -1407,60 +1407,69 @@ |
5 | supports_reverse_cherrypick = False |
6 | history_based = True |
7 | |
8 | - def _merged_lines(self, file_id): |
9 | - """Generate the merged lines. |
10 | - There is no distinction between lines that are meant to contain <<<<<<< |
11 | - and conflicts. |
12 | - """ |
13 | - if self.cherrypick: |
14 | - base = self.base_tree |
15 | - else: |
16 | - base = None |
17 | - plan = self.this_tree.plan_file_merge(file_id, self.other_tree, |
18 | + def _generate_merge_plan(self, file_id, base): |
19 | + return self.this_tree.plan_file_merge(file_id, self.other_tree, |
20 | base=base) |
21 | + |
22 | + def _merged_lines(self, file_id): |
23 | + """Generate the merged lines. |
24 | + There is no distinction between lines that are meant to contain <<<<<<< |
25 | + and conflicts. |
26 | + """ |
27 | + if self.cherrypick: |
28 | + base = self.base_tree |
29 | + else: |
30 | + base = None |
31 | + plan = self._generate_merge_plan(file_id, base) |
32 | if 'merge' in debug.debug_flags: |
33 | plan = list(plan) |
34 | trans_id = self.tt.trans_id_file_id(file_id) |
35 | name = self.tt.final_name(trans_id) + '.plan' |
36 | - contents = ('%10s|%s' % l for l in plan) |
37 | + contents = ('%11s|%s' % l for l in plan) |
38 | self.tt.new_file(name, self.tt.final_parent(trans_id), contents) |
39 | +<<<<<<< TREE |
40 | textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n', |
41 | '>>>>>>> MERGE-SOURCE\n') |
42 | return textmerge.merge_lines(self.reprocess) |
43 | +======= |
44 | + textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n', |
45 | + '>>>>>>> MERGE-SOURCE\n') |
46 | + lines, conflicts = textmerge.merge_lines(self.reprocess) |
47 | + if conflicts: |
48 | + base_lines = textmerge.base_from_plan() |
49 | + else: |
50 | + base_lines = None |
51 | + return lines, base_lines |
52 | +>>>>>>> MERGE-SOURCE |
53 | |
54 | def text_merge(self, file_id, trans_id): |
55 | """Perform a (weave) text merge for a given file and file-id. |
56 | If conflicts are encountered, .THIS and .OTHER files will be emitted, |
57 | and a conflict will be noted. |
58 | """ |
59 | - lines, conflicts = self._merged_lines(file_id) |
60 | + lines, base_lines = self._merged_lines(file_id) |
61 | lines = list(lines) |
62 | # Note we're checking whether the OUTPUT is binary in this case, |
63 | # because we don't want to get into weave merge guts. |
64 | textfile.check_text_lines(lines) |
65 | self.tt.create_file(lines, trans_id) |
66 | - if conflicts: |
67 | + if base_lines is not None: |
68 | + # Conflict |
69 | self._raw_conflicts.append(('text conflict', trans_id)) |
70 | name = self.tt.final_name(trans_id) |
71 | parent_id = self.tt.final_parent(trans_id) |
72 | file_group = self._dump_conflicts(name, parent_id, file_id, |
73 | - no_base=True) |
74 | + no_base=False, |
75 | + base_lines=base_lines) |
76 | file_group.append(trans_id) |
77 | |
78 | |
79 | class LCAMerger(WeaveMerger): |
80 | |
81 | - def _merged_lines(self, file_id): |
82 | - """Generate the merged lines. |
83 | - There is no distinction between lines that are meant to contain <<<<<<< |
84 | - and conflicts. |
85 | - """ |
86 | - if self.cherrypick: |
87 | - base = self.base_tree |
88 | - else: |
89 | - base = None |
90 | - plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree, |
91 | + def _generate_merge_plan(self, file_id, base): |
92 | + return self.this_tree.plan_file_lca_merge(file_id, self.other_tree, |
93 | base=base) |
94 | +<<<<<<< TREE |
95 | if 'merge' in debug.debug_flags: |
96 | plan = list(plan) |
97 | trans_id = self.tt.trans_id_file_id(file_id) |
98 | @@ -1471,6 +1480,9 @@ |
99 | '>>>>>>> MERGE-SOURCE\n') |
100 | return textmerge.merge_lines(self.reprocess) |
101 | |
102 | +======= |
103 | + |
104 | +>>>>>>> MERGE-SOURCE |
105 | |
106 | class Diff3Merger(Merge3Merger): |
107 | """Three-way merger using external diff3 for text merging""" |
108 | |
109 | === modified file 'bzrlib/tests/blackbox/test_merge.py' |
110 | --- bzrlib/tests/blackbox/test_merge.py 2009-12-07 21:46:28 +0000 |
111 | +++ bzrlib/tests/blackbox/test_merge.py 2009-12-08 21:10:33 +0000 |
112 | @@ -211,6 +211,22 @@ |
113 | self.failUnlessExists('sub/a.txt.OTHER') |
114 | self.failUnlessExists('sub/a.txt.BASE') |
115 | |
116 | + def test_conflict_leaves_base_this_other_files(self): |
117 | + tree, other = self.create_conflicting_branches() |
118 | + self.run_bzr('merge ../other', working_dir='tree', |
119 | + retcode=1) |
120 | + self.assertFileEqual('a\nb\nc\n', 'tree/fname.BASE') |
121 | + self.assertFileEqual('a\nB\nD\n', 'tree/fname.OTHER') |
122 | + self.assertFileEqual('a\nB\nC\n', 'tree/fname.THIS') |
123 | + |
124 | + def test_weave_conflict_leaves_base_this_other_files(self): |
125 | + tree, other = self.create_conflicting_branches() |
126 | + self.run_bzr('merge ../other --weave', working_dir='tree', |
127 | + retcode=1) |
128 | + self.assertFileEqual('a\nb\nc\n', 'tree/fname.BASE') |
129 | + self.assertFileEqual('a\nB\nD\n', 'tree/fname.OTHER') |
130 | + self.assertFileEqual('a\nB\nC\n', 'tree/fname.THIS') |
131 | + |
132 | def test_merge_remember(self): |
133 | """Merge changes from one branch to another, test submit location.""" |
134 | tree_a = self.make_branch_and_tree('branch_a') |
135 | |
136 | === modified file 'bzrlib/tests/test_merge.py' |
137 | --- bzrlib/tests/test_merge.py 2009-09-19 00:32:14 +0000 |
138 | +++ bzrlib/tests/test_merge.py 2009-12-08 21:10:33 +0000 |
139 | @@ -38,6 +38,7 @@ |
140 | from bzrlib.workingtree import WorkingTree |
141 | from bzrlib.transform import TreeTransform |
142 | |
143 | + |
144 | class TestMerge(TestCaseWithTransport): |
145 | """Test appending more than one revision""" |
146 | |
147 | @@ -524,6 +525,12 @@ |
148 | self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg') |
149 | return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',)) |
150 | |
151 | + def test_base_from_plan(self): |
152 | + self.setup_plan_merge() |
153 | + plan = self.plan_merge_vf.plan_merge('B', 'C') |
154 | + pwm = versionedfile.PlanWeaveMerge(plan) |
155 | + self.assertEqual(['a\n', 'b\n', 'c\n'], pwm.base_from_plan()) |
156 | + |
157 | def test_unique_lines(self): |
158 | plan = self.setup_plan_merge() |
159 | self.assertEqual(plan._unique_lines( |
160 | @@ -827,6 +834,111 @@ |
161 | ('unchanged', 'f\n'), |
162 | ('unchanged', 'g\n')], |
163 | list(plan)) |
164 | + plan = self.plan_merge_vf.plan_lca_merge('F', 'G') |
165 | + # This is one of the main differences between plan_merge and |
166 | + # plan_lca_merge. plan_lca_merge generates a conflict for 'x => z', |
167 | + # because 'x' was not present in one of the bases. However, in this |
168 | + # case it is spurious because 'x' does not exist in the global base A. |
169 | + self.assertEqual([ |
170 | + ('unchanged', 'h\n'), |
171 | + ('unchanged', 'a\n'), |
172 | + ('conflicted-a', 'x\n'), |
173 | + ('new-b', 'z\n'), |
174 | + ('unchanged', 'c\n'), |
175 | + ('unchanged', 'd\n'), |
176 | + ('unchanged', 'y\n'), |
177 | + ('unchanged', 'f\n'), |
178 | + ('unchanged', 'g\n')], |
179 | + list(plan)) |
180 | + |
181 | + def test_criss_cross_flip_flop(self): |
182 | + # This is specificly trying to trigger problems when using limited |
183 | + # ancestry and weaves. The ancestry graph looks like: |
184 | + # XX unused ancestor, should not show up in the weave |
185 | + # | |
186 | + # A Unique LCA |
187 | + # / \ |
188 | + # B C B & C both introduce a new line |
189 | + # |\ /| |
190 | + # | X | |
191 | + # |/ \| |
192 | + # D E B & C are both merged, so both are common ancestors |
193 | + # In the process of merging, both sides order the new |
194 | + # lines differently |
195 | + # |
196 | + self.add_rev('root', 'XX', [], 'qrs') |
197 | + self.add_rev('root', 'A', ['XX'], 'abcdef') |
198 | + self.add_rev('root', 'B', ['A'], 'abcdgef') |
199 | + self.add_rev('root', 'C', ['A'], 'abcdhef') |
200 | + self.add_rev('root', 'D', ['B', 'C'], 'abcdghef') |
201 | + self.add_rev('root', 'E', ['C', 'B'], 'abcdhgef') |
202 | + plan = list(self.plan_merge_vf.plan_merge('D', 'E')) |
203 | + self.assertEqual([ |
204 | + ('unchanged', 'a\n'), |
205 | + ('unchanged', 'b\n'), |
206 | + ('unchanged', 'c\n'), |
207 | + ('unchanged', 'd\n'), |
208 | + ('new-b', 'h\n'), |
209 | + ('unchanged', 'g\n'), |
210 | + ('killed-b', 'h\n'), |
211 | + ('unchanged', 'e\n'), |
212 | + ('unchanged', 'f\n'), |
213 | + ], plan) |
214 | + pwm = versionedfile.PlanWeaveMerge(plan) |
215 | + self.assertEqualDiff('\n'.join('abcdghef') + '\n', |
216 | + ''.join(pwm.base_from_plan())) |
217 | + # Reversing the order reverses the merge plan, and final order of 'hg' |
218 | + # => 'gh' |
219 | + plan = list(self.plan_merge_vf.plan_merge('E', 'D')) |
220 | + self.assertEqual([ |
221 | + ('unchanged', 'a\n'), |
222 | + ('unchanged', 'b\n'), |
223 | + ('unchanged', 'c\n'), |
224 | + ('unchanged', 'd\n'), |
225 | + ('new-b', 'g\n'), |
226 | + ('unchanged', 'h\n'), |
227 | + ('killed-b', 'g\n'), |
228 | + ('unchanged', 'e\n'), |
229 | + ('unchanged', 'f\n'), |
230 | + ], plan) |
231 | + pwm = versionedfile.PlanWeaveMerge(plan) |
232 | + self.assertEqualDiff('\n'.join('abcdhgef') + '\n', |
233 | + ''.join(pwm.base_from_plan())) |
234 | + # This is where lca differs, in that it (fairly correctly) determines |
235 | + # that there is a conflict because both sides resolved the merge |
236 | + # differently |
237 | + plan = list(self.plan_merge_vf.plan_lca_merge('D', 'E')) |
238 | + self.assertEqual([ |
239 | + ('unchanged', 'a\n'), |
240 | + ('unchanged', 'b\n'), |
241 | + ('unchanged', 'c\n'), |
242 | + ('unchanged', 'd\n'), |
243 | + ('conflicted-b', 'h\n'), |
244 | + ('unchanged', 'g\n'), |
245 | + ('conflicted-a', 'h\n'), |
246 | + ('unchanged', 'e\n'), |
247 | + ('unchanged', 'f\n'), |
248 | + ], plan) |
249 | + pwm = versionedfile.PlanWeaveMerge(plan) |
250 | + self.assertEqualDiff('\n'.join('abcdgef') + '\n', |
251 | + ''.join(pwm.base_from_plan())) |
252 | + # Reversing it changes what line is doubled, but still gives a |
253 | + # double-conflict |
254 | + plan = list(self.plan_merge_vf.plan_lca_merge('E', 'D')) |
255 | + self.assertEqual([ |
256 | + ('unchanged', 'a\n'), |
257 | + ('unchanged', 'b\n'), |
258 | + ('unchanged', 'c\n'), |
259 | + ('unchanged', 'd\n'), |
260 | + ('conflicted-b', 'g\n'), |
261 | + ('unchanged', 'h\n'), |
262 | + ('conflicted-a', 'g\n'), |
263 | + ('unchanged', 'e\n'), |
264 | + ('unchanged', 'f\n'), |
265 | + ], plan) |
266 | + pwm = versionedfile.PlanWeaveMerge(plan) |
267 | + self.assertEqualDiff('\n'.join('abcdhef') + '\n', |
268 | + ''.join(pwm.base_from_plan())) |
269 | |
270 | def assertRemoveExternalReferences(self, filtered_parent_map, |
271 | child_map, tails, parent_map): |
272 | |
273 | === modified file 'bzrlib/tests/test_merge_core.py' |
274 | --- bzrlib/tests/test_merge_core.py 2009-08-20 04:09:58 +0000 |
275 | +++ bzrlib/tests/test_merge_core.py 2009-12-08 21:10:33 +0000 |
276 | @@ -434,6 +434,7 @@ |
277 | self.assertEqual('text2', builder.this.get_file('1').read()) |
278 | builder.cleanup() |
279 | |
280 | + |
281 | class FunctionalMergeTest(TestCaseWithTransport): |
282 | |
283 | def test_trivial_star_merge(self): |
284 | @@ -467,30 +468,61 @@ |
285 | self.assertEqual("Mary\n", open("original/file2", "rt").read()) |
286 | |
287 | def test_conflicts(self): |
288 | - os.mkdir('a') |
289 | wta = self.make_branch_and_tree('a') |
290 | - a = wta.branch |
291 | - file('a/file', 'wb').write('contents\n') |
292 | + self.build_tree_contents([('a/file', 'contents\n')]) |
293 | wta.add('file') |
294 | wta.commit('base revision', allow_pointless=False) |
295 | - d_b = a.bzrdir.clone('b') |
296 | - b = d_b.open_branch() |
297 | - file('a/file', 'wb').write('other contents\n') |
298 | + d_b = wta.branch.bzrdir.clone('b') |
299 | + self.build_tree_contents([('a/file', 'other contents\n')]) |
300 | wta.commit('other revision', allow_pointless=False) |
301 | - file('b/file', 'wb').write('this contents contents\n') |
302 | + self.build_tree_contents([('b/file', 'this contents contents\n')]) |
303 | wtb = d_b.open_workingtree() |
304 | wtb.commit('this revision', allow_pointless=False) |
305 | self.assertEqual(1, wtb.merge_from_branch(wta.branch)) |
306 | - self.assert_(os.path.lexists('b/file.THIS')) |
307 | - self.assert_(os.path.lexists('b/file.BASE')) |
308 | - self.assert_(os.path.lexists('b/file.OTHER')) |
309 | + self.failUnlessExists('b/file.THIS') |
310 | + self.failUnlessExists('b/file.BASE') |
311 | + self.failUnlessExists('b/file.OTHER') |
312 | wtb.revert() |
313 | self.assertEqual(1, wtb.merge_from_branch(wta.branch, |
314 | merge_type=WeaveMerger)) |
315 | - self.assert_(os.path.lexists('b/file')) |
316 | - self.assert_(os.path.lexists('b/file.THIS')) |
317 | - self.assert_(not os.path.lexists('b/file.BASE')) |
318 | - self.assert_(os.path.lexists('b/file.OTHER')) |
319 | + self.failUnlessExists('b/file') |
320 | + self.failUnlessExists('b/file.THIS') |
321 | + self.failUnlessExists('b/file.BASE') |
322 | + self.failUnlessExists('b/file.OTHER') |
323 | + |
324 | + def test_weave_conflicts_not_in_base(self): |
325 | + builder = self.make_branch_builder('source') |
326 | + builder.start_series() |
327 | + # See bug #494197 |
328 | + # A base revision (before criss-cross) |
329 | + # |\ |
330 | + # B C B does nothing, C adds 'foo' |
331 | + # |X| |
332 | + # D E D and E modify foo in incompatible ways |
333 | + # |
334 | + # Merging will conflict, with C as a clean base text. However, the |
335 | + # current code uses A as the global base and 'foo' doesn't exist there. |
336 | + # It isn't trivial to create foo.BASE because it tries to look up |
337 | + # attributes like 'executable' in A. |
338 | + builder.build_snapshot('A-id', None, [ |
339 | + ('add', ('', 'TREE_ROOT', 'directory', None))]) |
340 | + builder.build_snapshot('B-id', ['A-id'], []) |
341 | + builder.build_snapshot('C-id', ['A-id'], [ |
342 | + ('add', ('foo', 'foo-id', 'file', 'orig\ncontents\n'))]) |
343 | + builder.build_snapshot('D-id', ['B-id', 'C-id'], [ |
344 | + ('add', ('foo', 'foo-id', 'file', 'orig\ncontents\nand D\n'))]) |
345 | + builder.build_snapshot('E-id', ['C-id', 'B-id'], [ |
346 | + ('modify', ('foo-id', 'orig\ncontents\nand E\n'))]) |
347 | + builder.finish_series() |
348 | + tree = builder.get_branch().create_checkout('tree', lightweight=True) |
349 | + self.assertEqual(1, tree.merge_from_branch(tree.branch, |
350 | + to_revision='D-id', |
351 | + merge_type=WeaveMerger)) |
352 | + self.failUnlessExists('tree/foo.THIS') |
353 | + self.failUnlessExists('tree/foo.OTHER') |
354 | + self.expectFailure('fail to create .BASE in some criss-cross merges', |
355 | + self.failUnlessExists, 'tree/foo.BASE') |
356 | + self.failUnlessExists('tree/foo.BASE') |
357 | |
358 | def test_merge_unrelated(self): |
359 | """Sucessfully merges unrelated branches with no common names""" |
360 | |
361 | === modified file 'bzrlib/versionedfile.py' |
362 | --- bzrlib/versionedfile.py 2009-10-19 15:06:58 +0000 |
363 | +++ bzrlib/versionedfile.py 2009-12-08 21:10:33 +0000 |
364 | @@ -1426,7 +1426,7 @@ |
365 | def __init__(self, plan, a_marker=TextMerge.A_MARKER, |
366 | b_marker=TextMerge.B_MARKER): |
367 | TextMerge.__init__(self, a_marker, b_marker) |
368 | - self.plan = plan |
369 | + self.plan = list(plan) |
370 | |
371 | def _merge_struct(self): |
372 | lines_a = [] |
373 | @@ -1490,6 +1490,60 @@ |
374 | for struct in outstanding_struct(): |
375 | yield struct |
376 | |
377 | + def base_from_plan(self): |
378 | + """Construct a BASE file from the plan text.""" |
379 | + base_lines = [] |
380 | + for state, line in self.plan: |
381 | + if state in ('killed-a', 'killed-b', 'killed-both', 'unchanged'): |
382 | + # If unchanged, then this line is straight from base. If a or b |
383 | + # or both killed the line, then it *used* to be in base. |
384 | + base_lines.append(line) |
385 | + else: |
386 | + if state not in ('killed-base', 'irrelevant', |
387 | + 'ghost-a', 'ghost-b', |
388 | + 'new-a', 'new-b', |
389 | + 'conflicted-a', 'conflicted-b'): |
390 | + # killed-base, irrelevant means it doesn't apply |
391 | + # ghost-a/ghost-b are harder to say for sure, but they |
392 | + # aren't in the 'inc_c' which means they aren't in the |
393 | + # shared base of a & b. So we don't include them. And |
394 | + # obviously if the line is newly inserted, it isn't in base |
395 | + |
396 | + # If 'conflicted-a' or b, then it is new vs one base, but |
397 | + # old versus another base. However, if we make it present |
398 | + # in the base, it will be deleted from the target, and it |
399 | + # seems better to get a line doubled in the merge result, |
400 | + # rather than have it deleted entirely. |
401 | + # Example, each node is the 'text' at that point: |
402 | + # MN |
403 | + # / \ |
404 | + # MaN MbN |
405 | + # | X | |
406 | + # MabN MbaN |
407 | + # \ / |
408 | + # ??? |
409 | + # There was a criss-cross conflict merge. Both sides |
410 | + # include the other, but put themselves first. |
411 | + # Weave marks this as a 'clean' merge, picking OTHER over |
412 | + # THIS. (Though the details depend on order inserted into |
413 | + # weave, etc.) |
414 | + # LCA generates a plan: |
415 | + # [('unchanged', M), |
416 | + # ('conflicted-b', b), |
417 | + # ('unchanged', a), |
418 | + # ('conflicted-a', b), |
419 | + # ('unchanged', N)] |
420 | + # If you mark 'conflicted-*' as part of BASE, then a 3-way |
421 | + # merge tool will cleanly generate "MaN" (as BASE vs THIS |
422 | + # removes one 'b', and BASE vs OTHER removes the other) |
423 | + # If you include neither, 3-way creates a clean "MbabN" as |
424 | + # THIS adds one 'b', and OTHER does too. |
425 | + # It seems that having the line 2 times is better than |
426 | + # having it omitted. (Easier to manually delete than notice |
427 | + # it needs to be added.) |
428 | + raise AssertionError('Unknown state: %s' % (state,)) |
429 | + return base_lines |
430 | + |
431 | |
432 | class WeaveMerge(PlanWeaveMerge): |
433 | """Weave merge that takes a VersionedFile and two versions as its input.""" |
434 | |
435 | === added file 'plan_to_base.py' |
436 | --- plan_to_base.py 1970-01-01 00:00:00 +0000 |
437 | +++ plan_to_base.py 2009-12-08 21:10:33 +0000 |
438 | @@ -0,0 +1,76 @@ |
439 | +#!/usr/bin/env python |
440 | +# Copyright (C) 2009 Canonical Ltd |
441 | +# |
442 | +# This program is free software; you can redistribute it and/or modify |
443 | +# it under the terms of the GNU General Public License as published by |
444 | +# the Free Software Foundation; either version 2 of the License, or |
445 | +# (at your option) any later version. |
446 | +# |
447 | +# This program is distributed in the hope that it will be useful, |
448 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
449 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
450 | +# GNU General Public License for more details. |
451 | +# |
452 | +# You should have received a copy of the GNU General Public License |
453 | +# along with this program; if not, write to the Free Software |
454 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
455 | + |
456 | +"""A script that converts a bzr merge .plan file into a .BASE file.""" |
457 | + |
458 | +import sys |
459 | + |
460 | + |
461 | +def plan_lines_to_base_lines(plan_lines): |
462 | + _ghost_warning = False |
463 | + base_lines = [] |
464 | + for line in plan_lines: |
465 | + action, content = line.split('|', 1) |
466 | + action = action.strip() |
467 | + if action in ('killed-a', 'killed-b', 'killed-both', 'unchanged'): |
468 | + # If lines were removed by a or b or both, then they must have been |
469 | + # in the base. if unchanged, then they are copied from the base |
470 | + base_lines.append(content) |
471 | + elif action in ('killed-base', 'irrelevant', 'ghost-a', 'ghost-b', |
472 | + 'new-a', 'new-b'): |
473 | + # The first 4 are ones that are always suppressed |
474 | + # the last 2 are lines that are in A or B, but *not* in BASE, so we |
475 | + # ignore them |
476 | + continue |
477 | + else: |
478 | + sys.stderr.write('Unknown action: %s\n' % (action,)) |
479 | + return base_lines |
480 | + |
481 | + |
482 | +def plan_file_to_base_file(plan_filename): |
483 | + if not plan_filename.endswith('.plan'): |
484 | + sys.stderr.write('"%s" does not look like a .plan file\n' |
485 | + % (plan_filename,)) |
486 | + return |
487 | + plan_file = open(plan_filename, 'rb') |
488 | + try: |
489 | + plan_lines = plan_file.readlines() |
490 | + finally: |
491 | + plan_file.close() |
492 | + base_filename = plan_filename[:-4] + 'BASE' |
493 | + base_lines = plan_lines_to_base_lines(plan_lines) |
494 | + f = open(base_filename, 'wb') |
495 | + try: |
496 | + f.writelines(base_lines) |
497 | + finally: |
498 | + f.close() |
499 | + |
500 | + |
501 | +def main(args): |
502 | + import optparse |
503 | + p = optparse.OptionParser('%prog foo.plan*') |
504 | + |
505 | + opts, args = p.parse_args(args) |
506 | + if len(args) < 1: |
507 | + sys.stderr.write('You must supply exactly a .plan file.\n') |
508 | + return 1 |
509 | + for arg in args: |
510 | + plan_file_to_base_file(arg) |
511 | + |
512 | + |
513 | +if __name__ == '__main__': |
514 | + sys.exit(main(sys.argv[1:])) |
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: /bugs.edge. launchpad. net/bzr/ +bug/494197
https:/
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. :)