178
108
transform_tree(tree, tree.branch.basis_tree())
180
110
def test_ignore_zero_merge_inner(self):
181
# Test that merge_inner's ignore zero parameter is effective
182
tree_a = self.make_branch_and_tree('a')
111
# Test that merge_inner's ignore zero paramter is effective
112
tree_a =self.make_branch_and_tree('a')
183
113
tree_a.commit(message="hello")
184
dir_b = tree_a.controldir.sprout('b')
114
dir_b = tree_a.bzrdir.sprout('b')
185
115
tree_b = dir_b.open_workingtree()
187
self.addCleanup(tree_b.unlock)
188
116
tree_a.commit(message="hello again")
189
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
118
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
190
119
this_tree=tree_b, ignore_zero=True)
191
self.assertTrue('All changes applied successfully.\n' not in
194
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
120
log = self._get_log()
121
self.failUnless('All changes applied successfully.\n' not in log)
123
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
195
124
this_tree=tree_b, ignore_zero=False)
197
'All changes applied successfully.\n' in self.get_log())
199
def test_merge_inner_conflicts(self):
200
tree_a = self.make_branch_and_tree('a')
201
tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
202
merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
203
self.assertEqual(1, len(tree_a.conflicts()))
205
def test_rmdir_conflict(self):
206
tree_a = self.make_branch_and_tree('a')
207
self.build_tree(['a/b/'])
208
tree_a.add('b', b'b-id')
209
tree_a.commit('added b')
210
# basis_tree() is only guaranteed to be valid as long as it is actually
211
# the basis tree. This mutates the tree after grabbing basis, so go to
213
base_tree = tree_a.branch.repository.revision_tree(
214
tree_a.last_revision())
215
tree_z = tree_a.controldir.sprout('z').open_workingtree()
216
self.build_tree(['a/b/c'])
218
tree_a.commit('added c')
220
tree_z.commit('removed b')
221
merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
223
conflicts.MissingParent('Created directory', 'b', b'b-id'),
224
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
226
merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
229
conflicts.DeletingParent('Not deleting', 'b', b'b-id'),
230
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
233
def test_nested_merge(self):
235
'iter_changes doesn\'t work with changes in nested trees')
236
tree = self.make_branch_and_tree('tree',
237
format='development-subtree')
238
sub_tree = self.make_branch_and_tree('tree/sub-tree',
239
format='development-subtree')
240
sub_tree.set_root_id(b'sub-tree-root')
241
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
243
sub_tree.commit('foo')
244
tree.add_reference(sub_tree)
245
tree.commit('set text to 1')
246
tree2 = tree.controldir.sprout('tree2').open_workingtree()
247
# modify the file in the subtree
248
self.build_tree_contents([('tree2/sub-tree/file', b'text2')])
249
# and merge the changes from the diverged subtree into the containing
251
tree2.commit('changed file text')
252
tree.merge_from_branch(tree2.branch)
253
self.assertFileEqual(b'text2', 'tree/sub-tree/file')
255
def test_merge_with_missing(self):
256
tree_a = self.make_branch_and_tree('tree_a')
257
self.build_tree_contents([('tree_a/file', b'content_1')])
259
tree_a.commit('commit base')
260
# basis_tree() is only guaranteed to be valid as long as it is actually
261
# the basis tree. This test commits to the tree after grabbing basis,
262
# so we go to the repository.
263
base_tree = tree_a.branch.repository.revision_tree(
264
tree_a.last_revision())
265
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
266
self.build_tree_contents([('tree_a/file', b'content_2')])
267
tree_a.commit('commit other')
268
other_tree = tree_a.basis_tree()
269
# 'file' is now missing but isn't altered in any commit in b so no
270
# change should be applied.
271
os.unlink('tree_b/file')
272
merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
274
def test_merge_kind_change(self):
275
tree_a = self.make_branch_and_tree('tree_a')
276
self.build_tree_contents([('tree_a/file', b'content_1')])
277
tree_a.add('file', b'file-id')
278
tree_a.commit('added file')
279
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
280
os.unlink('tree_a/file')
281
self.build_tree(['tree_a/file/'])
282
tree_a.commit('changed file to directory')
283
tree_b.merge_from_branch(tree_a.branch)
284
self.assertEqual('directory', file_kind('tree_b/file'))
286
self.assertEqual('file', file_kind('tree_b/file'))
287
self.build_tree_contents([('tree_b/file', b'content_2')])
288
tree_b.commit('content change')
289
tree_b.merge_from_branch(tree_a.branch)
290
self.assertEqual(tree_b.conflicts(),
291
[conflicts.ContentsConflict('file',
292
file_id=b'file-id')])
294
def test_merge_type_registry(self):
295
merge_type_option = option.Option.OPTIONS['merge-type']
296
self.assertFalse('merge4' in [x[0] for x in
297
merge_type_option.iter_switches()])
298
registry = _mod_merge.get_merge_type_registry()
299
registry.register_lazy('merge4', 'breezy.merge', 'Merge4Merger',
300
'time-travelling merge')
301
self.assertTrue('merge4' in [x[0] for x in
302
merge_type_option.iter_switches()])
303
registry.remove('merge4')
304
self.assertFalse('merge4' in [x[0] for x in
305
merge_type_option.iter_switches()])
307
def test_merge_other_moves_we_deleted(self):
308
tree_a = self.make_branch_and_tree('A')
310
self.addCleanup(tree_a.unlock)
311
self.build_tree(['A/a'])
313
tree_a.commit('1', rev_id=b'rev-1')
315
tree_a.rename_one('a', 'b')
317
bzrdir_b = tree_a.controldir.sprout('B', revision_id=b'rev-1')
318
tree_b = bzrdir_b.open_workingtree()
320
self.addCleanup(tree_b.unlock)
324
tree_b.merge_from_branch(tree_a.branch)
325
except AttributeError:
326
self.fail('tried to join a path when name was None')
328
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
329
tree_a = self.make_branch_and_tree('a')
330
self.build_tree(['a/file_1', 'a/file_2'])
331
tree_a.add(['file_1'])
332
tree_a.commit('commit 1')
333
tree_a.add(['file_2'])
334
tree_a.commit('commit 2')
335
tree_b = tree_a.controldir.sprout('b').open_workingtree()
336
tree_b.rename_one('file_1', 'renamed')
337
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
338
merger.merge_type = _mod_merge.Merge3Merger
340
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
342
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
343
tree_a = self.make_branch_and_tree('a')
344
self.build_tree(['a/file_1', 'a/file_2'])
345
tree_a.add(['file_1'])
346
tree_a.commit('commit 1')
347
tree_a.add(['file_2'])
348
tree_a.commit('commit 2')
349
tree_b = tree_a.controldir.sprout('b').open_workingtree()
350
tree_b.rename_one('file_1', 'renamed')
351
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
352
merger.merge_type = _mod_merge.WeaveMerger
354
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
356
def prepare_cherrypick(self):
357
"""Prepare a pair of trees for cherrypicking tests.
359
Both trees have a file, 'file'.
360
rev1 sets content to 'a'.
363
A full merge of rev2b and rev3b into this_tree would add both 'b' and
364
'c'. A successful cherrypick of rev2b-rev3b into this_tree will add
367
this_tree = self.make_branch_and_tree('this')
368
self.build_tree_contents([('this/file', b"a\n")])
369
this_tree.add('file')
370
this_tree.commit('rev1')
371
other_tree = this_tree.controldir.sprout('other').open_workingtree()
372
self.build_tree_contents([('other/file', b"a\nb\n")])
373
other_tree.commit('rev2b', rev_id=b'rev2b')
374
self.build_tree_contents([('other/file', b"c\na\nb\n")])
375
other_tree.commit('rev3b', rev_id=b'rev3b')
376
this_tree.lock_write()
377
self.addCleanup(this_tree.unlock)
378
return this_tree, other_tree
380
def test_weave_cherrypick(self):
381
this_tree, other_tree = self.prepare_cherrypick()
382
merger = _mod_merge.Merger.from_revision_ids(
383
this_tree, b'rev3b', b'rev2b', other_tree.branch)
384
merger.merge_type = _mod_merge.WeaveMerger
386
self.assertFileEqual(b'c\na\n', 'this/file')
388
def test_weave_cannot_reverse_cherrypick(self):
389
this_tree, other_tree = self.prepare_cherrypick()
390
merger = _mod_merge.Merger.from_revision_ids(
391
this_tree, b'rev2b', b'rev3b', other_tree.branch)
392
merger.merge_type = _mod_merge.WeaveMerger
393
self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
395
def test_merge3_can_reverse_cherrypick(self):
396
this_tree, other_tree = self.prepare_cherrypick()
397
merger = _mod_merge.Merger.from_revision_ids(
398
this_tree, b'rev2b', b'rev3b', other_tree.branch)
399
merger.merge_type = _mod_merge.Merge3Merger
402
def test_merge3_will_detect_cherrypick(self):
403
this_tree = self.make_branch_and_tree('this')
404
self.build_tree_contents([('this/file', b"a\n")])
405
this_tree.add('file')
406
this_tree.commit('rev1')
407
other_tree = this_tree.controldir.sprout('other').open_workingtree()
408
self.build_tree_contents([('other/file', b"a\nb\n")])
409
other_tree.commit('rev2b', rev_id=b'rev2b')
410
self.build_tree_contents([('other/file', b"a\nb\nc\n")])
411
other_tree.commit('rev3b', rev_id=b'rev3b')
412
this_tree.lock_write()
413
self.addCleanup(this_tree.unlock)
415
merger = _mod_merge.Merger.from_revision_ids(
416
this_tree, b'rev3b', b'rev2b', other_tree.branch)
417
merger.merge_type = _mod_merge.Merge3Merger
419
self.assertFileEqual(b'a\n'
423
b'>>>>>>> MERGE-SOURCE\n',
426
def test_merge_reverse_revision_range(self):
427
tree = self.make_branch_and_tree(".")
429
self.addCleanup(tree.unlock)
430
self.build_tree(['a'])
432
first_rev = tree.commit("added a")
433
merger = _mod_merge.Merger.from_revision_ids(tree,
434
_mod_revision.NULL_REVISION,
436
merger.merge_type = _mod_merge.Merge3Merger
437
merger.interesting_files = 'a'
438
conflict_count = merger.do_merge()
439
self.assertEqual(0, conflict_count)
441
self.assertPathDoesNotExist("a")
443
self.assertPathExists("a")
445
def test_make_merger(self):
446
this_tree = self.make_branch_and_tree('this')
447
this_tree.commit('rev1', rev_id=b'rev1')
448
other_tree = this_tree.controldir.sprout('other').open_workingtree()
449
this_tree.commit('rev2', rev_id=b'rev2a')
450
other_tree.commit('rev2', rev_id=b'rev2b')
451
this_tree.lock_write()
452
self.addCleanup(this_tree.unlock)
453
merger = _mod_merge.Merger.from_revision_ids(
454
this_tree, b'rev2b', other_branch=other_tree.branch)
455
merger.merge_type = _mod_merge.Merge3Merger
456
tree_merger = merger.make_merger()
457
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
458
self.assertEqual(b'rev2b',
459
tree_merger.other_tree.get_revision_id())
460
self.assertEqual(b'rev1',
461
tree_merger.base_tree.get_revision_id())
462
self.assertEqual(other_tree.branch, tree_merger.other_branch)
464
def test_make_preview_transform(self):
465
this_tree = self.make_branch_and_tree('this')
466
self.build_tree_contents([('this/file', b'1\n')])
467
this_tree.add('file', b'file-id')
468
this_tree.commit('rev1', rev_id=b'rev1')
469
other_tree = this_tree.controldir.sprout('other').open_workingtree()
470
self.build_tree_contents([('this/file', b'1\n2a\n')])
471
this_tree.commit('rev2', rev_id=b'rev2a')
472
self.build_tree_contents([('other/file', b'2b\n1\n')])
473
other_tree.commit('rev2', rev_id=b'rev2b')
474
this_tree.lock_write()
475
self.addCleanup(this_tree.unlock)
476
merger = _mod_merge.Merger.from_revision_ids(
477
this_tree, b'rev2b', other_branch=other_tree.branch)
478
merger.merge_type = _mod_merge.Merge3Merger
479
tree_merger = merger.make_merger()
480
with tree_merger.make_preview_transform() as tt:
481
preview_tree = tt.get_preview_tree()
482
with this_tree.get_file('file') as tree_file:
483
self.assertEqual(b'1\n2a\n', tree_file.read())
484
with preview_tree.get_file('file') as preview_file:
485
self.assertEqual(b'2b\n1\n2a\n', preview_file.read())
487
def test_do_merge(self):
488
this_tree = self.make_branch_and_tree('this')
489
self.build_tree_contents([('this/file', b'1\n')])
490
this_tree.add('file', b'file-id')
491
this_tree.commit('rev1', rev_id=b'rev1')
492
other_tree = this_tree.controldir.sprout('other').open_workingtree()
493
self.build_tree_contents([('this/file', b'1\n2a\n')])
494
this_tree.commit('rev2', rev_id=b'rev2a')
495
self.build_tree_contents([('other/file', b'2b\n1\n')])
496
other_tree.commit('rev2', rev_id=b'rev2b')
497
this_tree.lock_write()
498
self.addCleanup(this_tree.unlock)
499
merger = _mod_merge.Merger.from_revision_ids(
500
this_tree, b'rev2b', other_branch=other_tree.branch)
501
merger.merge_type = _mod_merge.Merge3Merger
502
tree_merger = merger.make_merger()
503
tt = tree_merger.do_merge()
504
with this_tree.get_file('file') as tree_file:
505
self.assertEqual(b'2b\n1\n2a\n', tree_file.read())
507
def test_merge_require_tree_root(self):
508
tree = self.make_branch_and_tree(".")
510
self.addCleanup(tree.unlock)
511
self.build_tree(['a'])
513
first_rev = tree.commit("added a")
514
old_root_id = tree.path2id('')
515
merger = _mod_merge.Merger.from_revision_ids(tree,
516
_mod_revision.NULL_REVISION,
518
merger.merge_type = _mod_merge.Merge3Merger
519
conflict_count = merger.do_merge()
520
self.assertEqual(0, conflict_count)
521
self.assertEqual({''}, set(tree.all_versioned_paths()))
522
tree.set_parent_ids([])
524
def test_merge_add_into_deleted_root(self):
525
# Yes, people actually do this. And report bugs if it breaks.
526
source = self.make_branch_and_tree('source', format='rich-root-pack')
527
self.build_tree(['source/foo/'])
528
source.add('foo', b'foo-id')
529
source.commit('Add foo')
530
target = source.controldir.sprout('target').open_workingtree()
531
subtree = target.extract('foo')
532
subtree.commit('Delete root')
533
self.build_tree(['source/bar'])
534
source.add('bar', b'bar-id')
535
source.commit('Add bar')
536
subtree.merge_from_branch(source.branch)
538
def test_merge_joined_branch(self):
539
source = self.make_branch_and_tree('source', format='rich-root-pack')
540
self.build_tree(['source/foo'])
542
source.commit('Add foo')
543
target = self.make_branch_and_tree('target', format='rich-root-pack')
544
self.build_tree(['target/bla'])
546
target.commit('Add bla')
547
nested = source.controldir.sprout('target/subtree').open_workingtree()
548
target.subsume(nested)
549
target.commit('Join nested')
550
self.build_tree(['source/bar'])
552
source.commit('Add bar')
553
target.merge_from_branch(source.branch)
554
target.commit('Merge source')
557
class TestPlanMerge(TestCaseWithMemoryTransport):
560
super(TestPlanMerge, self).setUp()
561
mapper = versionedfile.PrefixMapper()
562
factory = knit.make_file_factory(True, mapper)
563
self.vf = factory(self.get_transport())
564
self.plan_merge_vf = versionedfile._PlanMergeVersionedFile(b'root')
565
self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
567
def add_version(self, key, parents, text):
568
self.vf.add_lines(key, parents, [int2byte(
569
c) + b'\n' for c in bytearray(text)])
571
def add_rev(self, prefix, revision_id, parents, text):
572
self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
575
def add_uncommitted_version(self, key, parents, text):
576
self.plan_merge_vf.add_lines(key, parents,
577
[int2byte(c) + b'\n' for c in bytearray(text)])
579
def setup_plan_merge(self):
580
self.add_rev(b'root', b'A', [], b'abc')
581
self.add_rev(b'root', b'B', [b'A'], b'acehg')
582
self.add_rev(b'root', b'C', [b'A'], b'fabg')
583
return _PlanMerge(b'B', b'C', self.plan_merge_vf, (b'root',))
585
def setup_plan_merge_uncommitted(self):
586
self.add_version((b'root', b'A'), [], b'abc')
587
self.add_uncommitted_version(
588
(b'root', b'B:'), [(b'root', b'A')], b'acehg')
589
self.add_uncommitted_version(
590
(b'root', b'C:'), [(b'root', b'A')], b'fabg')
591
return _PlanMerge(b'B:', b'C:', self.plan_merge_vf, (b'root',))
593
def test_base_from_plan(self):
594
self.setup_plan_merge()
595
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
596
pwm = versionedfile.PlanWeaveMerge(plan)
597
self.assertEqual([b'a\n', b'b\n', b'c\n'], pwm.base_from_plan())
599
def test_unique_lines(self):
600
plan = self.setup_plan_merge()
601
self.assertEqual(plan._unique_lines(
602
plan._get_matching_blocks(b'B', b'C')),
605
def test_plan_merge(self):
606
self.setup_plan_merge()
607
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
610
('unchanged', b'a\n'),
611
('killed-a', b'b\n'),
612
('killed-b', b'c\n'),
619
def test_plan_merge_cherrypick(self):
620
self.add_rev(b'root', b'A', [], b'abc')
621
self.add_rev(b'root', b'B', [b'A'], b'abcde')
622
self.add_rev(b'root', b'C', [b'A'], b'abcefg')
623
self.add_rev(b'root', b'D', [b'A', b'B', b'C'], b'abcdegh')
624
my_plan = _PlanMerge(b'B', b'D', self.plan_merge_vf, (b'root',))
625
# We shortcut when one text supersedes the other in the per-file graph.
626
# We don't actually need to compare the texts at this point.
635
list(my_plan.plan_merge()))
637
def test_plan_merge_no_common_ancestor(self):
638
self.add_rev(b'root', b'A', [], b'abc')
639
self.add_rev(b'root', b'B', [], b'xyz')
640
my_plan = _PlanMerge(b'A', b'B', self.plan_merge_vf, (b'root',))
648
list(my_plan.plan_merge()))
650
def test_plan_merge_tail_ancestors(self):
651
# The graph looks like this:
652
# A # Common to all ancestors
654
# B C # Ancestors of E, only common to one side
656
# D E F # D, F are unique to G, H respectively
657
# |/ \| # E is the LCA for G & H, and the unique LCA for
662
# I J # criss-cross merge of G, H
664
# In this situation, a simple pruning of ancestors of E will leave D &
665
# F "dangling", which looks like they introduce lines different from
666
# the ones in E, but in actuality C&B introduced the lines, and they
667
# are already present in E
669
# Introduce the base text
670
self.add_rev(b'root', b'A', [], b'abc')
671
# Introduces a new line B
672
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
673
# Introduces a new line C
674
self.add_rev(b'root', b'C', [b'A'], b'abCc')
675
# Introduce new line D
676
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
677
# Merges B and C by just incorporating both
678
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
679
# Introduce new line F
680
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
681
# Merge D & E by just combining the texts
682
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
683
# Merge F & E by just combining the texts
684
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
685
# Merge G & H by just combining texts
686
self.add_rev(b'root', b'I', [b'G', b'H'], b'DaBbCcF')
687
# Merge G & H but supersede an old line in B
688
self.add_rev(b'root', b'J', [b'H', b'G'], b'DaJbCcF')
689
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
691
('unchanged', b'D\n'),
692
('unchanged', b'a\n'),
693
('killed-b', b'B\n'),
695
('unchanged', b'b\n'),
696
('unchanged', b'C\n'),
697
('unchanged', b'c\n'),
698
('unchanged', b'F\n')],
701
def test_plan_merge_tail_triple_ancestors(self):
702
# The graph looks like this:
703
# A # Common to all ancestors
705
# B C # Ancestors of E, only common to one side
707
# D E F # D, F are unique to G, H respectively
708
# |/|\| # E is the LCA for G & H, and the unique LCA for
710
# |\ /| # Q is just an extra node which is merged into both
713
# I J # criss-cross merge of G, H
715
# This is the same as the test_plan_merge_tail_ancestors, except we add
716
# a third LCA that doesn't add new lines, but will trigger our more
717
# involved ancestry logic
719
self.add_rev(b'root', b'A', [], b'abc')
720
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
721
self.add_rev(b'root', b'C', [b'A'], b'abCc')
722
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
723
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
724
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
725
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
726
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
727
self.add_rev(b'root', b'Q', [b'E'], b'aBbCc')
728
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DaBbCcF')
729
# Merge G & H but supersede an old line in B
730
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DaJbCcF')
731
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
733
('unchanged', b'D\n'),
734
('unchanged', b'a\n'),
735
('killed-b', b'B\n'),
737
('unchanged', b'b\n'),
738
('unchanged', b'C\n'),
739
('unchanged', b'c\n'),
740
('unchanged', b'F\n')],
743
def test_plan_merge_2_tail_triple_ancestors(self):
744
# The graph looks like this:
745
# A B # 2 tails going back to NULL
747
# D E F # D, is unique to G, F to H
748
# |/|\| # E is the LCA for G & H, and the unique LCA for
750
# |\ /| # Q is just an extra node which is merged into both
753
# I J # criss-cross merge of G, H (and Q)
756
# This is meant to test after hitting a 3-way LCA, and multiple tail
757
# ancestors (only have NULL_REVISION in common)
759
self.add_rev(b'root', b'A', [], b'abc')
760
self.add_rev(b'root', b'B', [], b'def')
761
self.add_rev(b'root', b'D', [b'A'], b'Dabc')
762
self.add_rev(b'root', b'E', [b'A', b'B'], b'abcdef')
763
self.add_rev(b'root', b'F', [b'B'], b'defF')
764
self.add_rev(b'root', b'G', [b'D', b'E'], b'Dabcdef')
765
self.add_rev(b'root', b'H', [b'F', b'E'], b'abcdefF')
766
self.add_rev(b'root', b'Q', [b'E'], b'abcdef')
767
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DabcdefF')
768
# Merge G & H but supersede an old line in B
769
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DabcdJfF')
770
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
772
('unchanged', b'D\n'),
773
('unchanged', b'a\n'),
774
('unchanged', b'b\n'),
775
('unchanged', b'c\n'),
776
('unchanged', b'd\n'),
777
('killed-b', b'e\n'),
779
('unchanged', b'f\n'),
780
('unchanged', b'F\n')],
783
def test_plan_merge_uncommitted_files(self):
784
self.setup_plan_merge_uncommitted()
785
plan = self.plan_merge_vf.plan_merge(b'B:', b'C:')
788
('unchanged', b'a\n'),
789
('killed-a', b'b\n'),
790
('killed-b', b'c\n'),
797
def test_plan_merge_insert_order(self):
798
"""Weave merges are sensitive to the order of insertion.
800
Specifically for overlapping regions, it effects which region gets put
801
'first'. And when a user resolves an overlapping merge, if they use the
802
same ordering, then the lines match the parents, if they don't only
803
*some* of the lines match.
805
self.add_rev(b'root', b'A', [], b'abcdef')
806
self.add_rev(b'root', b'B', [b'A'], b'abwxcdef')
807
self.add_rev(b'root', b'C', [b'A'], b'abyzcdef')
808
# Merge, and resolve the conflict by adding *both* sets of lines
809
# If we get the ordering wrong, these will look like new lines in D,
810
# rather than carried over from B, C
811
self.add_rev(b'root', b'D', [b'B', b'C'],
813
# Supersede the lines in B and delete the lines in C, which will
814
# conflict if they are treated as being in D
815
self.add_rev(b'root', b'E', [b'C', b'B'],
817
# Same thing for the lines in C
818
self.add_rev(b'root', b'F', [b'C'], b'abpqcdef')
819
plan = self.plan_merge_vf.plan_merge(b'D', b'E')
821
('unchanged', b'a\n'),
822
('unchanged', b'b\n'),
823
('killed-b', b'w\n'),
824
('killed-b', b'x\n'),
825
('killed-b', b'y\n'),
826
('killed-b', b'z\n'),
829
('unchanged', b'c\n'),
830
('unchanged', b'd\n'),
831
('unchanged', b'e\n'),
832
('unchanged', b'f\n')],
834
plan = self.plan_merge_vf.plan_merge(b'E', b'D')
835
# Going in the opposite direction shows the effect of the opposite plan
837
('unchanged', b'a\n'),
838
('unchanged', b'b\n'),
841
('killed-a', b'y\n'),
842
('killed-a', b'z\n'),
843
('killed-both', b'w\n'),
844
('killed-both', b'x\n'),
847
('unchanged', b'c\n'),
848
('unchanged', b'd\n'),
849
('unchanged', b'e\n'),
850
('unchanged', b'f\n')],
853
def test_plan_merge_criss_cross(self):
854
# This is specificly trying to trigger problems when using limited
855
# ancestry and weaves. The ancestry graph looks like:
856
# XX unused ancestor, should not show up in the weave
860
# B \ Introduces a line 'foo'
862
# C D E C & D both have 'foo', E has different changes
866
# F G All of C, D, E are merged into F and G, so they are
867
# all common ancestors.
869
# The specific issue with weaves:
870
# B introduced a text ('foo') that is present in both C and D.
871
# If we do not include B (because it isn't an ancestor of E), then
872
# the A=>C and A=>D look like both sides independently introduce the
873
# text ('foo'). If F does not modify the text, it would still appear
874
# to have deleted on of the versions from C or D. If G then modifies
875
# 'foo', it should appear as superseding the value in F (since it
876
# came from B), rather than conflict because of the resolution during
878
self.add_rev(b'root', b'XX', [], b'qrs')
879
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
880
self.add_rev(b'root', b'B', [b'A'], b'axcdef')
881
self.add_rev(b'root', b'C', [b'B'], b'axcdefg')
882
self.add_rev(b'root', b'D', [b'B'], b'haxcdef')
883
self.add_rev(b'root', b'E', [b'A'], b'abcdyf')
884
# Simple combining of all texts
885
self.add_rev(b'root', b'F', [b'C', b'D', b'E'], b'haxcdyfg')
886
# combine and supersede 'x'
887
self.add_rev(b'root', b'G', [b'C', b'D', b'E'], b'hazcdyfg')
888
plan = self.plan_merge_vf.plan_merge(b'F', b'G')
890
('unchanged', b'h\n'),
891
('unchanged', b'a\n'),
892
('killed-base', b'b\n'),
893
('killed-b', b'x\n'),
895
('unchanged', b'c\n'),
896
('unchanged', b'd\n'),
897
('killed-base', b'e\n'),
898
('unchanged', b'y\n'),
899
('unchanged', b'f\n'),
900
('unchanged', b'g\n')],
902
plan = self.plan_merge_vf.plan_lca_merge(b'F', b'G')
903
# This is one of the main differences between plan_merge and
904
# plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
905
# because 'x' was not present in one of the bases. However, in this
906
# case it is spurious because 'x' does not exist in the global base A.
908
('unchanged', b'h\n'),
909
('unchanged', b'a\n'),
910
('conflicted-a', b'x\n'),
912
('unchanged', b'c\n'),
913
('unchanged', b'd\n'),
914
('unchanged', b'y\n'),
915
('unchanged', b'f\n'),
916
('unchanged', b'g\n')],
919
def test_criss_cross_flip_flop(self):
920
# This is specificly trying to trigger problems when using limited
921
# ancestry and weaves. The ancestry graph looks like:
922
# XX unused ancestor, should not show up in the weave
926
# B C B & C both introduce a new line
930
# D E B & C are both merged, so both are common ancestors
931
# In the process of merging, both sides order the new
934
self.add_rev(b'root', b'XX', [], b'qrs')
935
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
936
self.add_rev(b'root', b'B', [b'A'], b'abcdgef')
937
self.add_rev(b'root', b'C', [b'A'], b'abcdhef')
938
self.add_rev(b'root', b'D', [b'B', b'C'], b'abcdghef')
939
self.add_rev(b'root', b'E', [b'C', b'B'], b'abcdhgef')
940
plan = list(self.plan_merge_vf.plan_merge(b'D', b'E'))
942
('unchanged', b'a\n'),
943
('unchanged', b'b\n'),
944
('unchanged', b'c\n'),
945
('unchanged', b'd\n'),
947
('unchanged', b'g\n'),
948
('killed-b', b'h\n'),
949
('unchanged', b'e\n'),
950
('unchanged', b'f\n'),
952
pwm = versionedfile.PlanWeaveMerge(plan)
953
self.assertEqualDiff(b'a\nb\nc\nd\ng\nh\ne\nf\n',
954
b''.join(pwm.base_from_plan()))
955
# Reversing the order reverses the merge plan, and final order of 'hg'
957
plan = list(self.plan_merge_vf.plan_merge(b'E', b'D'))
959
('unchanged', b'a\n'),
960
('unchanged', b'b\n'),
961
('unchanged', b'c\n'),
962
('unchanged', b'd\n'),
964
('unchanged', b'h\n'),
965
('killed-b', b'g\n'),
966
('unchanged', b'e\n'),
967
('unchanged', b'f\n'),
969
pwm = versionedfile.PlanWeaveMerge(plan)
970
self.assertEqualDiff(b'a\nb\nc\nd\nh\ng\ne\nf\n',
971
b''.join(pwm.base_from_plan()))
972
# This is where lca differs, in that it (fairly correctly) determines
973
# that there is a conflict because both sides resolved the merge
975
plan = list(self.plan_merge_vf.plan_lca_merge(b'D', b'E'))
977
('unchanged', b'a\n'),
978
('unchanged', b'b\n'),
979
('unchanged', b'c\n'),
980
('unchanged', b'd\n'),
981
('conflicted-b', b'h\n'),
982
('unchanged', b'g\n'),
983
('conflicted-a', b'h\n'),
984
('unchanged', b'e\n'),
985
('unchanged', b'f\n'),
987
pwm = versionedfile.PlanWeaveMerge(plan)
988
self.assertEqualDiff(b'a\nb\nc\nd\ng\ne\nf\n',
989
b''.join(pwm.base_from_plan()))
990
# Reversing it changes what line is doubled, but still gives a
992
plan = list(self.plan_merge_vf.plan_lca_merge(b'E', b'D'))
994
('unchanged', b'a\n'),
995
('unchanged', b'b\n'),
996
('unchanged', b'c\n'),
997
('unchanged', b'd\n'),
998
('conflicted-b', b'g\n'),
999
('unchanged', b'h\n'),
1000
('conflicted-a', b'g\n'),
1001
('unchanged', b'e\n'),
1002
('unchanged', b'f\n'),
1004
pwm = versionedfile.PlanWeaveMerge(plan)
1005
self.assertEqualDiff(b'a\nb\nc\nd\nh\ne\nf\n',
1006
b''.join(pwm.base_from_plan()))
1008
def assertRemoveExternalReferences(self, filtered_parent_map,
1009
child_map, tails, parent_map):
1010
"""Assert results for _PlanMerge._remove_external_references."""
1011
(act_filtered_parent_map, act_child_map,
1012
act_tails) = _PlanMerge._remove_external_references(parent_map)
1014
# The parent map *should* preserve ordering, but the ordering of
1015
# children is not strictly defined
1016
# child_map = dict((k, sorted(children))
1017
# for k, children in child_map.iteritems())
1018
# act_child_map = dict(k, sorted(children)
1019
# for k, children in act_child_map.iteritems())
1020
self.assertEqual(filtered_parent_map, act_filtered_parent_map)
1021
self.assertEqual(child_map, act_child_map)
1022
self.assertEqual(sorted(tails), sorted(act_tails))
1024
def test__remove_external_references(self):
1025
# First, nothing to remove
1026
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1027
{1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
1028
# The reverse direction
1029
self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
1030
{3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
1032
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1033
{1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
1035
self.assertRemoveExternalReferences(
1036
{4: [2, 3], 3: [], 2: [1], 1: []},
1037
{1: [2], 2: [4], 3: [4], 4: []},
1039
{4: [2, 3], 3: [5], 2: [1], 1: [6]})
1041
self.assertRemoveExternalReferences(
1042
{1: [3], 2: [3, 4], 3: [], 4: []},
1043
{1: [], 2: [], 3: [1, 2], 4: [2]},
1045
{1: [3], 2: [3, 4], 3: [5], 4: []})
1047
def assertPruneTails(self, pruned_map, tails, parent_map):
1049
for key, parent_keys in parent_map.items():
1050
child_map.setdefault(key, [])
1051
for pkey in parent_keys:
1052
child_map.setdefault(pkey, []).append(key)
1053
_PlanMerge._prune_tails(parent_map, child_map, tails)
1054
self.assertEqual(pruned_map, parent_map)
1056
def test__prune_tails(self):
1057
# Nothing requested to prune
1058
self.assertPruneTails({1: [], 2: [], 3: []}, [],
1059
{1: [], 2: [], 3: []})
1060
# Prune a single entry
1061
self.assertPruneTails({1: [], 3: []}, [2],
1062
{1: [], 2: [], 3: []})
1064
self.assertPruneTails({1: []}, [3],
1065
{1: [], 2: [3], 3: []})
1066
# Prune a chain with a diamond
1067
self.assertPruneTails({1: []}, [5],
1068
{1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
1069
# Prune a partial chain
1070
self.assertPruneTails({1: [6], 6: []}, [5],
1071
{1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
1073
# Prune a chain with multiple tips, that pulls out intermediates
1074
self.assertPruneTails({1: [3], 3: []}, [4, 5],
1075
{1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []})
1076
self.assertPruneTails({1: [3], 3: []}, [5, 4],
1077
{1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []})
1079
def test_subtract_plans(self):
1081
('unchanged', b'a\n'),
1083
('killed-a', b'c\n'),
1086
('killed-b', b'f\n'),
1087
('killed-b', b'g\n'),
1090
('unchanged', b'a\n'),
1092
('killed-a', b'c\n'),
1095
('killed-b', b'f\n'),
1096
('killed-b', b'i\n'),
1099
('unchanged', b'a\n'),
1101
('killed-a', b'c\n'),
1103
('unchanged', b'f\n'),
1104
('killed-b', b'i\n'),
1106
self.assertEqual(subtracted_plan,
1107
list(_PlanMerge._subtract_plans(old_plan, new_plan)))
1109
def setup_merge_with_base(self):
1110
self.add_rev(b'root', b'COMMON', [], b'abc')
1111
self.add_rev(b'root', b'THIS', [b'COMMON'], b'abcd')
1112
self.add_rev(b'root', b'BASE', [b'COMMON'], b'eabc')
1113
self.add_rev(b'root', b'OTHER', [b'BASE'], b'eafb')
1115
def test_plan_merge_with_base(self):
1116
self.setup_merge_with_base()
1117
plan = self.plan_merge_vf.plan_merge(b'THIS', b'OTHER', b'BASE')
1118
self.assertEqual([('unchanged', b'a\n'),
1120
('unchanged', b'b\n'),
1121
('killed-b', b'c\n'),
1125
def test_plan_lca_merge(self):
1126
self.setup_plan_merge()
1127
plan = self.plan_merge_vf.plan_lca_merge(b'B', b'C')
1130
('unchanged', b'a\n'),
1131
('killed-b', b'c\n'),
1134
('killed-a', b'b\n'),
1135
('unchanged', b'g\n')],
1138
def test_plan_lca_merge_uncommitted_files(self):
1139
self.setup_plan_merge_uncommitted()
1140
plan = self.plan_merge_vf.plan_lca_merge(b'B:', b'C:')
1143
('unchanged', b'a\n'),
1144
('killed-b', b'c\n'),
1147
('killed-a', b'b\n'),
1148
('unchanged', b'g\n')],
1151
def test_plan_lca_merge_with_base(self):
1152
self.setup_merge_with_base()
1153
plan = self.plan_merge_vf.plan_lca_merge(b'THIS', b'OTHER', b'BASE')
1154
self.assertEqual([('unchanged', b'a\n'),
1156
('unchanged', b'b\n'),
1157
('killed-b', b'c\n'),
1161
def test_plan_lca_merge_with_criss_cross(self):
1162
self.add_version((b'root', b'ROOT'), [], b'abc')
1163
# each side makes a change
1164
self.add_version((b'root', b'REV1'), [(b'root', b'ROOT')], b'abcd')
1165
self.add_version((b'root', b'REV2'), [(b'root', b'ROOT')], b'abce')
1166
# both sides merge, discarding others' changes
1167
self.add_version((b'root', b'LCA1'),
1168
[(b'root', b'REV1'), (b'root', b'REV2')], b'abcd')
1169
self.add_version((b'root', b'LCA2'),
1170
[(b'root', b'REV1'), (b'root', b'REV2')], b'fabce')
1171
plan = self.plan_merge_vf.plan_lca_merge(b'LCA1', b'LCA2')
1172
self.assertEqual([('new-b', b'f\n'),
1173
('unchanged', b'a\n'),
1174
('unchanged', b'b\n'),
1175
('unchanged', b'c\n'),
1176
('conflicted-a', b'd\n'),
1177
('conflicted-b', b'e\n'),
1180
def test_plan_lca_merge_with_null(self):
1181
self.add_version((b'root', b'A'), [], b'ab')
1182
self.add_version((b'root', b'B'), [], b'bc')
1183
plan = self.plan_merge_vf.plan_lca_merge(b'A', b'B')
1184
self.assertEqual([('new-a', b'a\n'),
1185
('unchanged', b'b\n'),
1189
def test_plan_merge_with_delete_and_change(self):
1190
self.add_rev(b'root', b'C', [], b'a')
1191
self.add_rev(b'root', b'A', [b'C'], b'b')
1192
self.add_rev(b'root', b'B', [b'C'], b'')
1193
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1194
self.assertEqual([('killed-both', b'a\n'),
1198
def test_plan_merge_with_move_and_change(self):
1199
self.add_rev(b'root', b'C', [], b'abcd')
1200
self.add_rev(b'root', b'A', [b'C'], b'acbd')
1201
self.add_rev(b'root', b'B', [b'C'], b'aBcd')
1202
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1203
self.assertEqual([('unchanged', b'a\n'),
1205
('killed-b', b'b\n'),
1207
('killed-a', b'c\n'),
1208
('unchanged', b'd\n'),
1212
class LoggingMerger(object):
1213
# These seem to be the required attributes
1214
requires_base = False
1215
supports_reprocess = False
1216
supports_show_base = False
1217
supports_cherrypick = False
1218
# We intentionally do not define supports_lca_trees
1220
def __init__(self, *args, **kwargs):
1222
self.kwargs = kwargs
1225
class TestMergerBase(TestCaseWithMemoryTransport):
1226
"""Common functionality for Merger tests that don't write to disk."""
1228
def get_builder(self):
1229
builder = self.make_branch_builder('path')
1230
builder.start_series()
1231
self.addCleanup(builder.finish_series)
1234
def setup_simple_graph(self):
1235
"""Create a simple 3-node graph.
1237
:return: A BranchBuilder
1244
builder = self.get_builder()
1245
builder.build_snapshot(None,
1246
[('add', ('', None, 'directory', None))],
1247
revision_id=b'A-id')
1248
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1249
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1252
def setup_criss_cross_graph(self):
1253
"""Create a 5-node graph with a criss-cross.
1255
:return: A BranchBuilder
1262
builder = self.setup_simple_graph()
1263
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1264
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1267
def make_Merger(self, builder, other_revision_id, interesting_files=None):
1268
"""Make a Merger object from a branch builder"""
1269
mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1270
mem_tree.lock_write()
1271
self.addCleanup(mem_tree.unlock)
1272
merger = _mod_merge.Merger.from_revision_ids(
1273
mem_tree, other_revision_id)
1274
merger.set_interesting_files(interesting_files)
1275
merger.merge_type = _mod_merge.Merge3Merger
1279
class TestMergerInMemory(TestMergerBase):
1281
def test_cache_trees_with_revision_ids_None(self):
1282
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1283
original_cache = dict(merger._cached_trees)
1284
merger.cache_trees_with_revision_ids([None])
1285
self.assertEqual(original_cache, merger._cached_trees)
1287
def test_cache_trees_with_revision_ids_no_revision_id(self):
1288
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1289
original_cache = dict(merger._cached_trees)
1290
tree = self.make_branch_and_memory_tree('tree')
1291
merger.cache_trees_with_revision_ids([tree])
1292
self.assertEqual(original_cache, merger._cached_trees)
1294
def test_cache_trees_with_revision_ids_having_revision_id(self):
1295
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1296
original_cache = dict(merger._cached_trees)
1297
tree = merger.this_branch.repository.revision_tree(b'B-id')
1298
original_cache[b'B-id'] = tree
1299
merger.cache_trees_with_revision_ids([tree])
1300
self.assertEqual(original_cache, merger._cached_trees)
1302
def test_find_base(self):
1303
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1304
self.assertEqual(b'A-id', merger.base_rev_id)
1305
self.assertFalse(merger._is_criss_cross)
1306
self.assertIs(None, merger._lca_trees)
1308
def test_find_base_criss_cross(self):
1309
builder = self.setup_criss_cross_graph()
1310
merger = self.make_Merger(builder, b'E-id')
1311
self.assertEqual(b'A-id', merger.base_rev_id)
1312
self.assertTrue(merger._is_criss_cross)
1313
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1314
for t in merger._lca_trees])
1315
# If we swap the order, we should get a different lca order
1316
builder.build_snapshot([b'E-id'], [], revision_id=b'F-id')
1317
merger = self.make_Merger(builder, b'D-id')
1318
self.assertEqual([b'C-id', b'B-id'], [t.get_revision_id()
1319
for t in merger._lca_trees])
1321
def test_find_base_triple_criss_cross(self):
1324
# B C F # F is merged into both branches
1331
builder = self.setup_criss_cross_graph()
1332
builder.build_snapshot([b'A-id'], [], revision_id=b'F-id')
1333
builder.build_snapshot([b'E-id', b'F-id'], [], revision_id=b'H-id')
1334
builder.build_snapshot([b'D-id', b'F-id'], [], revision_id=b'G-id')
1335
merger = self.make_Merger(builder, b'H-id')
1336
self.assertEqual([b'B-id', b'C-id', b'F-id'],
1337
[t.get_revision_id() for t in merger._lca_trees])
1339
def test_find_base_new_root_criss_cross(self):
1345
builder = self.get_builder()
1346
builder.build_snapshot(None,
1347
[('add', ('', None, 'directory', None))],
1348
revision_id=b'A-id')
1349
builder.build_snapshot([],
1350
[('add', ('', None, 'directory', None))],
1351
revision_id=b'B-id')
1352
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'D-id')
1353
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'C-id')
1354
merger = self.make_Merger(builder, b'D-id')
1355
self.assertEqual(b'A-id', merger.base_rev_id)
1356
self.assertTrue(merger._is_criss_cross)
1357
self.assertEqual([b'A-id', b'B-id'], [t.get_revision_id()
1358
for t in merger._lca_trees])
1360
def test_no_criss_cross_passed_to_merge_type(self):
1361
class LCATreesMerger(LoggingMerger):
1362
supports_lca_trees = True
1364
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1365
merger.merge_type = LCATreesMerger
1366
merge_obj = merger.make_merger()
1367
self.assertIsInstance(merge_obj, LCATreesMerger)
1368
self.assertFalse('lca_trees' in merge_obj.kwargs)
1370
def test_criss_cross_passed_to_merge_type(self):
1371
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1372
merger.merge_type = _mod_merge.Merge3Merger
1373
merge_obj = merger.make_merger()
1374
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1375
for t in merger._lca_trees])
1377
def test_criss_cross_not_supported_merge_type(self):
1378
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1379
# We explicitly do not define supports_lca_trees
1380
merger.merge_type = LoggingMerger
1381
merge_obj = merger.make_merger()
1382
self.assertIsInstance(merge_obj, LoggingMerger)
1383
self.assertFalse('lca_trees' in merge_obj.kwargs)
1385
def test_criss_cross_unsupported_merge_type(self):
1386
class UnsupportedLCATreesMerger(LoggingMerger):
1387
supports_lca_trees = False
1389
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1390
merger.merge_type = UnsupportedLCATreesMerger
1391
merge_obj = merger.make_merger()
1392
self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
1393
self.assertFalse('lca_trees' in merge_obj.kwargs)
1396
class TestMergerEntriesLCA(TestMergerBase):
1398
def make_merge_obj(self, builder, other_revision_id,
1399
interesting_files=None):
1400
merger = self.make_Merger(builder, other_revision_id,
1401
interesting_files=interesting_files)
1402
return merger.make_merger()
1404
def test_simple(self):
1405
builder = self.get_builder()
1406
builder.build_snapshot(None,
1407
[('add', (u'', b'a-root-id', 'directory', None)),
1408
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1409
revision_id=b'A-id')
1410
builder.build_snapshot([b'A-id'],
1411
[('modify', ('a', b'a\nb\nC\nc\n'))],
1412
revision_id=b'C-id')
1413
builder.build_snapshot([b'A-id'],
1414
[('modify', ('a', b'a\nB\nb\nc\n'))],
1415
revision_id=b'B-id')
1416
builder.build_snapshot([b'C-id', b'B-id'],
1417
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1418
revision_id=b'E-id')
1419
builder.build_snapshot([b'B-id', b'C-id'],
1420
[('modify', ('a', b'a\nB\nb\nC\nc\n'))],
1421
revision_id=b'D-id', )
1422
merge_obj = self.make_merge_obj(builder, b'E-id')
1424
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1425
for t in merge_obj._lca_trees])
1426
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1427
entries = list(merge_obj._entries_lca())
1429
# (file_id, changed, parents, names, executable)
1430
# BASE, lca1, lca2, OTHER, THIS
1431
root_id = b'a-root-id'
1432
self.assertEqual([(b'a-id', True,
1433
((u'a', [u'a', u'a']), u'a', u'a'),
1434
((root_id, [root_id, root_id]), root_id, root_id),
1435
((u'a', [u'a', u'a']), u'a', u'a'),
1436
((False, [False, False]), False, False)),
1439
def test_not_in_base(self):
1440
# LCAs all have the same last-modified revision for the file, as do
1441
# the tips, but the base has something different
1442
# A base, doesn't have the file
1444
# B C B introduces 'foo', C introduces 'bar'
1446
# D E D and E now both have 'foo' and 'bar'
1448
# F G the files are now in F, G, D and E, but not in A
1451
builder = self.get_builder()
1452
builder.build_snapshot(None,
1453
[('add', (u'', b'a-root-id', 'directory', None))],
1454
revision_id=b'A-id')
1455
builder.build_snapshot([b'A-id'],
1456
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1457
revision_id=b'B-id')
1458
builder.build_snapshot([b'A-id'],
1459
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1460
revision_id=b'C-id')
1461
builder.build_snapshot([b'B-id', b'C-id'],
1462
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1463
revision_id=b'D-id')
1464
builder.build_snapshot([b'C-id', b'B-id'],
1465
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1466
revision_id=b'E-id')
1467
builder.build_snapshot([b'E-id', b'D-id'],
1468
[('modify', (u'bar', b'd\ne\nf\nG\n'))],
1469
revision_id=b'G-id')
1470
builder.build_snapshot([b'D-id', b'E-id'], [], revision_id=b'F-id')
1471
merge_obj = self.make_merge_obj(builder, b'G-id')
1473
self.assertEqual([b'D-id', b'E-id'], [t.get_revision_id()
1474
for t in merge_obj._lca_trees])
1475
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1476
entries = list(merge_obj._entries_lca())
1477
root_id = b'a-root-id'
1478
self.assertEqual([(b'bar-id', True,
1479
((None, [u'bar', u'bar']), u'bar', u'bar'),
1480
((None, [root_id, root_id]), root_id, root_id),
1481
((None, [u'bar', u'bar']), u'bar', u'bar'),
1482
((None, [False, False]), False, False)),
1485
def test_not_in_this(self):
1486
builder = self.get_builder()
1487
builder.build_snapshot(None,
1488
[('add', (u'', b'a-root-id', 'directory', None)),
1489
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1490
revision_id=b'A-id')
1491
builder.build_snapshot([b'A-id'],
1492
[('modify', ('a', b'a\nB\nb\nc\n'))],
1493
revision_id=b'B-id')
1494
builder.build_snapshot([b'A-id'],
1495
[('modify', ('a', b'a\nb\nC\nc\n'))],
1496
revision_id=b'C-id')
1497
builder.build_snapshot([b'C-id', b'B-id'],
1498
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1499
revision_id=b'E-id')
1500
builder.build_snapshot([b'B-id', b'C-id'],
1501
[('unversion', 'a')],
1502
revision_id=b'D-id')
1503
merge_obj = self.make_merge_obj(builder, b'E-id')
1505
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1506
for t in merge_obj._lca_trees])
1507
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1509
entries = list(merge_obj._entries_lca())
1510
root_id = b'a-root-id'
1511
self.assertEqual([(b'a-id', True,
1512
((u'a', [u'a', u'a']), u'a', None),
1513
((root_id, [root_id, root_id]), root_id, None),
1514
((u'a', [u'a', u'a']), u'a', None),
1515
((False, [False, False]), False, None)),
1518
def test_file_not_in_one_lca(self):
1521
# B C # B no file, C introduces a file
1523
# D E # D and E both have the file, unchanged from C
1524
builder = self.get_builder()
1525
builder.build_snapshot(None,
1526
[('add', (u'', b'a-root-id', 'directory', None))],
1527
revision_id=b'A-id')
1528
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1529
builder.build_snapshot([b'A-id'],
1530
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1531
revision_id=b'C-id')
1532
builder.build_snapshot([b'C-id', b'B-id'],
1533
[], revision_id=b'E-id') # Inherited from C
1534
builder.build_snapshot([b'B-id', b'C-id'], # Merged from C
1535
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1536
revision_id=b'D-id')
1537
merge_obj = self.make_merge_obj(builder, b'E-id')
1539
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1540
for t in merge_obj._lca_trees])
1541
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1543
entries = list(merge_obj._entries_lca())
1544
self.assertEqual([], entries)
1546
def test_not_in_other(self):
1547
builder = self.get_builder()
1548
builder.build_snapshot(None,
1549
[('add', (u'', b'a-root-id', 'directory', None)),
1550
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1551
revision_id=b'A-id')
1552
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1553
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1554
builder.build_snapshot(
1556
[('unversion', 'a')], revision_id=b'E-id')
1557
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1558
merge_obj = self.make_merge_obj(builder, b'E-id')
1560
entries = list(merge_obj._entries_lca())
1561
root_id = b'a-root-id'
1562
self.assertEqual([(b'a-id', True,
1563
((u'a', [u'a', u'a']), None, u'a'),
1564
((root_id, [root_id, root_id]), None, root_id),
1565
((u'a', [u'a', u'a']), None, u'a'),
1566
((False, [False, False]), None, False)),
1569
def test_not_in_other_or_lca(self):
1570
# A base, introduces 'foo'
1572
# B C B nothing, C deletes foo
1574
# D E D restores foo (same as B), E leaves it deleted
1576
# A => B, no changes
1577
# A => C, delete foo (C should supersede B)
1578
# C => D, restore foo
1579
# C => E, no changes
1580
# D would then win 'cleanly' and no record would be given
1581
builder = self.get_builder()
1582
builder.build_snapshot(None,
1583
[('add', (u'', b'a-root-id', 'directory', None)),
1584
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1585
revision_id=b'A-id')
1586
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1587
builder.build_snapshot([b'A-id'],
1588
[('unversion', 'foo')], revision_id=b'C-id')
1589
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1590
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1591
merge_obj = self.make_merge_obj(builder, b'E-id')
1593
entries = list(merge_obj._entries_lca())
1594
self.assertEqual([], entries)
1596
def test_not_in_other_mod_in_lca1_not_in_lca2(self):
1597
# A base, introduces 'foo'
1599
# B C B changes 'foo', C deletes foo
1601
# D E D restores foo (same as B), E leaves it deleted (as C)
1603
# A => B, modified foo
1604
# A => C, delete foo, C does not supersede B
1605
# B => D, no changes
1606
# C => D, resolve in favor of B
1607
# B => E, resolve in favor of E
1608
# C => E, no changes
1609
# In this case, we have a conflict of how the changes were resolved. E
1610
# picked C and D picked B, so we should issue a conflict
1611
builder = self.get_builder()
1612
builder.build_snapshot(None,
1613
[('add', (u'', b'a-root-id', 'directory', None)),
1614
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1615
revision_id=b'A-id')
1616
builder.build_snapshot([b'A-id'], [
1617
('modify', ('foo', b'new-content\n'))],
1618
revision_id=b'B-id')
1619
builder.build_snapshot([b'A-id'],
1620
[('unversion', 'foo')],
1621
revision_id=b'C-id')
1622
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1623
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1624
merge_obj = self.make_merge_obj(builder, b'E-id')
1626
entries = list(merge_obj._entries_lca())
1627
root_id = b'a-root-id'
1628
self.assertEqual([(b'foo-id', True,
1629
((u'foo', [u'foo', None]), None, u'foo'),
1630
((root_id, [root_id, None]), None, root_id),
1631
((u'foo', [u'foo', None]), None, 'foo'),
1632
((False, [False, None]), None, False)),
1635
def test_only_in_one_lca(self):
1638
# B C B nothing, C add file
1640
# D E D still has nothing, E removes file
1643
# C => D, removed the file
1645
# C => E, removed the file
1646
# Thus D & E have identical changes, and this is a no-op
1649
# A => C, add file, thus C supersedes B
1650
# w/ C=BASE, D=THIS, E=OTHER we have 'happy convergence'
1651
builder = self.get_builder()
1652
builder.build_snapshot(None,
1653
[('add', (u'', b'a-root-id', 'directory', None))],
1654
revision_id=b'A-id')
1655
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1656
builder.build_snapshot([b'A-id'],
1657
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1658
revision_id=b'C-id')
1659
builder.build_snapshot([b'C-id', b'B-id'],
1660
[('unversion', 'a')],
1661
revision_id=b'E-id')
1662
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1663
merge_obj = self.make_merge_obj(builder, b'E-id')
1665
entries = list(merge_obj._entries_lca())
1666
self.assertEqual([], entries)
1668
def test_only_in_other(self):
1669
builder = self.get_builder()
1670
builder.build_snapshot(None,
1671
[('add', (u'', b'a-root-id', 'directory', None))],
1672
revision_id=b'A-id')
1673
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1674
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1675
builder.build_snapshot([b'C-id', b'B-id'],
1676
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1677
revision_id=b'E-id')
1678
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1679
merge_obj = self.make_merge_obj(builder, b'E-id')
1681
entries = list(merge_obj._entries_lca())
1682
root_id = b'a-root-id'
1683
self.assertEqual([(b'a-id', True,
1684
((None, [None, None]), u'a', None),
1685
((None, [None, None]), root_id, None),
1686
((None, [None, None]), u'a', None),
1687
((None, [None, None]), False, None)),
1690
def test_one_lca_supersedes(self):
1691
# One LCA supersedes the other LCAs last modified value, but the
1692
# value is not the same as BASE.
1693
# A base, introduces 'foo', last mod A
1695
# B C B modifies 'foo' (mod B), C does nothing (mod A)
1697
# D E D does nothing (mod B), E updates 'foo' (mod E)
1699
# F G F updates 'foo' (mod F). G does nothing (mod E)
1701
# At this point, G should not be considered to modify 'foo', even
1702
# though its LCAs disagree. This is because the modification in E
1703
# completely supersedes the value in D.
1704
builder = self.get_builder()
1705
builder.build_snapshot(None,
1706
[('add', (u'', b'a-root-id', 'directory', None)),
1707
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1708
revision_id=b'A-id')
1709
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1710
builder.build_snapshot([b'A-id'],
1711
[('modify', ('foo', b'B content\n'))],
1712
revision_id=b'B-id')
1713
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1714
builder.build_snapshot([b'C-id', b'B-id'],
1715
[('modify', ('foo', b'E content\n'))],
1716
revision_id=b'E-id')
1717
builder.build_snapshot([b'E-id', b'D-id'], [], revision_id=b'G-id')
1718
builder.build_snapshot([b'D-id', b'E-id'],
1719
[('modify', ('foo', b'F content\n'))],
1720
revision_id=b'F-id')
1721
merge_obj = self.make_merge_obj(builder, b'G-id')
1723
self.assertEqual([], list(merge_obj._entries_lca()))
1725
def test_one_lca_supersedes_path(self):
1726
# Double-criss-cross merge, the ultimate base value is different from
1730
# B C B value 'bar', C = 'foo'
1732
# D E D = 'bar', E supersedes to 'bing'
1734
# F G F = 'bing', G supersedes to 'barry'
1736
# In this case, we technically should not care about the value 'bar' for
1737
# D, because it was clearly superseded by E's 'bing'. The
1738
# per-file/attribute graph would actually look like:
1747
# Because the other side of the merge never modifies the value, it just
1748
# takes the value from the merge.
1750
# ATM this fails because we will prune 'foo' from the LCAs, but we
1751
# won't prune 'bar'. This is getting far off into edge-case land, so we
1752
# aren't supporting it yet.
1754
builder = self.get_builder()
1755
builder.build_snapshot(None,
1756
[('add', (u'', b'a-root-id', 'directory', None)),
1757
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1758
revision_id=b'A-id')
1759
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1760
builder.build_snapshot([b'A-id'],
1761
[('rename', ('foo', 'bar'))],
1762
revision_id=b'B-id')
1763
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1764
builder.build_snapshot([b'C-id', b'B-id'],
1765
[('rename', ('foo', 'bing'))],
1766
revision_id=b'E-id') # override to bing
1767
builder.build_snapshot([b'E-id', b'D-id'],
1768
[('rename', ('bing', 'barry'))],
1769
revision_id=b'G-id') # override to barry
1770
builder.build_snapshot([b'D-id', b'E-id'],
1771
[('rename', ('bar', 'bing'))],
1772
revision_id=b'F-id') # Merge in E's change
1773
merge_obj = self.make_merge_obj(builder, b'G-id')
1775
self.expectFailure("We don't do an actual heads() check on lca values,"
1776
" or use the per-attribute graph",
1777
self.assertEqual, [], list(merge_obj._entries_lca()))
1779
def test_one_lca_accidentally_pruned(self):
1780
# Another incorrect resolution from the same basic flaw:
1783
# B C B value 'bar', C = 'foo'
1785
# D E D = 'bar', E reverts to 'foo'
1787
# F G F = 'bing', G switches to 'bar'
1789
# 'bar' will not be seen as an interesting change, because 'foo' will
1790
# be pruned from the LCAs, even though it was newly introduced by E
1792
builder = self.get_builder()
1793
builder.build_snapshot(None,
1794
[('add', (u'', b'a-root-id', 'directory', None)),
1795
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1796
revision_id=b'A-id')
1797
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1798
builder.build_snapshot([b'A-id'],
1799
[('rename', ('foo', 'bar'))],
1800
revision_id=b'B-id')
1801
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1802
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1803
builder.build_snapshot([b'E-id', b'D-id'],
1804
[('rename', ('foo', 'bar'))],
1805
revision_id=b'G-id')
1806
builder.build_snapshot([b'D-id', b'E-id'],
1807
[('rename', ('bar', 'bing'))],
1808
revision_id=b'F-id') # should end up conflicting
1809
merge_obj = self.make_merge_obj(builder, b'G-id')
1811
entries = list(merge_obj._entries_lca())
1812
root_id = b'a-root-id'
1813
self.expectFailure("We prune values from BASE even when relevant.",
1816
((root_id, [root_id, root_id]), root_id, root_id),
1817
((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
1818
((False, [False, False]), False, False)),
1821
def test_both_sides_revert(self):
1822
# Both sides of a criss-cross revert the text to the lca
1823
# A base, introduces 'foo'
1825
# B C B modifies 'foo', C modifies 'foo'
1827
# D E D reverts to B, E reverts to C
1828
# This should conflict
1829
builder = self.get_builder()
1830
builder.build_snapshot(None,
1831
[('add', (u'', b'a-root-id', 'directory', None)),
1832
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1833
revision_id=b'A-id')
1834
builder.build_snapshot([b'A-id'],
1835
[('modify', ('foo', b'B content\n'))],
1836
revision_id=b'B-id')
1837
builder.build_snapshot([b'A-id'],
1838
[('modify', ('foo', b'C content\n'))],
1839
revision_id=b'C-id')
1840
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1841
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1842
merge_obj = self.make_merge_obj(builder, b'E-id')
1844
entries = list(merge_obj._entries_lca())
1845
root_id = b'a-root-id'
1846
self.assertEqual([(b'foo-id', True,
1847
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1848
((root_id, [root_id, root_id]), root_id, root_id),
1849
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1850
((False, [False, False]), False, False)),
1853
def test_different_lca_resolve_one_side_updates_content(self):
1854
# Both sides converge, but then one side updates the text.
1855
# A base, introduces 'foo'
1857
# B C B modifies 'foo', C modifies 'foo'
1859
# D E D reverts to B, E reverts to C
1861
# F F updates to a new value
1862
# We need to emit an entry for 'foo', because D & E differed on the
1864
builder = self.get_builder()
1865
builder.build_snapshot(None,
1866
[('add', (u'', b'a-root-id', 'directory', None)),
1867
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1868
revision_id=b'A-id')
1869
builder.build_snapshot([b'A-id'],
1870
[('modify', ('foo', b'B content\n'))],
1871
revision_id=b'B-id')
1872
builder.build_snapshot([b'A-id'],
1873
[('modify', ('foo', b'C content\n'))],
1874
revision_id=b'C-id', )
1875
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1876
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1877
builder.build_snapshot([b'D-id'],
1878
[('modify', ('foo', b'F content\n'))],
1879
revision_id=b'F-id')
1880
merge_obj = self.make_merge_obj(builder, b'E-id')
1882
entries = list(merge_obj._entries_lca())
1883
root_id = b'a-root-id'
1884
self.assertEqual([(b'foo-id', True,
1885
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1886
((root_id, [root_id, root_id]), root_id, root_id),
1887
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1888
((False, [False, False]), False, False)),
1891
def test_same_lca_resolution_one_side_updates_content(self):
1892
# Both sides converge, but then one side updates the text.
1893
# A base, introduces 'foo'
1895
# B C B modifies 'foo', C modifies 'foo'
1897
# D E D and E use C's value
1899
# F F updates to a new value
1900
# I think it is a bug that this conflicts, but we don't have a way to
1901
# detect otherwise. And because of:
1902
# test_different_lca_resolve_one_side_updates_content
1903
# We need to conflict.
1905
builder = self.get_builder()
1906
builder.build_snapshot(None,
1907
[('add', (u'', b'a-root-id', 'directory', None)),
1908
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1909
revision_id=b'A-id')
1910
builder.build_snapshot([b'A-id'],
1911
[('modify', ('foo', b'B content\n'))],
1912
revision_id=b'B-id')
1913
builder.build_snapshot([b'A-id'],
1914
[('modify', ('foo', b'C content\n'))],
1915
revision_id=b'C-id')
1916
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1917
builder.build_snapshot([b'B-id', b'C-id'],
1918
[('modify', ('foo', b'C content\n'))],
1919
revision_id=b'D-id') # Same as E
1920
builder.build_snapshot([b'D-id'],
1921
[('modify', ('foo', b'F content\n'))],
1922
revision_id=b'F-id')
1923
merge_obj = self.make_merge_obj(builder, b'E-id')
1925
entries = list(merge_obj._entries_lca())
1926
self.expectFailure("We don't detect that LCA resolution was the"
1927
" same on both sides",
1928
self.assertEqual, [], entries)
1930
def test_only_path_changed(self):
1931
builder = self.get_builder()
1932
builder.build_snapshot(None,
1933
[('add', (u'', b'a-root-id', 'directory', None)),
1934
('add', (u'a', b'a-id', 'file', b'content\n'))],
1935
revision_id=b'A-id')
1936
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1937
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1938
builder.build_snapshot([b'C-id', b'B-id'],
1939
[('rename', (u'a', u'b'))],
1940
revision_id=b'E-id')
1941
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1942
merge_obj = self.make_merge_obj(builder, b'E-id')
1943
entries = list(merge_obj._entries_lca())
1944
root_id = b'a-root-id'
1945
# The content was not changed, only the path
1946
self.assertEqual([(b'a-id', False,
1947
((u'a', [u'a', u'a']), u'b', u'a'),
1948
((root_id, [root_id, root_id]), root_id, root_id),
1949
((u'a', [u'a', u'a']), u'b', u'a'),
1950
((False, [False, False]), False, False)),
1953
def test_kind_changed(self):
1954
# Identical content, except 'D' changes a-id into a directory
1955
builder = self.get_builder()
1956
builder.build_snapshot(None,
1957
[('add', (u'', b'a-root-id', 'directory', None)),
1958
('add', (u'a', b'a-id', 'file', b'content\n'))],
1959
revision_id=b'A-id')
1960
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1961
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1962
builder.build_snapshot([b'C-id', b'B-id'],
1963
[('unversion', 'a'),
1965
('add', (u'a', b'a-id', 'directory', None))],
1966
revision_id=b'E-id')
1967
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1968
merge_obj = self.make_merge_obj(builder, b'E-id')
1969
entries = list(merge_obj._entries_lca())
1970
root_id = b'a-root-id'
1971
# Only the kind was changed (content)
1972
self.assertEqual([(b'a-id', True,
1973
((u'a', [u'a', u'a']), u'a', u'a'),
1974
((root_id, [root_id, root_id]), root_id, root_id),
1975
((u'a', [u'a', u'a']), u'a', u'a'),
1976
((False, [False, False]), False, False)),
1979
def test_this_changed_kind(self):
1980
# Identical content, but THIS changes a file to a directory
1981
builder = self.get_builder()
1982
builder.build_snapshot(None,
1983
[('add', (u'', b'a-root-id', 'directory', None)),
1984
('add', (u'a', b'a-id', 'file', b'content\n'))],
1985
revision_id=b'A-id')
1986
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1987
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1988
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1989
builder.build_snapshot([b'B-id', b'C-id'],
1990
[('unversion', 'a'),
1992
('add', (u'a', b'a-id', 'directory', None))],
1993
revision_id=b'D-id')
1994
merge_obj = self.make_merge_obj(builder, b'E-id')
1995
entries = list(merge_obj._entries_lca())
1996
# Only the kind was changed (content)
1997
self.assertEqual([], entries)
1999
def test_interesting_files(self):
2000
# Two files modified, but we should filter one of them
2001
builder = self.get_builder()
2002
builder.build_snapshot(None,
2003
[('add', (u'', b'a-root-id', 'directory', None)),
2004
('add', (u'a', b'a-id', 'file', b'content\n')),
2005
('add', (u'b', b'b-id', 'file', b'content\n'))],
2006
revision_id=b'A-id')
2007
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2008
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2009
builder.build_snapshot([b'C-id', b'B-id'],
2010
[('modify', ('a', b'new-content\n')),
2011
('modify', ('b', b'new-content\n'))],
2012
revision_id=b'E-id')
2013
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2014
merge_obj = self.make_merge_obj(builder, b'E-id',
2015
interesting_files=['b'])
2016
entries = list(merge_obj._entries_lca())
2017
root_id = b'a-root-id'
2018
self.assertEqual([(b'b-id', True,
2019
((u'b', [u'b', u'b']), u'b', u'b'),
2020
((root_id, [root_id, root_id]), root_id, root_id),
2021
((u'b', [u'b', u'b']), u'b', u'b'),
2022
((False, [False, False]), False, False)),
2025
def test_interesting_file_in_this(self):
2026
# This renamed the file, but it should still match the entry in other
2027
builder = self.get_builder()
2028
builder.build_snapshot(None,
2029
[('add', (u'', b'a-root-id', 'directory', None)),
2030
('add', (u'a', b'a-id', 'file', b'content\n')),
2031
('add', (u'b', b'b-id', 'file', b'content\n'))],
2032
revision_id=b'A-id')
2033
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2034
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2035
builder.build_snapshot([b'C-id', b'B-id'],
2036
[('modify', ('a', b'new-content\n')),
2037
('modify', ('b', b'new-content\n'))],
2038
revision_id=b'E-id')
2039
builder.build_snapshot([b'B-id', b'C-id'],
2040
[('rename', ('b', 'c'))],
2041
revision_id=b'D-id')
2042
merge_obj = self.make_merge_obj(builder, b'E-id',
2043
interesting_files=['c'])
2044
entries = list(merge_obj._entries_lca())
2045
root_id = b'a-root-id'
2046
self.assertEqual([(b'b-id', True,
2047
((u'b', [u'b', u'b']), u'b', u'c'),
2048
((root_id, [root_id, root_id]), root_id, root_id),
2049
((u'b', [u'b', u'b']), u'b', u'c'),
2050
((False, [False, False]), False, False)),
2053
def test_interesting_file_in_base(self):
2054
# This renamed the file, but it should still match the entry in BASE
2055
builder = self.get_builder()
2056
builder.build_snapshot(None,
2057
[('add', (u'', b'a-root-id', 'directory', None)),
2058
('add', (u'a', b'a-id', 'file', b'content\n')),
2059
('add', (u'c', b'c-id', 'file', b'content\n'))],
2060
revision_id=b'A-id')
2061
builder.build_snapshot([b'A-id'],
2062
[('rename', ('c', 'b'))],
2063
revision_id=b'B-id')
2064
builder.build_snapshot([b'A-id'],
2065
[('rename', ('c', 'b'))],
2066
revision_id=b'C-id')
2067
builder.build_snapshot([b'C-id', b'B-id'],
2068
[('modify', ('a', b'new-content\n')),
2069
('modify', ('b', b'new-content\n'))],
2070
revision_id=b'E-id')
2071
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2072
merge_obj = self.make_merge_obj(builder, b'E-id',
2073
interesting_files=['c'])
2074
entries = list(merge_obj._entries_lca())
2075
root_id = b'a-root-id'
2076
self.assertEqual([(b'c-id', True,
2077
((u'c', [u'b', u'b']), u'b', u'b'),
2078
((root_id, [root_id, root_id]), root_id, root_id),
2079
((u'c', [u'b', u'b']), u'b', u'b'),
2080
((False, [False, False]), False, False)),
2083
def test_interesting_file_in_lca(self):
2084
# This renamed the file, but it should still match the entry in LCA
2085
builder = self.get_builder()
2086
builder.build_snapshot(None,
2087
[('add', (u'', b'a-root-id', 'directory', None)),
2088
('add', (u'a', b'a-id', 'file', b'content\n')),
2089
('add', (u'b', b'b-id', 'file', b'content\n'))],
2090
revision_id=b'A-id')
2091
builder.build_snapshot([b'A-id'],
2092
[('rename', ('b', 'c'))], revision_id=b'B-id')
2093
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2094
builder.build_snapshot([b'C-id', b'B-id'],
2095
[('modify', ('a', b'new-content\n')),
2096
('modify', ('b', b'new-content\n'))],
2097
revision_id=b'E-id')
2098
builder.build_snapshot([b'B-id', b'C-id'],
2099
[('rename', ('c', 'b'))], revision_id=b'D-id')
2100
merge_obj = self.make_merge_obj(builder, b'E-id',
2101
interesting_files=['c'])
2102
entries = list(merge_obj._entries_lca())
2103
root_id = b'a-root-id'
2104
self.assertEqual([(b'b-id', True,
2105
((u'b', [u'c', u'b']), u'b', u'b'),
2106
((root_id, [root_id, root_id]), root_id, root_id),
2107
((u'b', [u'c', u'b']), u'b', u'b'),
2108
((False, [False, False]), False, False)),
2111
def test_interesting_files(self):
2112
# Two files modified, but we should filter one of them
2113
builder = self.get_builder()
2114
builder.build_snapshot(None,
2115
[('add', (u'', b'a-root-id', 'directory', None)),
2116
('add', (u'a', b'a-id', 'file', b'content\n')),
2117
('add', (u'b', b'b-id', 'file', b'content\n'))],
2118
revision_id=b'A-id')
2119
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2120
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2121
builder.build_snapshot([b'C-id', b'B-id'],
2122
[('modify', ('a', b'new-content\n')),
2123
('modify', ('b', b'new-content\n'))], revision_id=b'E-id')
2124
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2125
merge_obj = self.make_merge_obj(builder, b'E-id',
2126
interesting_files=['b'])
2127
entries = list(merge_obj._entries_lca())
2128
root_id = b'a-root-id'
2129
self.assertEqual([(b'b-id', True,
2130
((u'b', [u'b', u'b']), u'b', u'b'),
2131
((root_id, [root_id, root_id]), root_id, root_id),
2132
((u'b', [u'b', u'b']), u'b', u'b'),
2133
((False, [False, False]), False, False)),
2137
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
2139
def get_builder(self):
2140
builder = self.make_branch_builder('path')
2141
builder.start_series()
2142
self.addCleanup(builder.finish_series)
2145
def get_wt_from_builder(self, builder):
2146
"""Get a real WorkingTree from the builder."""
2147
the_branch = builder.get_branch()
2148
wt = the_branch.controldir.create_workingtree()
2149
# Note: This is a little bit ugly, but we are holding the branch
2150
# write-locked as part of the build process, and we would like to
2151
# maintain that. So we just force the WT to re-use the same
2153
wt._branch = the_branch
2155
self.addCleanup(wt.unlock)
2158
def do_merge(self, builder, other_revision_id):
2159
wt = self.get_wt_from_builder(builder)
2160
merger = _mod_merge.Merger.from_revision_ids(
2161
wt, other_revision_id)
2162
merger.merge_type = _mod_merge.Merge3Merger
2163
return wt, merger.do_merge()
2165
def test_simple_lca(self):
2166
builder = self.get_builder()
2167
builder.build_snapshot(None,
2168
[('add', (u'', b'a-root-id', 'directory', None)),
2169
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
2170
revision_id=b'A-id')
2171
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2172
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2173
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2174
builder.build_snapshot([b'B-id', b'C-id'],
2175
[('modify', ('a', b'a\nb\nc\nd\ne\nf\n'))],
2176
revision_id=b'D-id')
2177
wt, conflicts = self.do_merge(builder, b'E-id')
2178
self.assertEqual(0, conflicts)
2179
# The merge should have simply update the contents of 'a'
2180
self.assertEqual(b'a\nb\nc\nd\ne\nf\n', wt.get_file_text('a'))
2182
def test_conflict_without_lca(self):
2183
# This test would cause a merge conflict, unless we use the lca trees
2184
# to determine the real ancestry
2187
# B C Path renamed to 'bar' in B
2191
# D E Path at 'bar' in D and E
2193
# F Path at 'baz' in F, which supersedes 'bar' and 'foo'
2194
builder = self.get_builder()
2195
builder.build_snapshot(None,
2196
[('add', (u'', b'a-root-id', 'directory', None)),
2197
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2198
revision_id=b'A-id')
2199
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2200
builder.build_snapshot([b'A-id'],
2201
[('rename', ('foo', 'bar'))], revision_id=b'B-id', )
2202
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2203
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2204
builder.build_snapshot([b'E-id'],
2205
[('rename', ('bar', 'baz'))], revision_id=b'F-id')
2206
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2207
wt, conflicts = self.do_merge(builder, b'F-id')
2208
self.assertEqual(0, conflicts)
2209
# The merge should simply recognize that the final rename takes
2211
self.assertEqual('baz', wt.id2path(b'foo-id'))
2213
def test_other_deletes_lca_renames(self):
2214
# This test would cause a merge conflict, unless we use the lca trees
2215
# to determine the real ancestry
2218
# B C Path renamed to 'bar' in B
2222
# D E Path at 'bar' in D and E
2225
builder = self.get_builder()
2226
builder.build_snapshot(None,
2227
[('add', (u'', b'a-root-id', 'directory', None)),
2228
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2229
revision_id=b'A-id')
2230
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2231
builder.build_snapshot([b'A-id'],
2232
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2233
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2234
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2235
builder.build_snapshot([b'E-id'],
2236
[('unversion', 'bar')], revision_id=b'F-id')
2237
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2238
wt, conflicts = self.do_merge(builder, b'F-id')
2239
self.assertEqual(0, conflicts)
2240
self.assertRaises(errors.NoSuchId, wt.id2path, b'foo-id')
2242
def test_executable_changes(self):
2251
# F Executable bit changed
2252
builder = self.get_builder()
2253
builder.build_snapshot(None,
2254
[('add', (u'', b'a-root-id', 'directory', None)),
2255
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2256
revision_id=b'A-id')
2257
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2258
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2259
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2260
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2261
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2262
wt = self.get_wt_from_builder(builder)
2263
with wt.get_transform() as tt:
2264
tt.set_executability(True, tt.trans_id_tree_path('foo'))
2266
self.assertTrue(wt.is_executable('foo'))
2267
wt.commit('F-id', rev_id=b'F-id')
2268
# Reset to D, so that we can merge F
2269
wt.set_parent_ids([b'D-id'])
2270
wt.branch.set_last_revision_info(3, b'D-id')
2272
self.assertFalse(wt.is_executable('foo'))
2273
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2274
self.assertEqual(0, conflicts)
2275
self.assertTrue(wt.is_executable('foo'))
2277
def test_create_symlink(self):
2278
self.requireFeature(features.SymlinkFeature)
2287
# F Add a symlink 'foo' => 'bar'
2288
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2289
# have symlink support
2290
builder = self.get_builder()
2291
builder.build_snapshot(None,
2292
[('add', (u'', b'a-root-id', 'directory', None))],
2293
revision_id=b'A-id')
2294
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2295
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2296
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2297
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2298
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2299
wt = self.get_wt_from_builder(builder)
2300
os.symlink('bar', 'path/foo')
2301
wt.add(['foo'], [b'foo-id'])
2302
self.assertEqual('bar', wt.get_symlink_target('foo'))
2303
wt.commit('add symlink', rev_id=b'F-id')
2304
# Reset to D, so that we can merge F
2305
wt.set_parent_ids([b'D-id'])
2306
wt.branch.set_last_revision_info(3, b'D-id')
2308
self.assertFalse(wt.is_versioned('foo'))
2309
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2310
self.assertEqual(0, conflicts)
2311
self.assertEqual(b'foo-id', wt.path2id('foo'))
2312
self.assertEqual('bar', wt.get_symlink_target('foo'))
2314
def test_both_sides_revert(self):
2315
# Both sides of a criss-cross revert the text to the lca
2316
# A base, introduces 'foo'
2318
# B C B modifies 'foo', C modifies 'foo'
2320
# D E D reverts to B, E reverts to C
2321
# This should conflict
2322
# This must be done with a real WorkingTree, because normally their
2323
# inventory contains "None" rather than a real sha1
2324
builder = self.get_builder()
2325
builder.build_snapshot(None,
2326
[('add', (u'', b'a-root-id', 'directory', None)),
2327
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
2328
revision_id=b'A-id')
2329
builder.build_snapshot([b'A-id'],
2330
[('modify', ('foo', b'B content\n'))],
2331
revision_id=b'B-id')
2332
builder.build_snapshot([b'A-id'],
2333
[('modify', ('foo', b'C content\n'))],
2334
revision_id=b'C-id')
2335
builder.build_snapshot([b'C-id', b'B-id'], [],
2336
revision_id=b'E-id')
2337
builder.build_snapshot([b'B-id', b'C-id'], [],
2338
revision_id=b'D-id')
2339
wt, conflicts = self.do_merge(builder, b'E-id')
2340
self.assertEqual(1, conflicts)
2341
self.assertEqualDiff(b'<<<<<<< TREE\n'
2345
b'>>>>>>> MERGE-SOURCE\n',
2346
wt.get_file_text('foo'))
2348
def test_modified_symlink(self):
2349
self.requireFeature(features.SymlinkFeature)
2350
# A Create symlink foo => bar
2352
# B C B relinks foo => baz
2356
# D E D & E have foo => baz
2358
# F F changes it to bing
2360
# Merging D & F should result in F cleanly overriding D, because D's
2361
# value actually comes from B
2363
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2364
# have symlink support
2365
wt = self.make_branch_and_tree('path')
2367
self.addCleanup(wt.unlock)
2368
os.symlink('bar', 'path/foo')
2369
wt.add(['foo'], [b'foo-id'])
2370
wt.commit('add symlink', rev_id=b'A-id')
2371
os.remove('path/foo')
2372
os.symlink('baz', 'path/foo')
2373
wt.commit('foo => baz', rev_id=b'B-id')
2374
wt.set_last_revision(b'A-id')
2375
wt.branch.set_last_revision_info(1, b'A-id')
2377
wt.commit('C', rev_id=b'C-id')
2378
wt.merge_from_branch(wt.branch, b'B-id')
2379
self.assertEqual('baz', wt.get_symlink_target('foo'))
2380
wt.commit('E merges C & B', rev_id=b'E-id')
2381
os.remove('path/foo')
2382
os.symlink('bing', 'path/foo')
2383
wt.commit('F foo => bing', rev_id=b'F-id')
2384
wt.set_last_revision(b'B-id')
2385
wt.branch.set_last_revision_info(2, b'B-id')
2387
wt.merge_from_branch(wt.branch, b'C-id')
2388
wt.commit('D merges B & C', rev_id=b'D-id')
2389
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2390
self.assertEqual(0, conflicts)
2391
self.assertEqual('bing', wt.get_symlink_target('foo'))
2393
def test_renamed_symlink(self):
2394
self.requireFeature(features.SymlinkFeature)
2395
# A Create symlink foo => bar
2397
# B C B renames foo => barry
2401
# D E D & E have barry
2403
# F F renames barry to blah
2405
# Merging D & F should result in F cleanly overriding D, because D's
2406
# value actually comes from B
2408
wt = self.make_branch_and_tree('path')
2410
self.addCleanup(wt.unlock)
2411
os.symlink('bar', 'path/foo')
2412
wt.add(['foo'], [b'foo-id'])
2413
wt.commit('A add symlink', rev_id=b'A-id')
2414
wt.rename_one('foo', 'barry')
2415
wt.commit('B foo => barry', rev_id=b'B-id')
2416
wt.set_last_revision(b'A-id')
2417
wt.branch.set_last_revision_info(1, b'A-id')
2419
wt.commit('C', rev_id=b'C-id')
2420
wt.merge_from_branch(wt.branch, b'B-id')
2421
self.assertEqual('barry', wt.id2path(b'foo-id'))
2422
self.assertEqual('bar', wt.get_symlink_target('barry'))
2423
wt.commit('E merges C & B', rev_id=b'E-id')
2424
wt.rename_one('barry', 'blah')
2425
wt.commit('F barry => blah', rev_id=b'F-id')
2426
wt.set_last_revision(b'B-id')
2427
wt.branch.set_last_revision_info(2, b'B-id')
2429
wt.merge_from_branch(wt.branch, b'C-id')
2430
wt.commit('D merges B & C', rev_id=b'D-id')
2431
self.assertEqual('barry', wt.id2path(b'foo-id'))
2432
# Check the output of the Merger object directly
2433
merger = _mod_merge.Merger.from_revision_ids(wt, b'F-id')
2434
merger.merge_type = _mod_merge.Merge3Merger
2435
merge_obj = merger.make_merger()
2436
root_id = wt.path2id('')
2437
entries = list(merge_obj._entries_lca())
2438
# No content change, just a path change
2439
self.assertEqual([(b'foo-id', False,
2440
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2441
((root_id, [root_id, root_id]), root_id, root_id),
2442
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2443
((False, [False, False]), False, False)),
2445
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2446
self.assertEqual(0, conflicts)
2447
self.assertEqual('blah', wt.id2path(b'foo-id'))
2449
def test_symlink_no_content_change(self):
2450
self.requireFeature(features.SymlinkFeature)
2451
# A Create symlink foo => bar
2453
# B C B relinks foo => baz
2457
# D E D & E have foo => baz
2459
# F F has foo => bing
2461
# Merging E into F should not cause a conflict, because E doesn't have
2462
# a content change relative to the LCAs (it does relative to A)
2463
wt = self.make_branch_and_tree('path')
2465
self.addCleanup(wt.unlock)
2466
os.symlink('bar', 'path/foo')
2467
wt.add(['foo'], [b'foo-id'])
2468
wt.commit('add symlink', rev_id=b'A-id')
2469
os.remove('path/foo')
2470
os.symlink('baz', 'path/foo')
2471
wt.commit('foo => baz', rev_id=b'B-id')
2472
wt.set_last_revision(b'A-id')
2473
wt.branch.set_last_revision_info(1, b'A-id')
2475
wt.commit('C', rev_id=b'C-id')
2476
wt.merge_from_branch(wt.branch, b'B-id')
2477
self.assertEqual('baz', wt.get_symlink_target('foo'))
2478
wt.commit('E merges C & B', rev_id=b'E-id')
2479
wt.set_last_revision(b'B-id')
2480
wt.branch.set_last_revision_info(2, b'B-id')
2482
wt.merge_from_branch(wt.branch, b'C-id')
2483
wt.commit('D merges B & C', rev_id=b'D-id')
2484
os.remove('path/foo')
2485
os.symlink('bing', 'path/foo')
2486
wt.commit('F foo => bing', rev_id=b'F-id')
2488
# Check the output of the Merger object directly
2489
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2490
merger.merge_type = _mod_merge.Merge3Merger
2491
merge_obj = merger.make_merger()
2492
# Nothing interesting happened in OTHER relative to BASE
2493
self.assertEqual([], list(merge_obj._entries_lca()))
2494
# Now do a real merge, just to test the rest of the stack
2495
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'E-id')
2496
self.assertEqual(0, conflicts)
2497
self.assertEqual('bing', wt.get_symlink_target('foo'))
2499
def test_symlink_this_changed_kind(self):
2500
self.requireFeature(features.SymlinkFeature)
2503
# B C B creates symlink foo => bar
2507
# D E D changes foo into a file, E has foo => bing
2509
# Mostly, this is trying to test that we don't try to os.readlink() on
2510
# a file, or when there is nothing there
2511
wt = self.make_branch_and_tree('path')
2513
self.addCleanup(wt.unlock)
2514
wt.commit('base', rev_id=b'A-id')
2515
os.symlink('bar', 'path/foo')
2516
wt.add(['foo'], [b'foo-id'])
2517
wt.commit('add symlink foo => bar', rev_id=b'B-id')
2518
wt.set_last_revision(b'A-id')
2519
wt.branch.set_last_revision_info(1, b'A-id')
2521
wt.commit('C', rev_id=b'C-id')
2522
wt.merge_from_branch(wt.branch, b'B-id')
2523
self.assertEqual('bar', wt.get_symlink_target('foo'))
2524
os.remove('path/foo')
2525
# We have to change the link in E, or it won't try to do a comparison
2526
os.symlink('bing', 'path/foo')
2527
wt.commit('E merges C & B, overrides to bing', rev_id=b'E-id')
2528
wt.set_last_revision(b'B-id')
2529
wt.branch.set_last_revision_info(2, b'B-id')
2531
wt.merge_from_branch(wt.branch, b'C-id')
2532
os.remove('path/foo')
2533
self.build_tree_contents([('path/foo', b'file content\n')])
2534
# XXX: workaround, WT doesn't detect kind changes unless you do
2536
list(wt.iter_changes(wt.basis_tree()))
2537
wt.commit('D merges B & C, makes it a file', rev_id=b'D-id')
2539
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2540
merger.merge_type = _mod_merge.Merge3Merger
2541
merge_obj = merger.make_merger()
2542
entries = list(merge_obj._entries_lca())
2543
root_id = wt.path2id('')
2544
self.assertEqual([(b'foo-id', True,
2545
((None, [u'foo', None]), u'foo', u'foo'),
2546
((None, [root_id, None]), root_id, root_id),
2547
((None, [u'foo', None]), u'foo', u'foo'),
2548
((None, [False, None]), False, False)),
2551
def test_symlink_all_wt(self):
2552
"""Check behavior if all trees are Working Trees."""
2553
self.requireFeature(features.SymlinkFeature)
2554
# The big issue is that entry.symlink_target is None for WorkingTrees.
2555
# So we need to make sure we handle that case correctly.
2558
# B C B relinks foo => baz
2560
# D E D & E have foo => baz
2562
# F F changes it to bing
2563
# Merging D & F should result in F cleanly overriding D, because D's
2564
# value actually comes from B
2566
wt = self.make_branch_and_tree('path')
2568
self.addCleanup(wt.unlock)
2569
os.symlink('bar', 'path/foo')
2570
wt.add(['foo'], [b'foo-id'])
2571
wt.commit('add symlink', rev_id=b'A-id')
2572
os.remove('path/foo')
2573
os.symlink('baz', 'path/foo')
2574
wt.commit('foo => baz', rev_id=b'B-id')
2575
wt.set_last_revision(b'A-id')
2576
wt.branch.set_last_revision_info(1, b'A-id')
2578
wt.commit('C', rev_id=b'C-id')
2579
wt.merge_from_branch(wt.branch, b'B-id')
2580
self.assertEqual('baz', wt.get_symlink_target('foo'))
2581
wt.commit('E merges C & B', rev_id=b'E-id')
2582
os.remove('path/foo')
2583
os.symlink('bing', 'path/foo')
2584
wt.commit('F foo => bing', rev_id=b'F-id')
2585
wt.set_last_revision(b'B-id')
2586
wt.branch.set_last_revision_info(2, b'B-id')
2588
wt.merge_from_branch(wt.branch, b'C-id')
2589
wt.commit('D merges B & C', rev_id=b'D-id')
2590
wt_base = wt.controldir.sprout('base', b'A-id').open_workingtree()
2592
self.addCleanup(wt_base.unlock)
2593
wt_lca1 = wt.controldir.sprout('b-tree', b'B-id').open_workingtree()
2595
self.addCleanup(wt_lca1.unlock)
2596
wt_lca2 = wt.controldir.sprout('c-tree', b'C-id').open_workingtree()
2598
self.addCleanup(wt_lca2.unlock)
2599
wt_other = wt.controldir.sprout('other', b'F-id').open_workingtree()
2600
wt_other.lock_read()
2601
self.addCleanup(wt_other.unlock)
2602
merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
2603
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2604
entries = list(merge_obj._entries_lca())
2605
root_id = wt.path2id('')
2606
self.assertEqual([(b'foo-id', True,
2607
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2608
((root_id, [root_id, root_id]), root_id, root_id),
2609
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2610
((False, [False, False]), False, False)),
2613
def test_other_reverted_path_to_base(self):
2616
# B C Path at 'bar' in B
2623
builder = self.get_builder()
2624
builder.build_snapshot(None,
2625
[('add', (u'', b'a-root-id', 'directory', None)),
2626
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2627
revision_id=b'A-id')
2628
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2629
builder.build_snapshot([b'A-id'],
2630
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2631
builder.build_snapshot([b'C-id', b'B-id'],
2632
[('rename', ('foo', 'bar'))], revision_id=b'E-id') # merge the rename
2633
builder.build_snapshot([b'E-id'],
2634
[('rename', ('bar', 'foo'))], revision_id=b'F-id') # Rename back to BASE
2635
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2636
wt, conflicts = self.do_merge(builder, b'F-id')
2637
self.assertEqual(0, conflicts)
2638
self.assertEqual('foo', wt.id2path(b'foo-id'))
2640
def test_other_reverted_content_to_base(self):
2641
builder = self.get_builder()
2642
builder.build_snapshot(None,
2643
[('add', (u'', b'a-root-id', 'directory', None)),
2644
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2645
revision_id=b'A-id')
2646
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2647
builder.build_snapshot([b'A-id'],
2648
[('modify', ('foo', b'B content\n'))],
2649
revision_id=b'B-id')
2650
builder.build_snapshot([b'C-id', b'B-id'],
2651
[('modify', ('foo', b'B content\n'))],
2652
revision_id=b'E-id') # merge the content
2653
builder.build_snapshot([b'E-id'],
2654
[('modify', ('foo', b'base content\n'))],
2655
revision_id=b'F-id') # Revert back to BASE
2656
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2657
wt, conflicts = self.do_merge(builder, b'F-id')
2658
self.assertEqual(0, conflicts)
2659
# TODO: We need to use the per-file graph to properly select a BASE
2660
# before this will work. Or at least use the LCA trees to find
2661
# the appropriate content base. (which is B, not A).
2662
self.assertEqual(b'base content\n', wt.get_file_text('foo'))
2664
def test_other_modified_content(self):
2665
builder = self.get_builder()
2666
builder.build_snapshot(None,
2667
[('add', (u'', b'a-root-id', 'directory', None)),
2668
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2669
revision_id=b'A-id')
2670
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2671
builder.build_snapshot([b'A-id'],
2672
[('modify', ('foo', b'B content\n'))],
2673
revision_id=b'B-id')
2674
builder.build_snapshot([b'C-id', b'B-id'],
2675
[('modify', ('foo', b'B content\n'))],
2676
revision_id=b'E-id') # merge the content
2677
builder.build_snapshot([b'E-id'],
2678
[('modify', ('foo', b'F content\n'))],
2679
revision_id=b'F-id') # Override B content
2680
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2681
wt, conflicts = self.do_merge(builder, b'F-id')
2682
self.assertEqual(0, conflicts)
2683
self.assertEqual(b'F content\n', wt.get_file_text('foo'))
2685
def test_all_wt(self):
2686
"""Check behavior if all trees are Working Trees."""
2687
# The big issue is that entry.revision is None for WorkingTrees. (as is
2688
# entry.text_sha1, etc. So we need to make sure we handle that case
2690
# A Content of 'foo', path of 'a'
2692
# B C B modifies content, C renames 'a' => 'b'
2694
# D E E updates content, renames 'b' => 'c'
2695
builder = self.get_builder()
2696
builder.build_snapshot(None,
2697
[('add', (u'', b'a-root-id', 'directory', None)),
2698
('add', (u'a', b'a-id', 'file', b'base content\n')),
2699
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2700
revision_id=b'A-id')
2701
builder.build_snapshot([b'A-id'],
2702
[('modify', ('foo', b'B content\n'))],
2703
revision_id=b'B-id')
2704
builder.build_snapshot([b'A-id'],
2705
[('rename', ('a', 'b'))],
2706
revision_id=b'C-id')
2707
builder.build_snapshot([b'C-id', b'B-id'],
2708
[('rename', ('b', 'c')),
2709
('modify', ('foo', b'E content\n'))],
2710
revision_id=b'E-id')
2711
builder.build_snapshot([b'B-id', b'C-id'],
2712
[('rename', ('a', 'b'))], revision_id=b'D-id') # merged change
2713
wt_this = self.get_wt_from_builder(builder)
2714
wt_base = wt_this.controldir.sprout('base', b'A-id').open_workingtree()
2716
self.addCleanup(wt_base.unlock)
2717
wt_lca1 = wt_this.controldir.sprout(
2718
'b-tree', b'B-id').open_workingtree()
2720
self.addCleanup(wt_lca1.unlock)
2721
wt_lca2 = wt_this.controldir.sprout(
2722
'c-tree', b'C-id').open_workingtree()
2724
self.addCleanup(wt_lca2.unlock)
2725
wt_other = wt_this.controldir.sprout(
2726
'other', b'E-id').open_workingtree()
2727
wt_other.lock_read()
2728
self.addCleanup(wt_other.unlock)
2729
merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
2730
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2731
entries = list(merge_obj._entries_lca())
2732
root_id = b'a-root-id'
2733
self.assertEqual([(b'a-id', False,
2734
((u'a', [u'a', u'b']), u'c', u'b'),
2735
((root_id, [root_id, root_id]), root_id, root_id),
2736
((u'a', [u'a', u'b']), u'c', u'b'),
2737
((False, [False, False]), False, False)),
2739
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2740
((root_id, [root_id, root_id]), root_id, root_id),
2741
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2742
((False, [False, False]), False, False)),
2745
def test_nested_tree_unmodified(self):
2746
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2748
wt = self.make_branch_and_tree('tree',
2749
format='development-subtree')
2751
self.addCleanup(wt.unlock)
2752
sub_tree = self.make_branch_and_tree('tree/sub-tree',
2753
format='development-subtree')
2754
wt.set_root_id(b'a-root-id')
2755
sub_tree.set_root_id(b'sub-tree-root')
2756
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
2757
sub_tree.add('file')
2758
sub_tree.commit('foo', rev_id=b'sub-A-id')
2759
wt.add_reference(sub_tree)
2760
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2761
# Now create a criss-cross merge in the parent, without modifying the
2763
wt.commit('B', rev_id=b'B-id', recursive=None)
2764
wt.set_last_revision(b'A-id')
2765
wt.branch.set_last_revision_info(1, b'A-id')
2766
wt.commit('C', rev_id=b'C-id', recursive=None)
2767
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2768
wt.commit('E', rev_id=b'E-id', recursive=None)
2769
wt.set_parent_ids([b'B-id', b'C-id'])
2770
wt.branch.set_last_revision_info(2, b'B-id')
2771
wt.commit('D', rev_id=b'D-id', recursive=None)
2773
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2774
merger.merge_type = _mod_merge.Merge3Merger
2775
merge_obj = merger.make_merger()
2776
entries = list(merge_obj._entries_lca())
2777
self.assertEqual([], entries)
2779
def test_nested_tree_subtree_modified(self):
2780
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2782
wt = self.make_branch_and_tree('tree',
2783
format='development-subtree')
2785
self.addCleanup(wt.unlock)
2786
sub_tree = self.make_branch_and_tree('tree/sub',
2787
format='development-subtree')
2788
wt.set_root_id(b'a-root-id')
2789
sub_tree.set_root_id(b'sub-tree-root')
2790
self.build_tree_contents([('tree/sub/file', b'text1')])
2791
sub_tree.add('file')
2792
sub_tree.commit('foo', rev_id=b'sub-A-id')
2793
wt.add_reference(sub_tree)
2794
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2795
# Now create a criss-cross merge in the parent, without modifying the
2797
wt.commit('B', rev_id=b'B-id', recursive=None)
2798
wt.set_last_revision(b'A-id')
2799
wt.branch.set_last_revision_info(1, b'A-id')
2800
wt.commit('C', rev_id=b'C-id', recursive=None)
2801
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2802
self.build_tree_contents([('tree/sub/file', b'text2')])
2803
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2804
wt.commit('E', rev_id=b'E-id', recursive=None)
2805
wt.set_parent_ids([b'B-id', b'C-id'])
2806
wt.branch.set_last_revision_info(2, b'B-id')
2807
wt.commit('D', rev_id=b'D-id', recursive=None)
2809
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2810
merger.merge_type = _mod_merge.Merge3Merger
2811
merge_obj = merger.make_merger()
2812
entries = list(merge_obj._entries_lca())
2813
# Nothing interesting about this sub-tree, because content changes are
2814
# computed at a higher level
2815
self.assertEqual([], entries)
2817
def test_nested_tree_subtree_renamed(self):
2818
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2820
wt = self.make_branch_and_tree('tree',
2821
format='development-subtree')
2823
self.addCleanup(wt.unlock)
2824
sub_tree = self.make_branch_and_tree('tree/sub',
2825
format='development-subtree')
2826
wt.set_root_id(b'a-root-id')
2827
sub_tree.set_root_id(b'sub-tree-root')
2828
self.build_tree_contents([('tree/sub/file', b'text1')])
2829
sub_tree.add('file')
2830
sub_tree.commit('foo', rev_id=b'sub-A-id')
2831
wt.add_reference(sub_tree)
2832
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2833
# Now create a criss-cross merge in the parent, without modifying the
2835
wt.commit('B', rev_id=b'B-id', recursive=None)
2836
wt.set_last_revision(b'A-id')
2837
wt.branch.set_last_revision_info(1, b'A-id')
2838
wt.commit('C', rev_id=b'C-id', recursive=None)
2839
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2840
wt.rename_one('sub', 'alt_sub')
2841
wt.commit('E', rev_id=b'E-id', recursive=None)
2842
wt.set_last_revision(b'B-id')
2844
wt.set_parent_ids([b'B-id', b'C-id'])
2845
wt.branch.set_last_revision_info(2, b'B-id')
2846
wt.commit('D', rev_id=b'D-id', recursive=None)
2848
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2849
merger.merge_type = _mod_merge.Merge3Merger
2850
merge_obj = merger.make_merger()
2851
entries = list(merge_obj._entries_lca())
2852
root_id = b'a-root-id'
2853
self.assertEqual([(b'sub-tree-root', False,
2854
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2855
((root_id, [root_id, root_id]), root_id, root_id),
2856
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2857
((False, [False, False]), False, False)),
2860
def test_nested_tree_subtree_renamed_and_modified(self):
2861
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2863
wt = self.make_branch_and_tree('tree',
2864
format='development-subtree')
2866
self.addCleanup(wt.unlock)
2867
sub_tree = self.make_branch_and_tree('tree/sub',
2868
format='development-subtree')
2869
wt.set_root_id(b'a-root-id')
2870
sub_tree.set_root_id(b'sub-tree-root')
2871
self.build_tree_contents([('tree/sub/file', b'text1')])
2872
sub_tree.add('file')
2873
sub_tree.commit('foo', rev_id=b'sub-A-id')
2874
wt.add_reference(sub_tree)
2875
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2876
# Now create a criss-cross merge in the parent, without modifying the
2878
wt.commit('B', rev_id=b'B-id', recursive=None)
2879
wt.set_last_revision(b'A-id')
2880
wt.branch.set_last_revision_info(1, b'A-id')
2881
wt.commit('C', rev_id=b'C-id', recursive=None)
2882
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2883
self.build_tree_contents([('tree/sub/file', b'text2')])
2884
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2885
wt.rename_one('sub', 'alt_sub')
2886
wt.commit('E', rev_id=b'E-id', recursive=None)
2887
wt.set_last_revision(b'B-id')
2889
wt.set_parent_ids([b'B-id', b'C-id'])
2890
wt.branch.set_last_revision_info(2, b'B-id')
2891
wt.commit('D', rev_id=b'D-id', recursive=None)
2893
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2894
merger.merge_type = _mod_merge.Merge3Merger
2895
merge_obj = merger.make_merger()
2896
entries = list(merge_obj._entries_lca())
2897
root_id = b'a-root-id'
2898
self.assertEqual([(b'sub-tree-root', False,
2899
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2900
((root_id, [root_id, root_id]), root_id, root_id),
2901
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2902
((False, [False, False]), False, False)),
2906
class TestLCAMultiWay(tests.TestCase):
2908
def assertLCAMultiWay(self, expected, base, lcas, other, this,
2909
allow_overriding_lca=True):
2910
self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
2911
(base, lcas), other, this,
2912
allow_overriding_lca=allow_overriding_lca))
2914
def test_other_equal_equal_lcas(self):
2915
"""Test when OTHER=LCA and all LCAs are identical."""
2916
self.assertLCAMultiWay('this',
2917
'bval', ['bval', 'bval'], 'bval', 'bval')
2918
self.assertLCAMultiWay('this',
2919
'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
2920
self.assertLCAMultiWay('this',
2921
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
2922
self.assertLCAMultiWay('this',
2923
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
2924
self.assertLCAMultiWay('this',
2925
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
2927
def test_other_equal_this(self):
2928
"""Test when other and this are identical."""
2929
self.assertLCAMultiWay('this',
2930
'bval', ['bval', 'bval'], 'oval', 'oval')
2931
self.assertLCAMultiWay('this',
2932
'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
2933
self.assertLCAMultiWay('this',
2934
'bval', ['cval', 'dval'], 'oval', 'oval')
2935
self.assertLCAMultiWay('this',
2936
'bval', [None, 'lcaval'], 'oval', 'oval')
2937
self.assertLCAMultiWay('this',
2938
None, [None, 'lcaval'], 'oval', 'oval')
2939
self.assertLCAMultiWay('this',
2940
None, ['lcaval', 'lcaval'], 'oval', 'oval')
2941
self.assertLCAMultiWay('this',
2942
None, ['cval', 'dval'], 'oval', 'oval')
2943
self.assertLCAMultiWay('this',
2944
None, ['cval', 'dval'], None, None)
2945
self.assertLCAMultiWay('this',
2946
None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
2948
def test_no_lcas(self):
2949
self.assertLCAMultiWay('this',
2950
'bval', [], 'bval', 'tval')
2951
self.assertLCAMultiWay('other',
2952
'bval', [], 'oval', 'bval')
2953
self.assertLCAMultiWay('conflict',
2954
'bval', [], 'oval', 'tval')
2955
self.assertLCAMultiWay('this',
2956
'bval', [], 'oval', 'oval')
2958
def test_lca_supersedes_other_lca(self):
2959
"""If one lca == base, the other lca takes precedence"""
2960
self.assertLCAMultiWay('this',
2961
'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
2962
self.assertLCAMultiWay('this',
2963
'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
2964
# This is actually considered a 'revert' because the 'lcaval' in LCAS
2965
# supersedes the BASE val (in the other LCA) but then OTHER reverts it
2967
self.assertLCAMultiWay('other',
2968
'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
2969
self.assertLCAMultiWay('conflict',
2970
'bval', ['bval', 'lcaval'], 'bval', 'tval')
2972
def test_other_and_this_pick_different_lca(self):
2973
# OTHER and THIS resolve the lca conflict in different ways
2974
self.assertLCAMultiWay('conflict',
2975
'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
2976
self.assertLCAMultiWay('conflict',
2977
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
2978
self.assertLCAMultiWay('conflict',
2979
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
2981
def test_other_in_lca(self):
2982
# OTHER takes a value of one of the LCAs, THIS takes a new value, which
2983
# theoretically supersedes both LCA values and 'wins'
2984
self.assertLCAMultiWay(
2985
'this', 'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
2986
self.assertLCAMultiWay(
2987
'this', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val',
2989
self.assertLCAMultiWay('conflict',
2991
'lca2val'], 'lca1val', 'newval',
2992
allow_overriding_lca=False)
2993
self.assertLCAMultiWay('conflict',
2994
'bval', ['lca1val', 'lca2val',
2995
'lca3val'], 'lca1val', 'newval',
2996
allow_overriding_lca=False)
2997
# THIS reverted back to BASE, but that is an explicit supersede of all
2999
self.assertLCAMultiWay(
3000
'this', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val',
3002
self.assertLCAMultiWay(
3003
'this', 'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
3004
self.assertLCAMultiWay('conflict',
3005
'bval', ['lca1val', 'lca2val',
3006
'lca3val'], 'lca1val', 'bval',
3007
allow_overriding_lca=False)
3008
self.assertLCAMultiWay('conflict',
3009
'bval', ['lca1val', 'lca2val',
3010
'bval'], 'lca1val', 'bval',
3011
allow_overriding_lca=False)
3013
def test_this_in_lca(self):
3014
# THIS takes a value of one of the LCAs, OTHER takes a new value, which
3015
# theoretically supersedes both LCA values and 'wins'
3016
self.assertLCAMultiWay(
3017
'other', 'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
3018
self.assertLCAMultiWay(
3019
'other', 'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
3020
self.assertLCAMultiWay('conflict',
3022
'lca2val'], 'oval', 'lca1val',
3023
allow_overriding_lca=False)
3024
self.assertLCAMultiWay('conflict',
3026
'lca2val'], 'oval', 'lca2val',
3027
allow_overriding_lca=False)
3028
# OTHER reverted back to BASE, but that is an explicit supersede of all
3030
self.assertLCAMultiWay(
3031
'other', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval',
3033
self.assertLCAMultiWay(
3034
'conflict', 'bval', ['lca1val', 'lca2val', 'lca3val'],
3035
'bval', 'lca3val', allow_overriding_lca=False)
3037
def test_all_differ(self):
3038
self.assertLCAMultiWay(
3039
'conflict', 'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
3040
self.assertLCAMultiWay(
3041
'conflict', 'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval',
3043
self.assertLCAMultiWay(
3044
'conflict', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval',
3048
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
3051
super(TestConfigurableFileMerger, self).setUp()
3054
def get_merger_factory(self):
3055
# Allows the inner methods to access the test attributes
3058
class FooMerger(_mod_merge.ConfigurableFileMerger):
3060
default_files = ['bar']
3062
def merge_text(self, params):
3063
calls.append('merge_text')
3064
return ('not_applicable', None)
3066
def factory(merger):
3067
result = FooMerger(merger)
3068
# Make sure we start with a clean slate
3069
self.assertEqual(None, result.affected_files)
3070
# Track the original merger
3071
self.merger = result
3076
def _install_hook(self, factory):
3077
_mod_merge.Merger.hooks.install_named_hook('merge_file_content',
3078
factory, 'test factory')
3080
def make_builder(self):
3081
builder = test_merge_core.MergeBuilder(self.test_base_dir)
3082
self.addCleanup(builder.cleanup)
3085
def make_text_conflict(self, file_name='bar'):
3086
factory = self.get_merger_factory()
3087
self._install_hook(factory)
3088
builder = self.make_builder()
3089
builder.add_file(b'bar-id', builder.tree_root,
3090
file_name, b'text1', True)
3091
builder.change_contents(b'bar-id', other=b'text4', this=b'text3')
3094
def make_kind_change(self):
3095
factory = self.get_merger_factory()
3096
self._install_hook(factory)
3097
builder = self.make_builder()
3098
builder.add_file(b'bar-id', builder.tree_root, 'bar', b'text1', True,
3100
builder.add_dir(b'bar-dir', builder.tree_root, 'bar-id',
3101
base=False, other=False)
3104
def test_uses_this_branch(self):
3105
builder = self.make_text_conflict()
3106
with builder.make_preview_transform() as tt:
3109
def test_affected_files_cached(self):
3110
"""Ensures that the config variable is cached"""
3111
builder = self.make_text_conflict()
3112
conflicts = builder.merge()
3113
# The hook should set the variable
3114
self.assertEqual(['bar'], self.merger.affected_files)
3115
self.assertEqual(1, len(conflicts))
3117
def test_hook_called_for_text_conflicts(self):
3118
builder = self.make_text_conflict()
3120
# The hook should call the merge_text() method
3121
self.assertEqual(['merge_text'], self.calls)
3123
def test_hook_not_called_for_kind_change(self):
3124
builder = self.make_kind_change()
3126
# The hook should not call the merge_text() method
3127
self.assertEqual([], self.calls)
3129
def test_hook_not_called_for_other_files(self):
3130
builder = self.make_text_conflict('foobar')
3132
# The hook should not call the merge_text() method
3133
self.assertEqual([], self.calls)
3136
class TestMergeIntoBase(tests.TestCaseWithTransport):
3138
def setup_simple_branch(self, relpath, shape=None, root_id=None):
3139
"""One commit, containing tree specified by optional shape.
3141
Default is empty tree (just root entry).
3144
root_id = b'%s-root-id' % (relpath.encode('ascii'),)
3145
wt = self.make_branch_and_tree(relpath)
3146
wt.set_root_id(root_id)
3147
if shape is not None:
3148
adjusted_shape = [relpath + '/' + elem for elem in shape]
3149
self.build_tree(adjusted_shape)
3151
(b'%s-%s-id' % (relpath.encode('utf-8'),
3152
basename(elem.rstrip('/')).encode('ascii')))
3154
wt.add(shape, ids=ids)
3155
rev_id = b'r1-%s' % (relpath.encode('utf-8'),)
3156
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
3157
self.assertEqual(root_id, wt.path2id(''))
3160
def setup_two_branches(self, custom_root_ids=True):
3161
"""Setup 2 branches, one will be a library, the other a project."""
3165
root_id = inventory.ROOT_ID
3166
project_wt = self.setup_simple_branch(
3167
'project', ['README', 'dir/', 'dir/file.c'],
3169
lib_wt = self.setup_simple_branch(
3170
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3172
return project_wt, lib_wt
3174
def do_merge_into(self, location, merge_as):
3175
"""Helper for using MergeIntoMerger.
3177
:param location: location of directory to merge from, either the
3178
location of a branch or of a path inside a branch.
3179
:param merge_as: the path in a tree to add the new directory as.
3180
:returns: the conflicts from 'do_merge'.
3182
with cleanup.ExitStack() as stack:
3183
# Open and lock the various tree and branch objects
3184
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3185
stack.enter_context(wt.lock_write())
3186
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3188
stack.enter_context(branch_to_merge.lock_read())
3189
other_tree = branch_to_merge.basis_tree()
3190
stack.enter_context(other_tree.lock_read())
3192
merger = _mod_merge.MergeIntoMerger(
3193
this_tree=wt, other_tree=other_tree, other_branch=branch_to_merge,
3194
target_subdir=subdir_relpath, source_subpath=subdir_to_merge)
3195
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3196
conflicts = merger.do_merge()
3197
merger.set_pending()
3200
def assertTreeEntriesEqual(self, expected_entries, tree):
3201
"""Assert that 'tree' contains the expected inventory entries.
3203
:param expected_entries: sequence of (path, file-id) pairs.
3205
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3206
self.assertEqual(expected_entries, files)
3209
class TestMergeInto(TestMergeIntoBase):
3211
def test_newdir_with_unique_roots(self):
3212
"""Merge a branch with a unique root into a new directory."""
3213
project_wt, lib_wt = self.setup_two_branches()
3214
self.do_merge_into('lib1', 'project/lib1')
3215
project_wt.lock_read()
3216
self.addCleanup(project_wt.unlock)
3217
# The r1-lib1 revision should be merged into this one
3218
self.assertEqual([b'r1-project', b'r1-lib1'],
3219
project_wt.get_parent_ids())
3220
self.assertTreeEntriesEqual(
3221
[('', b'project-root-id'),
3222
('README', b'project-README-id'),
3223
('dir', b'project-dir-id'),
3224
('lib1', b'lib1-root-id'),
3225
('dir/file.c', b'project-file.c-id'),
3226
('lib1/Makefile', b'lib1-Makefile-id'),
3227
('lib1/README', b'lib1-README-id'),
3228
('lib1/foo.c', b'lib1-foo.c-id'),
3231
def test_subdir(self):
3232
"""Merge a branch into a subdirectory of an existing directory."""
3233
project_wt, lib_wt = self.setup_two_branches()
3234
self.do_merge_into('lib1', 'project/dir/lib1')
3235
project_wt.lock_read()
3236
self.addCleanup(project_wt.unlock)
3237
# The r1-lib1 revision should be merged into this one
3238
self.assertEqual([b'r1-project', b'r1-lib1'],
3239
project_wt.get_parent_ids())
3240
self.assertTreeEntriesEqual(
3241
[('', b'project-root-id'),
3242
('README', b'project-README-id'),
3243
('dir', b'project-dir-id'),
3244
('dir/file.c', b'project-file.c-id'),
3245
('dir/lib1', b'lib1-root-id'),
3246
('dir/lib1/Makefile', b'lib1-Makefile-id'),
3247
('dir/lib1/README', b'lib1-README-id'),
3248
('dir/lib1/foo.c', b'lib1-foo.c-id'),
3251
def test_newdir_with_repeat_roots(self):
3252
"""If the file-id of the dir to be merged already exists a new ID will
3253
be allocated to let the merge happen.
3255
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3256
root_id = project_wt.path2id('')
3257
self.do_merge_into('lib1', 'project/lib1')
3258
project_wt.lock_read()
3259
self.addCleanup(project_wt.unlock)
3260
# The r1-lib1 revision should be merged into this one
3261
self.assertEqual([b'r1-project', b'r1-lib1'],
3262
project_wt.get_parent_ids())
3263
new_lib1_id = project_wt.path2id('lib1')
3264
self.assertNotEqual(None, new_lib1_id)
3265
self.assertTreeEntriesEqual(
3267
('README', b'project-README-id'),
3268
('dir', b'project-dir-id'),
3269
('lib1', new_lib1_id),
3270
('dir/file.c', b'project-file.c-id'),
3271
('lib1/Makefile', b'lib1-Makefile-id'),
3272
('lib1/README', b'lib1-README-id'),
3273
('lib1/foo.c', b'lib1-foo.c-id'),
3276
def test_name_conflict(self):
3277
"""When the target directory name already exists a conflict is
3278
generated and the original directory is renamed to foo.moved.
3280
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3281
self.setup_simple_branch('src', ['README'])
3282
conflicts = self.do_merge_into('src', 'dest/dir')
3283
self.assertEqual(1, conflicts)
3285
self.addCleanup(dest_wt.unlock)
3286
# The r1-lib1 revision should be merged into this one
3287
self.assertEqual([b'r1-dest', b'r1-src'], dest_wt.get_parent_ids())
3288
self.assertTreeEntriesEqual(
3289
[('', b'dest-root-id'),
3290
('dir', b'src-root-id'),
3291
('dir.moved', b'dest-dir-id'),
3292
('dir/README', b'src-README-id'),
3293
('dir.moved/file.txt', b'dest-file.txt-id'),
3296
def test_file_id_conflict(self):
3297
"""A conflict is generated if the merge-into adds a file (or other
3298
inventory entry) with a file-id that already exists in the target tree.
3300
self.setup_simple_branch('dest', ['file.txt'])
3301
# Make a second tree with a file-id that will clash with file.txt in
3303
src_wt = self.make_branch_and_tree('src')
3304
self.build_tree(['src/README'])
3305
src_wt.add(['README'], ids=[b'dest-file.txt-id'])
3306
src_wt.commit("Rev 1 of src.", rev_id=b'r1-src')
3307
conflicts = self.do_merge_into('src', 'dest/dir')
3308
# This is an edge case that shouldn't happen to users very often. So
3309
# we don't care really about the exact presentation of the conflict,
3310
# just that there is one.
3311
self.assertEqual(1, conflicts)
3313
def test_only_subdir(self):
3314
"""When the location points to just part of a tree, merge just that
3317
dest_wt = self.setup_simple_branch('dest')
3318
self.setup_simple_branch('src', ['hello.txt', 'dir/', 'dir/foo.c'])
3319
self.do_merge_into('src/dir', 'dest/dir')
3321
self.addCleanup(dest_wt.unlock)
3322
# The r1-lib1 revision should NOT be merged into this one (this is a
3324
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3325
self.assertTreeEntriesEqual(
3326
[('', b'dest-root-id'),
3327
('dir', b'src-dir-id'),
3328
('dir/foo.c', b'src-foo.c-id'),
3331
def test_only_file(self):
3332
"""An edge case: merge just one file, not a whole dir."""
3333
dest_wt = self.setup_simple_branch('dest')
3334
self.setup_simple_branch('two-file', ['file1.txt', 'file2.txt'])
3335
self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3337
self.addCleanup(dest_wt.unlock)
3338
# The r1-lib1 revision should NOT be merged into this one
3339
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3340
self.assertTreeEntriesEqual(
3341
[('', b'dest-root-id'), ('file1.txt', b'two-file-file1.txt-id')],
3344
def test_no_such_source_path(self):
3345
"""PathNotInTree is raised if the specified path in the source tree
3348
dest_wt = self.setup_simple_branch('dest')
3349
self.setup_simple_branch('src', ['dir/'])
3350
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3351
'src/no-such-dir', 'dest/foo')
3353
self.addCleanup(dest_wt.unlock)
3354
# The dest tree is unmodified.
3355
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3356
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3358
def test_no_such_target_path(self):
3359
"""PathNotInTree is also raised if the specified path in the target
3360
tree does not exist.
3362
dest_wt = self.setup_simple_branch('dest')
3363
self.setup_simple_branch('src', ['file.txt'])
3364
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3365
'src', 'dest/no-such-dir/foo')
3367
self.addCleanup(dest_wt.unlock)
3368
# The dest tree is unmodified.
3369
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3370
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3373
class TestMergeHooks(TestCaseWithTransport):
3376
super(TestMergeHooks, self).setUp()
3377
self.tree_a = self.make_branch_and_tree('tree_a')
3378
self.build_tree_contents([('tree_a/file', b'content_1')])
3379
self.tree_a.add('file', b'file-id')
3380
self.tree_a.commit('added file')
3382
self.tree_b = self.tree_a.controldir.sprout(
3383
'tree_b').open_workingtree()
3384
self.build_tree_contents([('tree_b/file', b'content_2')])
3385
self.tree_b.commit('modify file')
3387
def test_pre_merge_hook_inject_different_tree(self):
3388
tree_c = self.tree_b.controldir.sprout('tree_c').open_workingtree()
3389
self.build_tree_contents([('tree_c/file', b'content_3')])
3390
tree_c.commit("more content")
3393
def factory(merger):
3394
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3395
merger.other_tree = tree_c
3396
calls.append(merger)
3397
_mod_merge.Merger.hooks.install_named_hook('pre_merge',
3398
factory, 'test factory')
3399
self.tree_a.merge_from_branch(self.tree_b.branch)
3401
self.assertFileEqual(b"content_3", 'tree_a/file')
3402
self.assertLength(1, calls)
3404
def test_post_merge_hook_called(self):
3407
def factory(merger):
3408
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3409
calls.append(merger)
3410
_mod_merge.Merger.hooks.install_named_hook('post_merge',
3411
factory, 'test factory')
3413
self.tree_a.merge_from_branch(self.tree_b.branch)
3415
self.assertFileEqual(b"content_2", 'tree_a/file')
3416
self.assertLength(1, calls)
125
log = self._get_log()
126
self.failUnless('All changes applied successfully.\n' in log)