1
# Copyright (C) 2005-2012, 2016 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
branch as _mod_branch,
27
revision as _mod_revision,
36
from ..conflicts import ConflictList, TextConflict
37
from ..errors import UnrelatedBranches, NoCommits
38
from ..merge import transform_tree, merge_inner, _PlanMerge
39
from ..osutils import basename, pathjoin, file_kind
42
TestCaseWithMemoryTransport,
43
TestCaseWithTransport,
46
from ..workingtree import WorkingTree
49
class TestMerge(TestCaseWithTransport):
50
"""Test appending more than one revision"""
52
def test_pending(self):
53
wt = self.make_branch_and_tree('.')
54
rev_a = wt.commit("lala!")
55
self.assertEqual([rev_a], wt.get_parent_ids())
56
self.assertRaises(errors.PointlessMerge, wt.merge_from_branch,
58
self.assertEqual([rev_a], wt.get_parent_ids())
62
wt = self.make_branch_and_tree('.')
66
wt.merge_from_branch(wt.branch, wt.branch.get_rev_id(2),
67
wt.branch.get_rev_id(1))
69
def test_nocommits(self):
70
wt = self.test_pending()
71
wt2 = self.make_branch_and_tree('branch2')
72
self.assertRaises(NoCommits, wt.merge_from_branch, wt2.branch)
75
def test_unrelated(self):
76
wt, wt2 = self.test_nocommits()
78
self.assertRaises(UnrelatedBranches, wt.merge_from_branch, wt2.branch)
81
def test_merge_one_file(self):
82
"""Do a partial merge of a tree which should not affect tree parents."""
83
wt1 = self.make_branch_and_tree('branch1')
84
tip = wt1.commit('empty commit')
85
wt2 = self.make_branch_and_tree('branch2')
87
with open('branch1/foo', 'wb') as f:
89
with open('branch1/bar', 'wb') as f:
93
wt1.commit('add foobar')
94
self.run_bzr('merge ../branch1/baz', retcode=3, working_dir='branch2')
95
self.run_bzr('merge ../branch1/foo', working_dir='branch2')
96
self.assertPathExists('branch2/foo')
97
self.assertPathDoesNotExist('branch2/bar')
98
wt2 = WorkingTree.open('branch2')
99
self.assertEqual([tip], wt2.get_parent_ids())
101
def test_pending_with_null(self):
102
"""When base is forced to revno 0, parent_ids are set"""
103
wt2 = self.test_unrelated()
104
wt1 = WorkingTree.open('.')
106
br1.fetch(wt2.branch)
107
# merge all of branch 2 into branch 1 even though they
109
wt1.merge_from_branch(wt2.branch, wt2.last_revision(), b'null:')
110
self.assertEqual([br1.last_revision(), wt2.branch.last_revision()],
111
wt1.get_parent_ids())
112
return (wt1, wt2.branch)
114
def test_two_roots(self):
115
"""Merge base is sane when two unrelated branches are merged"""
116
wt1, br2 = self.test_pending_with_null()
120
last = wt1.branch.last_revision()
121
last2 = br2.last_revision()
122
graph = wt1.branch.repository.get_graph()
123
self.assertEqual(last2, graph.find_unique_lca(last, last2))
127
def test_merge_into_null_tree(self):
128
wt = self.make_branch_and_tree('tree')
129
null_tree = wt.basis_tree()
130
self.build_tree(['tree/file'])
132
wt.commit('tree with root')
133
merger = _mod_merge.Merge3Merger(null_tree, null_tree, null_tree, wt,
134
this_branch=wt.branch,
136
with merger.make_preview_transform() as tt:
137
self.assertEqual([], tt.find_conflicts())
138
preview = tt.get_preview_tree()
139
self.assertEqual(wt.path2id(''), preview.path2id(''))
141
def test_merge_unrelated_retains_root(self):
142
wt = self.make_branch_and_tree('tree')
143
other_tree = self.make_branch_and_tree('other')
144
self.addCleanup(other_tree.lock_read().unlock)
145
merger = _mod_merge.Merge3Merger(wt, wt, wt.basis_tree(), other_tree,
146
this_branch=wt.branch,
148
with transform.TransformPreview(wt) as merger.tt:
149
merger._compute_transform()
150
new_root_id = merger.tt.final_file_id(merger.tt.root)
151
self.assertEqual(wt.path2id(''), new_root_id)
153
def test_create_rename(self):
154
"""Rename an inventory entry while creating the file"""
155
tree = self.make_branch_and_tree('.')
156
with open('name1', 'wb') as f:
159
tree.commit(message="hello")
160
tree.rename_one('name1', 'name2')
162
transform_tree(tree, tree.branch.basis_tree())
164
def test_layered_rename(self):
165
"""Rename both child and parent at same time"""
166
tree = self.make_branch_and_tree('.')
169
filename = pathjoin('dirname1', 'name1')
170
with open(filename, 'wb') as f:
173
tree.commit(message="hello")
174
filename2 = pathjoin('dirname1', 'name2')
175
tree.rename_one(filename, filename2)
176
tree.rename_one('dirname1', 'dirname2')
177
transform_tree(tree, tree.branch.basis_tree())
179
def test_ignore_zero_merge_inner(self):
180
# Test that merge_inner's ignore zero parameter is effective
181
tree_a = self.make_branch_and_tree('a')
182
tree_a.commit(message="hello")
183
dir_b = tree_a.controldir.sprout('b')
184
tree_b = dir_b.open_workingtree()
186
self.addCleanup(tree_b.unlock)
187
tree_a.commit(message="hello again")
188
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
189
this_tree=tree_b, ignore_zero=True)
190
self.assertTrue('All changes applied successfully.\n' not in
193
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
194
this_tree=tree_b, ignore_zero=False)
196
'All changes applied successfully.\n' in self.get_log())
198
def test_merge_inner_conflicts(self):
199
tree_a = self.make_branch_and_tree('a')
200
tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
201
merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
202
self.assertEqual(1, len(tree_a.conflicts()))
204
def test_rmdir_conflict(self):
205
tree_a = self.make_branch_and_tree('a')
206
self.build_tree(['a/b/'])
207
tree_a.add('b', b'b-id')
208
tree_a.commit('added b')
209
# basis_tree() is only guaranteed to be valid as long as it is actually
210
# the basis tree. This mutates the tree after grabbing basis, so go to
212
base_tree = tree_a.branch.repository.revision_tree(
213
tree_a.last_revision())
214
tree_z = tree_a.controldir.sprout('z').open_workingtree()
215
self.build_tree(['a/b/c'])
217
tree_a.commit('added c')
219
tree_z.commit('removed b')
220
merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
222
conflicts.MissingParent('Created directory', 'b', b'b-id'),
223
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
225
merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
228
conflicts.DeletingParent('Not deleting', 'b', b'b-id'),
229
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
232
def test_nested_merge(self):
234
'iter_changes doesn\'t work with changes in nested trees')
235
tree = self.make_branch_and_tree('tree',
236
format='development-subtree')
237
sub_tree = self.make_branch_and_tree('tree/sub-tree',
238
format='development-subtree')
239
sub_tree.set_root_id(b'sub-tree-root')
240
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
242
sub_tree.commit('foo')
243
tree.add_reference(sub_tree)
244
tree.commit('set text to 1')
245
tree2 = tree.controldir.sprout('tree2').open_workingtree()
246
# modify the file in the subtree
247
self.build_tree_contents([('tree2/sub-tree/file', b'text2')])
248
# and merge the changes from the diverged subtree into the containing
250
tree2.commit('changed file text')
251
tree.merge_from_branch(tree2.branch)
252
self.assertFileEqual(b'text2', 'tree/sub-tree/file')
254
def test_merge_with_missing(self):
255
tree_a = self.make_branch_and_tree('tree_a')
256
self.build_tree_contents([('tree_a/file', b'content_1')])
258
tree_a.commit('commit base')
259
# basis_tree() is only guaranteed to be valid as long as it is actually
260
# the basis tree. This test commits to the tree after grabbing basis,
261
# so we go to the repository.
262
base_tree = tree_a.branch.repository.revision_tree(
263
tree_a.last_revision())
264
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
265
self.build_tree_contents([('tree_a/file', b'content_2')])
266
tree_a.commit('commit other')
267
other_tree = tree_a.basis_tree()
268
# 'file' is now missing but isn't altered in any commit in b so no
269
# change should be applied.
270
os.unlink('tree_b/file')
271
merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
273
def test_merge_kind_change(self):
274
tree_a = self.make_branch_and_tree('tree_a')
275
self.build_tree_contents([('tree_a/file', b'content_1')])
276
tree_a.add('file', b'file-id')
277
tree_a.commit('added file')
278
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
279
os.unlink('tree_a/file')
280
self.build_tree(['tree_a/file/'])
281
tree_a.commit('changed file to directory')
282
tree_b.merge_from_branch(tree_a.branch)
283
self.assertEqual('directory', file_kind('tree_b/file'))
285
self.assertEqual('file', file_kind('tree_b/file'))
286
self.build_tree_contents([('tree_b/file', b'content_2')])
287
tree_b.commit('content change')
288
tree_b.merge_from_branch(tree_a.branch)
289
self.assertEqual(tree_b.conflicts(),
290
[conflicts.ContentsConflict('file',
291
file_id=b'file-id')])
293
def test_merge_type_registry(self):
294
merge_type_option = option.Option.OPTIONS['merge-type']
295
self.assertFalse('merge4' in [x[0] for x in
296
merge_type_option.iter_switches()])
297
registry = _mod_merge.get_merge_type_registry()
298
registry.register_lazy('merge4', 'breezy.merge', 'Merge4Merger',
299
'time-travelling merge')
300
self.assertTrue('merge4' in [x[0] for x in
301
merge_type_option.iter_switches()])
302
registry.remove('merge4')
303
self.assertFalse('merge4' in [x[0] for x in
304
merge_type_option.iter_switches()])
306
def test_merge_other_moves_we_deleted(self):
307
tree_a = self.make_branch_and_tree('A')
309
self.addCleanup(tree_a.unlock)
310
self.build_tree(['A/a'])
312
tree_a.commit('1', rev_id=b'rev-1')
314
tree_a.rename_one('a', 'b')
316
bzrdir_b = tree_a.controldir.sprout('B', revision_id=b'rev-1')
317
tree_b = bzrdir_b.open_workingtree()
319
self.addCleanup(tree_b.unlock)
323
tree_b.merge_from_branch(tree_a.branch)
324
except AttributeError:
325
self.fail('tried to join a path when name was None')
327
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
328
tree_a = self.make_branch_and_tree('a')
329
self.build_tree(['a/file_1', 'a/file_2'])
330
tree_a.add(['file_1'])
331
tree_a.commit('commit 1')
332
tree_a.add(['file_2'])
333
tree_a.commit('commit 2')
334
tree_b = tree_a.controldir.sprout('b').open_workingtree()
335
tree_b.rename_one('file_1', 'renamed')
336
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
337
merger.merge_type = _mod_merge.Merge3Merger
339
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
341
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
342
tree_a = self.make_branch_and_tree('a')
343
self.build_tree(['a/file_1', 'a/file_2'])
344
tree_a.add(['file_1'])
345
tree_a.commit('commit 1')
346
tree_a.add(['file_2'])
347
tree_a.commit('commit 2')
348
tree_b = tree_a.controldir.sprout('b').open_workingtree()
349
tree_b.rename_one('file_1', 'renamed')
350
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
351
merger.merge_type = _mod_merge.WeaveMerger
353
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
355
def prepare_cherrypick(self):
356
"""Prepare a pair of trees for cherrypicking tests.
358
Both trees have a file, 'file'.
359
rev1 sets content to 'a'.
362
A full merge of rev2b and rev3b into this_tree would add both 'b' and
363
'c'. A successful cherrypick of rev2b-rev3b into this_tree will add
366
this_tree = self.make_branch_and_tree('this')
367
self.build_tree_contents([('this/file', b"a\n")])
368
this_tree.add('file')
369
this_tree.commit('rev1')
370
other_tree = this_tree.controldir.sprout('other').open_workingtree()
371
self.build_tree_contents([('other/file', b"a\nb\n")])
372
other_tree.commit('rev2b', rev_id=b'rev2b')
373
self.build_tree_contents([('other/file', b"c\na\nb\n")])
374
other_tree.commit('rev3b', rev_id=b'rev3b')
375
this_tree.lock_write()
376
self.addCleanup(this_tree.unlock)
377
return this_tree, other_tree
379
def test_weave_cherrypick(self):
380
this_tree, other_tree = self.prepare_cherrypick()
381
merger = _mod_merge.Merger.from_revision_ids(
382
this_tree, b'rev3b', b'rev2b', other_tree.branch)
383
merger.merge_type = _mod_merge.WeaveMerger
385
self.assertFileEqual(b'c\na\n', 'this/file')
387
def test_weave_cannot_reverse_cherrypick(self):
388
this_tree, other_tree = self.prepare_cherrypick()
389
merger = _mod_merge.Merger.from_revision_ids(
390
this_tree, b'rev2b', b'rev3b', other_tree.branch)
391
merger.merge_type = _mod_merge.WeaveMerger
392
self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
394
def test_merge3_can_reverse_cherrypick(self):
395
this_tree, other_tree = self.prepare_cherrypick()
396
merger = _mod_merge.Merger.from_revision_ids(
397
this_tree, b'rev2b', b'rev3b', other_tree.branch)
398
merger.merge_type = _mod_merge.Merge3Merger
401
def test_merge3_will_detect_cherrypick(self):
402
this_tree = self.make_branch_and_tree('this')
403
self.build_tree_contents([('this/file', b"a\n")])
404
this_tree.add('file')
405
this_tree.commit('rev1')
406
other_tree = this_tree.controldir.sprout('other').open_workingtree()
407
self.build_tree_contents([('other/file', b"a\nb\n")])
408
other_tree.commit('rev2b', rev_id=b'rev2b')
409
self.build_tree_contents([('other/file', b"a\nb\nc\n")])
410
other_tree.commit('rev3b', rev_id=b'rev3b')
411
this_tree.lock_write()
412
self.addCleanup(this_tree.unlock)
414
merger = _mod_merge.Merger.from_revision_ids(
415
this_tree, b'rev3b', b'rev2b', other_tree.branch)
416
merger.merge_type = _mod_merge.Merge3Merger
418
self.assertFileEqual(b'a\n'
422
b'>>>>>>> MERGE-SOURCE\n',
425
def test_merge_reverse_revision_range(self):
426
tree = self.make_branch_and_tree(".")
428
self.addCleanup(tree.unlock)
429
self.build_tree(['a'])
431
first_rev = tree.commit("added a")
432
merger = _mod_merge.Merger.from_revision_ids(tree,
433
_mod_revision.NULL_REVISION,
435
merger.merge_type = _mod_merge.Merge3Merger
436
merger.interesting_files = 'a'
437
conflict_count = merger.do_merge()
438
self.assertEqual(0, conflict_count)
440
self.assertPathDoesNotExist("a")
442
self.assertPathExists("a")
444
def test_make_merger(self):
445
this_tree = self.make_branch_and_tree('this')
446
this_tree.commit('rev1', rev_id=b'rev1')
447
other_tree = this_tree.controldir.sprout('other').open_workingtree()
448
this_tree.commit('rev2', rev_id=b'rev2a')
449
other_tree.commit('rev2', rev_id=b'rev2b')
450
this_tree.lock_write()
451
self.addCleanup(this_tree.unlock)
452
merger = _mod_merge.Merger.from_revision_ids(
453
this_tree, b'rev2b', other_branch=other_tree.branch)
454
merger.merge_type = _mod_merge.Merge3Merger
455
tree_merger = merger.make_merger()
456
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
457
self.assertEqual(b'rev2b',
458
tree_merger.other_tree.get_revision_id())
459
self.assertEqual(b'rev1',
460
tree_merger.base_tree.get_revision_id())
461
self.assertEqual(other_tree.branch, tree_merger.other_branch)
463
def test_make_preview_transform(self):
464
this_tree = self.make_branch_and_tree('this')
465
self.build_tree_contents([('this/file', b'1\n')])
466
this_tree.add('file', b'file-id')
467
this_tree.commit('rev1', rev_id=b'rev1')
468
other_tree = this_tree.controldir.sprout('other').open_workingtree()
469
self.build_tree_contents([('this/file', b'1\n2a\n')])
470
this_tree.commit('rev2', rev_id=b'rev2a')
471
self.build_tree_contents([('other/file', b'2b\n1\n')])
472
other_tree.commit('rev2', rev_id=b'rev2b')
473
this_tree.lock_write()
474
self.addCleanup(this_tree.unlock)
475
merger = _mod_merge.Merger.from_revision_ids(
476
this_tree, b'rev2b', other_branch=other_tree.branch)
477
merger.merge_type = _mod_merge.Merge3Merger
478
tree_merger = merger.make_merger()
479
with tree_merger.make_preview_transform() as tt:
480
preview_tree = tt.get_preview_tree()
481
with this_tree.get_file('file') as tree_file:
482
self.assertEqual(b'1\n2a\n', tree_file.read())
483
with preview_tree.get_file('file') as preview_file:
484
self.assertEqual(b'2b\n1\n2a\n', preview_file.read())
486
def test_do_merge(self):
487
this_tree = self.make_branch_and_tree('this')
488
self.build_tree_contents([('this/file', b'1\n')])
489
this_tree.add('file', b'file-id')
490
this_tree.commit('rev1', rev_id=b'rev1')
491
other_tree = this_tree.controldir.sprout('other').open_workingtree()
492
self.build_tree_contents([('this/file', b'1\n2a\n')])
493
this_tree.commit('rev2', rev_id=b'rev2a')
494
self.build_tree_contents([('other/file', b'2b\n1\n')])
495
other_tree.commit('rev2', rev_id=b'rev2b')
496
this_tree.lock_write()
497
self.addCleanup(this_tree.unlock)
498
merger = _mod_merge.Merger.from_revision_ids(
499
this_tree, b'rev2b', other_branch=other_tree.branch)
500
merger.merge_type = _mod_merge.Merge3Merger
501
tree_merger = merger.make_merger()
502
tt = tree_merger.do_merge()
503
with this_tree.get_file('file') as tree_file:
504
self.assertEqual(b'2b\n1\n2a\n', tree_file.read())
506
def test_merge_require_tree_root(self):
507
tree = self.make_branch_and_tree(".")
509
self.addCleanup(tree.unlock)
510
self.build_tree(['a'])
512
first_rev = tree.commit("added a")
513
old_root_id = tree.path2id('')
514
merger = _mod_merge.Merger.from_revision_ids(tree,
515
_mod_revision.NULL_REVISION,
517
merger.merge_type = _mod_merge.Merge3Merger
518
conflict_count = merger.do_merge()
519
self.assertEqual(0, conflict_count)
520
self.assertEqual({''}, set(tree.all_versioned_paths()))
521
tree.set_parent_ids([])
523
def test_merge_add_into_deleted_root(self):
524
# Yes, people actually do this. And report bugs if it breaks.
525
source = self.make_branch_and_tree('source', format='rich-root-pack')
526
self.build_tree(['source/foo/'])
527
source.add('foo', b'foo-id')
528
source.commit('Add foo')
529
target = source.controldir.sprout('target').open_workingtree()
530
subtree = target.extract('foo')
531
subtree.commit('Delete root')
532
self.build_tree(['source/bar'])
533
source.add('bar', b'bar-id')
534
source.commit('Add bar')
535
subtree.merge_from_branch(source.branch)
537
def test_merge_joined_branch(self):
538
source = self.make_branch_and_tree('source', format='rich-root-pack')
539
self.build_tree(['source/foo'])
541
source.commit('Add foo')
542
target = self.make_branch_and_tree('target', format='rich-root-pack')
543
self.build_tree(['target/bla'])
545
target.commit('Add bla')
546
nested = source.controldir.sprout('target/subtree').open_workingtree()
547
target.subsume(nested)
548
target.commit('Join nested')
549
self.build_tree(['source/bar'])
551
source.commit('Add bar')
552
target.merge_from_branch(source.branch)
553
target.commit('Merge source')
556
class TestPlanMerge(TestCaseWithMemoryTransport):
559
super(TestPlanMerge, self).setUp()
560
mapper = versionedfile.PrefixMapper()
561
factory = knit.make_file_factory(True, mapper)
562
self.vf = factory(self.get_transport())
563
self.plan_merge_vf = versionedfile._PlanMergeVersionedFile(b'root')
564
self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
566
def add_version(self, key, parents, text):
568
key, parents, [bytes([c]) + b'\n' for c in bytearray(text)])
570
def add_rev(self, prefix, revision_id, parents, text):
571
self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
574
def add_uncommitted_version(self, key, parents, text):
575
self.plan_merge_vf.add_lines(key, parents,
576
[bytes([c]) + b'\n' for c in bytearray(text)])
578
def setup_plan_merge(self):
579
self.add_rev(b'root', b'A', [], b'abc')
580
self.add_rev(b'root', b'B', [b'A'], b'acehg')
581
self.add_rev(b'root', b'C', [b'A'], b'fabg')
582
return _PlanMerge(b'B', b'C', self.plan_merge_vf, (b'root',))
584
def setup_plan_merge_uncommitted(self):
585
self.add_version((b'root', b'A'), [], b'abc')
586
self.add_uncommitted_version(
587
(b'root', b'B:'), [(b'root', b'A')], b'acehg')
588
self.add_uncommitted_version(
589
(b'root', b'C:'), [(b'root', b'A')], b'fabg')
590
return _PlanMerge(b'B:', b'C:', self.plan_merge_vf, (b'root',))
592
def test_base_from_plan(self):
593
self.setup_plan_merge()
594
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
595
pwm = versionedfile.PlanWeaveMerge(plan)
596
self.assertEqual([b'a\n', b'b\n', b'c\n'], pwm.base_from_plan())
598
def test_unique_lines(self):
599
plan = self.setup_plan_merge()
600
self.assertEqual(plan._unique_lines(
601
plan._get_matching_blocks(b'B', b'C')),
604
def test_plan_merge(self):
605
self.setup_plan_merge()
606
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
609
('unchanged', b'a\n'),
610
('killed-a', b'b\n'),
611
('killed-b', b'c\n'),
618
def test_plan_merge_cherrypick(self):
619
self.add_rev(b'root', b'A', [], b'abc')
620
self.add_rev(b'root', b'B', [b'A'], b'abcde')
621
self.add_rev(b'root', b'C', [b'A'], b'abcefg')
622
self.add_rev(b'root', b'D', [b'A', b'B', b'C'], b'abcdegh')
623
my_plan = _PlanMerge(b'B', b'D', self.plan_merge_vf, (b'root',))
624
# We shortcut when one text supersedes the other in the per-file graph.
625
# We don't actually need to compare the texts at this point.
634
list(my_plan.plan_merge()))
636
def test_plan_merge_no_common_ancestor(self):
637
self.add_rev(b'root', b'A', [], b'abc')
638
self.add_rev(b'root', b'B', [], b'xyz')
639
my_plan = _PlanMerge(b'A', b'B', self.plan_merge_vf, (b'root',))
647
list(my_plan.plan_merge()))
649
def test_plan_merge_tail_ancestors(self):
650
# The graph looks like this:
651
# A # Common to all ancestors
653
# B C # Ancestors of E, only common to one side
655
# D E F # D, F are unique to G, H respectively
656
# |/ \| # E is the LCA for G & H, and the unique LCA for
661
# I J # criss-cross merge of G, H
663
# In this situation, a simple pruning of ancestors of E will leave D &
664
# F "dangling", which looks like they introduce lines different from
665
# the ones in E, but in actuality C&B introduced the lines, and they
666
# are already present in E
668
# Introduce the base text
669
self.add_rev(b'root', b'A', [], b'abc')
670
# Introduces a new line B
671
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
672
# Introduces a new line C
673
self.add_rev(b'root', b'C', [b'A'], b'abCc')
674
# Introduce new line D
675
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
676
# Merges B and C by just incorporating both
677
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
678
# Introduce new line F
679
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
680
# Merge D & E by just combining the texts
681
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
682
# Merge F & E by just combining the texts
683
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
684
# Merge G & H by just combining texts
685
self.add_rev(b'root', b'I', [b'G', b'H'], b'DaBbCcF')
686
# Merge G & H but supersede an old line in B
687
self.add_rev(b'root', b'J', [b'H', b'G'], b'DaJbCcF')
688
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
690
('unchanged', b'D\n'),
691
('unchanged', b'a\n'),
692
('killed-b', b'B\n'),
694
('unchanged', b'b\n'),
695
('unchanged', b'C\n'),
696
('unchanged', b'c\n'),
697
('unchanged', b'F\n')],
700
def test_plan_merge_tail_triple_ancestors(self):
701
# The graph looks like this:
702
# A # Common to all ancestors
704
# B C # Ancestors of E, only common to one side
706
# D E F # D, F are unique to G, H respectively
707
# |/|\| # E is the LCA for G & H, and the unique LCA for
709
# |\ /| # Q is just an extra node which is merged into both
712
# I J # criss-cross merge of G, H
714
# This is the same as the test_plan_merge_tail_ancestors, except we add
715
# a third LCA that doesn't add new lines, but will trigger our more
716
# involved ancestry logic
718
self.add_rev(b'root', b'A', [], b'abc')
719
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
720
self.add_rev(b'root', b'C', [b'A'], b'abCc')
721
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
722
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
723
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
724
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
725
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
726
self.add_rev(b'root', b'Q', [b'E'], b'aBbCc')
727
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DaBbCcF')
728
# Merge G & H but supersede an old line in B
729
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DaJbCcF')
730
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
732
('unchanged', b'D\n'),
733
('unchanged', b'a\n'),
734
('killed-b', b'B\n'),
736
('unchanged', b'b\n'),
737
('unchanged', b'C\n'),
738
('unchanged', b'c\n'),
739
('unchanged', b'F\n')],
742
def test_plan_merge_2_tail_triple_ancestors(self):
743
# The graph looks like this:
744
# A B # 2 tails going back to NULL
746
# D E F # D, is unique to G, F to H
747
# |/|\| # E is the LCA for G & H, and the unique LCA for
749
# |\ /| # Q is just an extra node which is merged into both
752
# I J # criss-cross merge of G, H (and Q)
755
# This is meant to test after hitting a 3-way LCA, and multiple tail
756
# ancestors (only have NULL_REVISION in common)
758
self.add_rev(b'root', b'A', [], b'abc')
759
self.add_rev(b'root', b'B', [], b'def')
760
self.add_rev(b'root', b'D', [b'A'], b'Dabc')
761
self.add_rev(b'root', b'E', [b'A', b'B'], b'abcdef')
762
self.add_rev(b'root', b'F', [b'B'], b'defF')
763
self.add_rev(b'root', b'G', [b'D', b'E'], b'Dabcdef')
764
self.add_rev(b'root', b'H', [b'F', b'E'], b'abcdefF')
765
self.add_rev(b'root', b'Q', [b'E'], b'abcdef')
766
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DabcdefF')
767
# Merge G & H but supersede an old line in B
768
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DabcdJfF')
769
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
771
('unchanged', b'D\n'),
772
('unchanged', b'a\n'),
773
('unchanged', b'b\n'),
774
('unchanged', b'c\n'),
775
('unchanged', b'd\n'),
776
('killed-b', b'e\n'),
778
('unchanged', b'f\n'),
779
('unchanged', b'F\n')],
782
def test_plan_merge_uncommitted_files(self):
783
self.setup_plan_merge_uncommitted()
784
plan = self.plan_merge_vf.plan_merge(b'B:', b'C:')
787
('unchanged', b'a\n'),
788
('killed-a', b'b\n'),
789
('killed-b', b'c\n'),
796
def test_plan_merge_insert_order(self):
797
"""Weave merges are sensitive to the order of insertion.
799
Specifically for overlapping regions, it effects which region gets put
800
'first'. And when a user resolves an overlapping merge, if they use the
801
same ordering, then the lines match the parents, if they don't only
802
*some* of the lines match.
804
self.add_rev(b'root', b'A', [], b'abcdef')
805
self.add_rev(b'root', b'B', [b'A'], b'abwxcdef')
806
self.add_rev(b'root', b'C', [b'A'], b'abyzcdef')
807
# Merge, and resolve the conflict by adding *both* sets of lines
808
# If we get the ordering wrong, these will look like new lines in D,
809
# rather than carried over from B, C
810
self.add_rev(b'root', b'D', [b'B', b'C'],
812
# Supersede the lines in B and delete the lines in C, which will
813
# conflict if they are treated as being in D
814
self.add_rev(b'root', b'E', [b'C', b'B'],
816
# Same thing for the lines in C
817
self.add_rev(b'root', b'F', [b'C'], b'abpqcdef')
818
plan = self.plan_merge_vf.plan_merge(b'D', b'E')
820
('unchanged', b'a\n'),
821
('unchanged', b'b\n'),
822
('killed-b', b'w\n'),
823
('killed-b', b'x\n'),
824
('killed-b', b'y\n'),
825
('killed-b', b'z\n'),
828
('unchanged', b'c\n'),
829
('unchanged', b'd\n'),
830
('unchanged', b'e\n'),
831
('unchanged', b'f\n')],
833
plan = self.plan_merge_vf.plan_merge(b'E', b'D')
834
# Going in the opposite direction shows the effect of the opposite plan
836
('unchanged', b'a\n'),
837
('unchanged', b'b\n'),
840
('killed-a', b'y\n'),
841
('killed-a', b'z\n'),
842
('killed-both', b'w\n'),
843
('killed-both', b'x\n'),
846
('unchanged', b'c\n'),
847
('unchanged', b'd\n'),
848
('unchanged', b'e\n'),
849
('unchanged', b'f\n')],
852
def test_plan_merge_criss_cross(self):
853
# This is specificly trying to trigger problems when using limited
854
# ancestry and weaves. The ancestry graph looks like:
855
# XX unused ancestor, should not show up in the weave
859
# B \ Introduces a line 'foo'
861
# C D E C & D both have 'foo', E has different changes
865
# F G All of C, D, E are merged into F and G, so they are
866
# all common ancestors.
868
# The specific issue with weaves:
869
# B introduced a text ('foo') that is present in both C and D.
870
# If we do not include B (because it isn't an ancestor of E), then
871
# the A=>C and A=>D look like both sides independently introduce the
872
# text ('foo'). If F does not modify the text, it would still appear
873
# to have deleted on of the versions from C or D. If G then modifies
874
# 'foo', it should appear as superseding the value in F (since it
875
# came from B), rather than conflict because of the resolution during
877
self.add_rev(b'root', b'XX', [], b'qrs')
878
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
879
self.add_rev(b'root', b'B', [b'A'], b'axcdef')
880
self.add_rev(b'root', b'C', [b'B'], b'axcdefg')
881
self.add_rev(b'root', b'D', [b'B'], b'haxcdef')
882
self.add_rev(b'root', b'E', [b'A'], b'abcdyf')
883
# Simple combining of all texts
884
self.add_rev(b'root', b'F', [b'C', b'D', b'E'], b'haxcdyfg')
885
# combine and supersede 'x'
886
self.add_rev(b'root', b'G', [b'C', b'D', b'E'], b'hazcdyfg')
887
plan = self.plan_merge_vf.plan_merge(b'F', b'G')
889
('unchanged', b'h\n'),
890
('unchanged', b'a\n'),
891
('killed-base', b'b\n'),
892
('killed-b', b'x\n'),
894
('unchanged', b'c\n'),
895
('unchanged', b'd\n'),
896
('killed-base', b'e\n'),
897
('unchanged', b'y\n'),
898
('unchanged', b'f\n'),
899
('unchanged', b'g\n')],
901
plan = self.plan_merge_vf.plan_lca_merge(b'F', b'G')
902
# This is one of the main differences between plan_merge and
903
# plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
904
# because 'x' was not present in one of the bases. However, in this
905
# case it is spurious because 'x' does not exist in the global base A.
907
('unchanged', b'h\n'),
908
('unchanged', b'a\n'),
909
('conflicted-a', b'x\n'),
911
('unchanged', b'c\n'),
912
('unchanged', b'd\n'),
913
('unchanged', b'y\n'),
914
('unchanged', b'f\n'),
915
('unchanged', b'g\n')],
918
def test_criss_cross_flip_flop(self):
919
# This is specificly trying to trigger problems when using limited
920
# ancestry and weaves. The ancestry graph looks like:
921
# XX unused ancestor, should not show up in the weave
925
# B C B & C both introduce a new line
929
# D E B & C are both merged, so both are common ancestors
930
# In the process of merging, both sides order the new
933
self.add_rev(b'root', b'XX', [], b'qrs')
934
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
935
self.add_rev(b'root', b'B', [b'A'], b'abcdgef')
936
self.add_rev(b'root', b'C', [b'A'], b'abcdhef')
937
self.add_rev(b'root', b'D', [b'B', b'C'], b'abcdghef')
938
self.add_rev(b'root', b'E', [b'C', b'B'], b'abcdhgef')
939
plan = list(self.plan_merge_vf.plan_merge(b'D', b'E'))
941
('unchanged', b'a\n'),
942
('unchanged', b'b\n'),
943
('unchanged', b'c\n'),
944
('unchanged', b'd\n'),
946
('unchanged', b'g\n'),
947
('killed-b', b'h\n'),
948
('unchanged', b'e\n'),
949
('unchanged', b'f\n'),
951
pwm = versionedfile.PlanWeaveMerge(plan)
952
self.assertEqualDiff(b'a\nb\nc\nd\ng\nh\ne\nf\n',
953
b''.join(pwm.base_from_plan()))
954
# Reversing the order reverses the merge plan, and final order of 'hg'
956
plan = list(self.plan_merge_vf.plan_merge(b'E', b'D'))
958
('unchanged', b'a\n'),
959
('unchanged', b'b\n'),
960
('unchanged', b'c\n'),
961
('unchanged', b'd\n'),
963
('unchanged', b'h\n'),
964
('killed-b', b'g\n'),
965
('unchanged', b'e\n'),
966
('unchanged', b'f\n'),
968
pwm = versionedfile.PlanWeaveMerge(plan)
969
self.assertEqualDiff(b'a\nb\nc\nd\nh\ng\ne\nf\n',
970
b''.join(pwm.base_from_plan()))
971
# This is where lca differs, in that it (fairly correctly) determines
972
# that there is a conflict because both sides resolved the merge
974
plan = list(self.plan_merge_vf.plan_lca_merge(b'D', b'E'))
976
('unchanged', b'a\n'),
977
('unchanged', b'b\n'),
978
('unchanged', b'c\n'),
979
('unchanged', b'd\n'),
980
('conflicted-b', b'h\n'),
981
('unchanged', b'g\n'),
982
('conflicted-a', b'h\n'),
983
('unchanged', b'e\n'),
984
('unchanged', b'f\n'),
986
pwm = versionedfile.PlanWeaveMerge(plan)
987
self.assertEqualDiff(b'a\nb\nc\nd\ng\ne\nf\n',
988
b''.join(pwm.base_from_plan()))
989
# Reversing it changes what line is doubled, but still gives a
991
plan = list(self.plan_merge_vf.plan_lca_merge(b'E', b'D'))
993
('unchanged', b'a\n'),
994
('unchanged', b'b\n'),
995
('unchanged', b'c\n'),
996
('unchanged', b'd\n'),
997
('conflicted-b', b'g\n'),
998
('unchanged', b'h\n'),
999
('conflicted-a', b'g\n'),
1000
('unchanged', b'e\n'),
1001
('unchanged', b'f\n'),
1003
pwm = versionedfile.PlanWeaveMerge(plan)
1004
self.assertEqualDiff(b'a\nb\nc\nd\nh\ne\nf\n',
1005
b''.join(pwm.base_from_plan()))
1007
def assertRemoveExternalReferences(self, filtered_parent_map,
1008
child_map, tails, parent_map):
1009
"""Assert results for _PlanMerge._remove_external_references."""
1010
(act_filtered_parent_map, act_child_map,
1011
act_tails) = _PlanMerge._remove_external_references(parent_map)
1013
# The parent map *should* preserve ordering, but the ordering of
1014
# children is not strictly defined
1015
# child_map = dict((k, sorted(children))
1016
# for k, children in child_map.iteritems())
1017
# act_child_map = dict(k, sorted(children)
1018
# for k, children in act_child_map.iteritems())
1019
self.assertEqual(filtered_parent_map, act_filtered_parent_map)
1020
self.assertEqual(child_map, act_child_map)
1021
self.assertEqual(sorted(tails), sorted(act_tails))
1023
def test__remove_external_references(self):
1024
# First, nothing to remove
1025
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1026
{1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
1027
# The reverse direction
1028
self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
1029
{3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
1031
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1032
{1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
1034
self.assertRemoveExternalReferences(
1035
{4: [2, 3], 3: [], 2: [1], 1: []},
1036
{1: [2], 2: [4], 3: [4], 4: []},
1038
{4: [2, 3], 3: [5], 2: [1], 1: [6]})
1040
self.assertRemoveExternalReferences(
1041
{1: [3], 2: [3, 4], 3: [], 4: []},
1042
{1: [], 2: [], 3: [1, 2], 4: [2]},
1044
{1: [3], 2: [3, 4], 3: [5], 4: []})
1046
def assertPruneTails(self, pruned_map, tails, parent_map):
1048
for key, parent_keys in parent_map.items():
1049
child_map.setdefault(key, [])
1050
for pkey in parent_keys:
1051
child_map.setdefault(pkey, []).append(key)
1052
_PlanMerge._prune_tails(parent_map, child_map, tails)
1053
self.assertEqual(pruned_map, parent_map)
1055
def test__prune_tails(self):
1056
# Nothing requested to prune
1057
self.assertPruneTails({1: [], 2: [], 3: []}, [],
1058
{1: [], 2: [], 3: []})
1059
# Prune a single entry
1060
self.assertPruneTails({1: [], 3: []}, [2],
1061
{1: [], 2: [], 3: []})
1063
self.assertPruneTails({1: []}, [3],
1064
{1: [], 2: [3], 3: []})
1065
# Prune a chain with a diamond
1066
self.assertPruneTails({1: []}, [5],
1067
{1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
1068
# Prune a partial chain
1069
self.assertPruneTails({1: [6], 6: []}, [5],
1070
{1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
1072
# Prune a chain with multiple tips, that pulls out intermediates
1073
self.assertPruneTails({1: [3], 3: []}, [4, 5],
1074
{1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []})
1075
self.assertPruneTails({1: [3], 3: []}, [5, 4],
1076
{1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []})
1078
def test_subtract_plans(self):
1080
('unchanged', b'a\n'),
1082
('killed-a', b'c\n'),
1085
('killed-b', b'f\n'),
1086
('killed-b', b'g\n'),
1089
('unchanged', b'a\n'),
1091
('killed-a', b'c\n'),
1094
('killed-b', b'f\n'),
1095
('killed-b', b'i\n'),
1098
('unchanged', b'a\n'),
1100
('killed-a', b'c\n'),
1102
('unchanged', b'f\n'),
1103
('killed-b', b'i\n'),
1105
self.assertEqual(subtracted_plan,
1106
list(_PlanMerge._subtract_plans(old_plan, new_plan)))
1108
def setup_merge_with_base(self):
1109
self.add_rev(b'root', b'COMMON', [], b'abc')
1110
self.add_rev(b'root', b'THIS', [b'COMMON'], b'abcd')
1111
self.add_rev(b'root', b'BASE', [b'COMMON'], b'eabc')
1112
self.add_rev(b'root', b'OTHER', [b'BASE'], b'eafb')
1114
def test_plan_merge_with_base(self):
1115
self.setup_merge_with_base()
1116
plan = self.plan_merge_vf.plan_merge(b'THIS', b'OTHER', b'BASE')
1117
self.assertEqual([('unchanged', b'a\n'),
1119
('unchanged', b'b\n'),
1120
('killed-b', b'c\n'),
1124
def test_plan_lca_merge(self):
1125
self.setup_plan_merge()
1126
plan = self.plan_merge_vf.plan_lca_merge(b'B', b'C')
1129
('unchanged', b'a\n'),
1130
('killed-b', b'c\n'),
1133
('killed-a', b'b\n'),
1134
('unchanged', b'g\n')],
1137
def test_plan_lca_merge_uncommitted_files(self):
1138
self.setup_plan_merge_uncommitted()
1139
plan = self.plan_merge_vf.plan_lca_merge(b'B:', b'C:')
1142
('unchanged', b'a\n'),
1143
('killed-b', b'c\n'),
1146
('killed-a', b'b\n'),
1147
('unchanged', b'g\n')],
1150
def test_plan_lca_merge_with_base(self):
1151
self.setup_merge_with_base()
1152
plan = self.plan_merge_vf.plan_lca_merge(b'THIS', b'OTHER', b'BASE')
1153
self.assertEqual([('unchanged', b'a\n'),
1155
('unchanged', b'b\n'),
1156
('killed-b', b'c\n'),
1160
def test_plan_lca_merge_with_criss_cross(self):
1161
self.add_version((b'root', b'ROOT'), [], b'abc')
1162
# each side makes a change
1163
self.add_version((b'root', b'REV1'), [(b'root', b'ROOT')], b'abcd')
1164
self.add_version((b'root', b'REV2'), [(b'root', b'ROOT')], b'abce')
1165
# both sides merge, discarding others' changes
1166
self.add_version((b'root', b'LCA1'),
1167
[(b'root', b'REV1'), (b'root', b'REV2')], b'abcd')
1168
self.add_version((b'root', b'LCA2'),
1169
[(b'root', b'REV1'), (b'root', b'REV2')], b'fabce')
1170
plan = self.plan_merge_vf.plan_lca_merge(b'LCA1', b'LCA2')
1171
self.assertEqual([('new-b', b'f\n'),
1172
('unchanged', b'a\n'),
1173
('unchanged', b'b\n'),
1174
('unchanged', b'c\n'),
1175
('conflicted-a', b'd\n'),
1176
('conflicted-b', b'e\n'),
1179
def test_plan_lca_merge_with_null(self):
1180
self.add_version((b'root', b'A'), [], b'ab')
1181
self.add_version((b'root', b'B'), [], b'bc')
1182
plan = self.plan_merge_vf.plan_lca_merge(b'A', b'B')
1183
self.assertEqual([('new-a', b'a\n'),
1184
('unchanged', b'b\n'),
1188
def test_plan_merge_with_delete_and_change(self):
1189
self.add_rev(b'root', b'C', [], b'a')
1190
self.add_rev(b'root', b'A', [b'C'], b'b')
1191
self.add_rev(b'root', b'B', [b'C'], b'')
1192
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1193
self.assertEqual([('killed-both', b'a\n'),
1197
def test_plan_merge_with_move_and_change(self):
1198
self.add_rev(b'root', b'C', [], b'abcd')
1199
self.add_rev(b'root', b'A', [b'C'], b'acbd')
1200
self.add_rev(b'root', b'B', [b'C'], b'aBcd')
1201
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1202
self.assertEqual([('unchanged', b'a\n'),
1204
('killed-b', b'b\n'),
1206
('killed-a', b'c\n'),
1207
('unchanged', b'd\n'),
1211
class LoggingMerger(object):
1212
# These seem to be the required attributes
1213
requires_base = False
1214
supports_reprocess = False
1215
supports_show_base = False
1216
supports_cherrypick = False
1217
# We intentionally do not define supports_lca_trees
1219
def __init__(self, *args, **kwargs):
1221
self.kwargs = kwargs
1224
class TestMergerBase(TestCaseWithMemoryTransport):
1225
"""Common functionality for Merger tests that don't write to disk."""
1227
def get_builder(self):
1228
builder = self.make_branch_builder('path')
1229
builder.start_series()
1230
self.addCleanup(builder.finish_series)
1233
def setup_simple_graph(self):
1234
"""Create a simple 3-node graph.
1236
:return: A BranchBuilder
1243
builder = self.get_builder()
1244
builder.build_snapshot(None,
1245
[('add', ('', None, 'directory', None))],
1246
revision_id=b'A-id')
1247
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1248
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1251
def setup_criss_cross_graph(self):
1252
"""Create a 5-node graph with a criss-cross.
1254
:return: A BranchBuilder
1261
builder = self.setup_simple_graph()
1262
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1263
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1266
def make_Merger(self, builder, other_revision_id, interesting_files=None):
1267
"""Make a Merger object from a branch builder"""
1268
mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1269
mem_tree.lock_write()
1270
self.addCleanup(mem_tree.unlock)
1271
merger = _mod_merge.Merger.from_revision_ids(
1272
mem_tree, other_revision_id)
1273
merger.set_interesting_files(interesting_files)
1274
merger.merge_type = _mod_merge.Merge3Merger
1278
class TestMergerInMemory(TestMergerBase):
1280
def test_cache_trees_with_revision_ids_None(self):
1281
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1282
original_cache = dict(merger._cached_trees)
1283
merger.cache_trees_with_revision_ids([None])
1284
self.assertEqual(original_cache, merger._cached_trees)
1286
def test_cache_trees_with_revision_ids_no_revision_id(self):
1287
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1288
original_cache = dict(merger._cached_trees)
1289
tree = self.make_branch_and_memory_tree('tree')
1290
merger.cache_trees_with_revision_ids([tree])
1291
self.assertEqual(original_cache, merger._cached_trees)
1293
def test_cache_trees_with_revision_ids_having_revision_id(self):
1294
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1295
original_cache = dict(merger._cached_trees)
1296
tree = merger.this_branch.repository.revision_tree(b'B-id')
1297
original_cache[b'B-id'] = tree
1298
merger.cache_trees_with_revision_ids([tree])
1299
self.assertEqual(original_cache, merger._cached_trees)
1301
def test_find_base(self):
1302
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1303
self.assertEqual(b'A-id', merger.base_rev_id)
1304
self.assertFalse(merger._is_criss_cross)
1305
self.assertIs(None, merger._lca_trees)
1307
def test_find_base_criss_cross(self):
1308
builder = self.setup_criss_cross_graph()
1309
merger = self.make_Merger(builder, b'E-id')
1310
self.assertEqual(b'A-id', merger.base_rev_id)
1311
self.assertTrue(merger._is_criss_cross)
1312
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1313
for t in merger._lca_trees])
1314
# If we swap the order, we should get a different lca order
1315
builder.build_snapshot([b'E-id'], [], revision_id=b'F-id')
1316
merger = self.make_Merger(builder, b'D-id')
1317
self.assertEqual([b'C-id', b'B-id'], [t.get_revision_id()
1318
for t in merger._lca_trees])
1320
def test_find_base_triple_criss_cross(self):
1323
# B C F # F is merged into both branches
1330
builder = self.setup_criss_cross_graph()
1331
builder.build_snapshot([b'A-id'], [], revision_id=b'F-id')
1332
builder.build_snapshot([b'E-id', b'F-id'], [], revision_id=b'H-id')
1333
builder.build_snapshot([b'D-id', b'F-id'], [], revision_id=b'G-id')
1334
merger = self.make_Merger(builder, b'H-id')
1335
self.assertEqual([b'B-id', b'C-id', b'F-id'],
1336
[t.get_revision_id() for t in merger._lca_trees])
1338
def test_find_base_new_root_criss_cross(self):
1344
builder = self.get_builder()
1345
builder.build_snapshot(None,
1346
[('add', ('', None, 'directory', None))],
1347
revision_id=b'A-id')
1348
builder.build_snapshot([],
1349
[('add', ('', None, 'directory', None))],
1350
revision_id=b'B-id')
1351
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'D-id')
1352
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'C-id')
1353
merger = self.make_Merger(builder, b'D-id')
1354
self.assertEqual(b'A-id', merger.base_rev_id)
1355
self.assertTrue(merger._is_criss_cross)
1356
self.assertEqual([b'A-id', b'B-id'], [t.get_revision_id()
1357
for t in merger._lca_trees])
1359
def test_no_criss_cross_passed_to_merge_type(self):
1360
class LCATreesMerger(LoggingMerger):
1361
supports_lca_trees = True
1363
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1364
merger.merge_type = LCATreesMerger
1365
merge_obj = merger.make_merger()
1366
self.assertIsInstance(merge_obj, LCATreesMerger)
1367
self.assertFalse('lca_trees' in merge_obj.kwargs)
1369
def test_criss_cross_passed_to_merge_type(self):
1370
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1371
merger.merge_type = _mod_merge.Merge3Merger
1372
merge_obj = merger.make_merger()
1373
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1374
for t in merger._lca_trees])
1376
def test_criss_cross_not_supported_merge_type(self):
1377
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1378
# We explicitly do not define supports_lca_trees
1379
merger.merge_type = LoggingMerger
1380
merge_obj = merger.make_merger()
1381
self.assertIsInstance(merge_obj, LoggingMerger)
1382
self.assertFalse('lca_trees' in merge_obj.kwargs)
1384
def test_criss_cross_unsupported_merge_type(self):
1385
class UnsupportedLCATreesMerger(LoggingMerger):
1386
supports_lca_trees = False
1388
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1389
merger.merge_type = UnsupportedLCATreesMerger
1390
merge_obj = merger.make_merger()
1391
self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
1392
self.assertFalse('lca_trees' in merge_obj.kwargs)
1395
class TestMergerEntriesLCA(TestMergerBase):
1397
def make_merge_obj(self, builder, other_revision_id,
1398
interesting_files=None):
1399
merger = self.make_Merger(builder, other_revision_id,
1400
interesting_files=interesting_files)
1401
return merger.make_merger()
1403
def test_simple(self):
1404
builder = self.get_builder()
1405
builder.build_snapshot(None,
1406
[('add', (u'', b'a-root-id', 'directory', None)),
1407
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1408
revision_id=b'A-id')
1409
builder.build_snapshot([b'A-id'],
1410
[('modify', ('a', b'a\nb\nC\nc\n'))],
1411
revision_id=b'C-id')
1412
builder.build_snapshot([b'A-id'],
1413
[('modify', ('a', b'a\nB\nb\nc\n'))],
1414
revision_id=b'B-id')
1415
builder.build_snapshot([b'C-id', b'B-id'],
1416
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1417
revision_id=b'E-id')
1418
builder.build_snapshot([b'B-id', b'C-id'],
1419
[('modify', ('a', b'a\nB\nb\nC\nc\n'))],
1420
revision_id=b'D-id', )
1421
merge_obj = self.make_merge_obj(builder, b'E-id')
1423
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1424
for t in merge_obj._lca_trees])
1425
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1426
entries = list(merge_obj._entries_lca())
1428
# (file_id, changed, parents, names, executable)
1429
# BASE, lca1, lca2, OTHER, THIS
1430
root_id = b'a-root-id'
1431
self.assertEqual([(b'a-id', True,
1432
((u'a', [u'a', u'a']), u'a', u'a'),
1433
((root_id, [root_id, root_id]), root_id, root_id),
1434
((u'a', [u'a', u'a']), u'a', u'a'),
1435
((False, [False, False]), False, False)),
1438
def test_not_in_base(self):
1439
# LCAs all have the same last-modified revision for the file, as do
1440
# the tips, but the base has something different
1441
# A base, doesn't have the file
1443
# B C B introduces 'foo', C introduces 'bar'
1445
# D E D and E now both have 'foo' and 'bar'
1447
# F G the files are now in F, G, D and E, but not in A
1450
builder = self.get_builder()
1451
builder.build_snapshot(None,
1452
[('add', (u'', b'a-root-id', 'directory', None))],
1453
revision_id=b'A-id')
1454
builder.build_snapshot([b'A-id'],
1455
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1456
revision_id=b'B-id')
1457
builder.build_snapshot([b'A-id'],
1458
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1459
revision_id=b'C-id')
1460
builder.build_snapshot([b'B-id', b'C-id'],
1461
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1462
revision_id=b'D-id')
1463
builder.build_snapshot([b'C-id', b'B-id'],
1464
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1465
revision_id=b'E-id')
1466
builder.build_snapshot([b'E-id', b'D-id'],
1467
[('modify', (u'bar', b'd\ne\nf\nG\n'))],
1468
revision_id=b'G-id')
1469
builder.build_snapshot([b'D-id', b'E-id'], [], revision_id=b'F-id')
1470
merge_obj = self.make_merge_obj(builder, b'G-id')
1472
self.assertEqual([b'D-id', b'E-id'], [t.get_revision_id()
1473
for t in merge_obj._lca_trees])
1474
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1475
entries = list(merge_obj._entries_lca())
1476
root_id = b'a-root-id'
1477
self.assertEqual([(b'bar-id', True,
1478
((None, [u'bar', u'bar']), u'bar', u'bar'),
1479
((None, [root_id, root_id]), root_id, root_id),
1480
((None, [u'bar', u'bar']), u'bar', u'bar'),
1481
((None, [False, False]), False, False)),
1484
def test_not_in_this(self):
1485
builder = self.get_builder()
1486
builder.build_snapshot(None,
1487
[('add', (u'', b'a-root-id', 'directory', None)),
1488
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1489
revision_id=b'A-id')
1490
builder.build_snapshot([b'A-id'],
1491
[('modify', ('a', b'a\nB\nb\nc\n'))],
1492
revision_id=b'B-id')
1493
builder.build_snapshot([b'A-id'],
1494
[('modify', ('a', b'a\nb\nC\nc\n'))],
1495
revision_id=b'C-id')
1496
builder.build_snapshot([b'C-id', b'B-id'],
1497
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1498
revision_id=b'E-id')
1499
builder.build_snapshot([b'B-id', b'C-id'],
1500
[('unversion', 'a')],
1501
revision_id=b'D-id')
1502
merge_obj = self.make_merge_obj(builder, b'E-id')
1504
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1505
for t in merge_obj._lca_trees])
1506
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1508
entries = list(merge_obj._entries_lca())
1509
root_id = b'a-root-id'
1510
self.assertEqual([(b'a-id', True,
1511
((u'a', [u'a', u'a']), u'a', None),
1512
((root_id, [root_id, root_id]), root_id, None),
1513
((u'a', [u'a', u'a']), u'a', None),
1514
((False, [False, False]), False, None)),
1517
def test_file_not_in_one_lca(self):
1520
# B C # B no file, C introduces a file
1522
# D E # D and E both have the file, unchanged from C
1523
builder = self.get_builder()
1524
builder.build_snapshot(None,
1525
[('add', (u'', b'a-root-id', 'directory', None))],
1526
revision_id=b'A-id')
1527
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1528
builder.build_snapshot([b'A-id'],
1529
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1530
revision_id=b'C-id')
1531
builder.build_snapshot([b'C-id', b'B-id'],
1532
[], revision_id=b'E-id') # Inherited from C
1533
builder.build_snapshot([b'B-id', b'C-id'], # Merged from C
1534
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1535
revision_id=b'D-id')
1536
merge_obj = self.make_merge_obj(builder, b'E-id')
1538
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1539
for t in merge_obj._lca_trees])
1540
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1542
entries = list(merge_obj._entries_lca())
1543
self.assertEqual([], entries)
1545
def test_not_in_other(self):
1546
builder = self.get_builder()
1547
builder.build_snapshot(None,
1548
[('add', (u'', b'a-root-id', 'directory', None)),
1549
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1550
revision_id=b'A-id')
1551
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1552
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1553
builder.build_snapshot(
1555
[('unversion', 'a')], revision_id=b'E-id')
1556
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1557
merge_obj = self.make_merge_obj(builder, b'E-id')
1559
entries = list(merge_obj._entries_lca())
1560
root_id = b'a-root-id'
1561
self.assertEqual([(b'a-id', True,
1562
((u'a', [u'a', u'a']), None, u'a'),
1563
((root_id, [root_id, root_id]), None, root_id),
1564
((u'a', [u'a', u'a']), None, u'a'),
1565
((False, [False, False]), None, False)),
1568
def test_not_in_other_or_lca(self):
1569
# A base, introduces 'foo'
1571
# B C B nothing, C deletes foo
1573
# D E D restores foo (same as B), E leaves it deleted
1575
# A => B, no changes
1576
# A => C, delete foo (C should supersede B)
1577
# C => D, restore foo
1578
# C => E, no changes
1579
# D would then win 'cleanly' and no record would be given
1580
builder = self.get_builder()
1581
builder.build_snapshot(None,
1582
[('add', (u'', b'a-root-id', 'directory', None)),
1583
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1584
revision_id=b'A-id')
1585
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1586
builder.build_snapshot([b'A-id'],
1587
[('unversion', 'foo')], revision_id=b'C-id')
1588
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1589
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1590
merge_obj = self.make_merge_obj(builder, b'E-id')
1592
entries = list(merge_obj._entries_lca())
1593
self.assertEqual([], entries)
1595
def test_not_in_other_mod_in_lca1_not_in_lca2(self):
1596
# A base, introduces 'foo'
1598
# B C B changes 'foo', C deletes foo
1600
# D E D restores foo (same as B), E leaves it deleted (as C)
1602
# A => B, modified foo
1603
# A => C, delete foo, C does not supersede B
1604
# B => D, no changes
1605
# C => D, resolve in favor of B
1606
# B => E, resolve in favor of E
1607
# C => E, no changes
1608
# In this case, we have a conflict of how the changes were resolved. E
1609
# picked C and D picked B, so we should issue a conflict
1610
builder = self.get_builder()
1611
builder.build_snapshot(None,
1612
[('add', (u'', b'a-root-id', 'directory', None)),
1613
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1614
revision_id=b'A-id')
1615
builder.build_snapshot([b'A-id'], [
1616
('modify', ('foo', b'new-content\n'))],
1617
revision_id=b'B-id')
1618
builder.build_snapshot([b'A-id'],
1619
[('unversion', 'foo')],
1620
revision_id=b'C-id')
1621
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1622
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1623
merge_obj = self.make_merge_obj(builder, b'E-id')
1625
entries = list(merge_obj._entries_lca())
1626
root_id = b'a-root-id'
1627
self.assertEqual([(b'foo-id', True,
1628
((u'foo', [u'foo', None]), None, u'foo'),
1629
((root_id, [root_id, None]), None, root_id),
1630
((u'foo', [u'foo', None]), None, 'foo'),
1631
((False, [False, None]), None, False)),
1634
def test_only_in_one_lca(self):
1637
# B C B nothing, C add file
1639
# D E D still has nothing, E removes file
1642
# C => D, removed the file
1644
# C => E, removed the file
1645
# Thus D & E have identical changes, and this is a no-op
1648
# A => C, add file, thus C supersedes B
1649
# w/ C=BASE, D=THIS, E=OTHER we have 'happy convergence'
1650
builder = self.get_builder()
1651
builder.build_snapshot(None,
1652
[('add', (u'', b'a-root-id', 'directory', None))],
1653
revision_id=b'A-id')
1654
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1655
builder.build_snapshot([b'A-id'],
1656
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1657
revision_id=b'C-id')
1658
builder.build_snapshot([b'C-id', b'B-id'],
1659
[('unversion', 'a')],
1660
revision_id=b'E-id')
1661
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1662
merge_obj = self.make_merge_obj(builder, b'E-id')
1664
entries = list(merge_obj._entries_lca())
1665
self.assertEqual([], entries)
1667
def test_only_in_other(self):
1668
builder = self.get_builder()
1669
builder.build_snapshot(None,
1670
[('add', (u'', b'a-root-id', 'directory', None))],
1671
revision_id=b'A-id')
1672
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1673
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1674
builder.build_snapshot([b'C-id', b'B-id'],
1675
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1676
revision_id=b'E-id')
1677
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1678
merge_obj = self.make_merge_obj(builder, b'E-id')
1680
entries = list(merge_obj._entries_lca())
1681
root_id = b'a-root-id'
1682
self.assertEqual([(b'a-id', True,
1683
((None, [None, None]), u'a', None),
1684
((None, [None, None]), root_id, None),
1685
((None, [None, None]), u'a', None),
1686
((None, [None, None]), False, None)),
1689
def test_one_lca_supersedes(self):
1690
# One LCA supersedes the other LCAs last modified value, but the
1691
# value is not the same as BASE.
1692
# A base, introduces 'foo', last mod A
1694
# B C B modifies 'foo' (mod B), C does nothing (mod A)
1696
# D E D does nothing (mod B), E updates 'foo' (mod E)
1698
# F G F updates 'foo' (mod F). G does nothing (mod E)
1700
# At this point, G should not be considered to modify 'foo', even
1701
# though its LCAs disagree. This is because the modification in E
1702
# completely supersedes the value in D.
1703
builder = self.get_builder()
1704
builder.build_snapshot(None,
1705
[('add', (u'', b'a-root-id', 'directory', None)),
1706
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1707
revision_id=b'A-id')
1708
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1709
builder.build_snapshot([b'A-id'],
1710
[('modify', ('foo', b'B content\n'))],
1711
revision_id=b'B-id')
1712
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1713
builder.build_snapshot([b'C-id', b'B-id'],
1714
[('modify', ('foo', b'E content\n'))],
1715
revision_id=b'E-id')
1716
builder.build_snapshot([b'E-id', b'D-id'], [], revision_id=b'G-id')
1717
builder.build_snapshot([b'D-id', b'E-id'],
1718
[('modify', ('foo', b'F content\n'))],
1719
revision_id=b'F-id')
1720
merge_obj = self.make_merge_obj(builder, b'G-id')
1722
self.assertEqual([], list(merge_obj._entries_lca()))
1724
def test_one_lca_supersedes_path(self):
1725
# Double-criss-cross merge, the ultimate base value is different from
1729
# B C B value 'bar', C = 'foo'
1731
# D E D = 'bar', E supersedes to 'bing'
1733
# F G F = 'bing', G supersedes to 'barry'
1735
# In this case, we technically should not care about the value 'bar' for
1736
# D, because it was clearly superseded by E's 'bing'. The
1737
# per-file/attribute graph would actually look like:
1746
# Because the other side of the merge never modifies the value, it just
1747
# takes the value from the merge.
1749
# ATM this fails because we will prune 'foo' from the LCAs, but we
1750
# won't prune 'bar'. This is getting far off into edge-case land, so we
1751
# aren't supporting it yet.
1753
builder = self.get_builder()
1754
builder.build_snapshot(None,
1755
[('add', (u'', b'a-root-id', 'directory', None)),
1756
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1757
revision_id=b'A-id')
1758
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1759
builder.build_snapshot([b'A-id'],
1760
[('rename', ('foo', 'bar'))],
1761
revision_id=b'B-id')
1762
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1763
builder.build_snapshot([b'C-id', b'B-id'],
1764
[('rename', ('foo', 'bing'))],
1765
revision_id=b'E-id') # override to bing
1766
builder.build_snapshot([b'E-id', b'D-id'],
1767
[('rename', ('bing', 'barry'))],
1768
revision_id=b'G-id') # override to barry
1769
builder.build_snapshot([b'D-id', b'E-id'],
1770
[('rename', ('bar', 'bing'))],
1771
revision_id=b'F-id') # Merge in E's change
1772
merge_obj = self.make_merge_obj(builder, b'G-id')
1774
self.expectFailure("We don't do an actual heads() check on lca values,"
1775
" or use the per-attribute graph",
1776
self.assertEqual, [], list(merge_obj._entries_lca()))
1778
def test_one_lca_accidentally_pruned(self):
1779
# Another incorrect resolution from the same basic flaw:
1782
# B C B value 'bar', C = 'foo'
1784
# D E D = 'bar', E reverts to 'foo'
1786
# F G F = 'bing', G switches to 'bar'
1788
# 'bar' will not be seen as an interesting change, because 'foo' will
1789
# be pruned from the LCAs, even though it was newly introduced by E
1791
builder = self.get_builder()
1792
builder.build_snapshot(None,
1793
[('add', (u'', b'a-root-id', 'directory', None)),
1794
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1795
revision_id=b'A-id')
1796
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1797
builder.build_snapshot([b'A-id'],
1798
[('rename', ('foo', 'bar'))],
1799
revision_id=b'B-id')
1800
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1801
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1802
builder.build_snapshot([b'E-id', b'D-id'],
1803
[('rename', ('foo', 'bar'))],
1804
revision_id=b'G-id')
1805
builder.build_snapshot([b'D-id', b'E-id'],
1806
[('rename', ('bar', 'bing'))],
1807
revision_id=b'F-id') # should end up conflicting
1808
merge_obj = self.make_merge_obj(builder, b'G-id')
1810
entries = list(merge_obj._entries_lca())
1811
root_id = b'a-root-id'
1812
self.expectFailure("We prune values from BASE even when relevant.",
1815
((root_id, [root_id, root_id]), root_id, root_id),
1816
((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
1817
((False, [False, False]), False, False)),
1820
def test_both_sides_revert(self):
1821
# Both sides of a criss-cross revert the text to the lca
1822
# A base, introduces 'foo'
1824
# B C B modifies 'foo', C modifies 'foo'
1826
# D E D reverts to B, E reverts to C
1827
# This should conflict
1828
builder = self.get_builder()
1829
builder.build_snapshot(None,
1830
[('add', (u'', b'a-root-id', 'directory', None)),
1831
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1832
revision_id=b'A-id')
1833
builder.build_snapshot([b'A-id'],
1834
[('modify', ('foo', b'B content\n'))],
1835
revision_id=b'B-id')
1836
builder.build_snapshot([b'A-id'],
1837
[('modify', ('foo', b'C content\n'))],
1838
revision_id=b'C-id')
1839
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1840
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1841
merge_obj = self.make_merge_obj(builder, b'E-id')
1843
entries = list(merge_obj._entries_lca())
1844
root_id = b'a-root-id'
1845
self.assertEqual([(b'foo-id', True,
1846
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1847
((root_id, [root_id, root_id]), root_id, root_id),
1848
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1849
((False, [False, False]), False, False)),
1852
def test_different_lca_resolve_one_side_updates_content(self):
1853
# Both sides converge, but then one side updates the text.
1854
# A base, introduces 'foo'
1856
# B C B modifies 'foo', C modifies 'foo'
1858
# D E D reverts to B, E reverts to C
1860
# F F updates to a new value
1861
# We need to emit an entry for 'foo', because D & E differed on the
1863
builder = self.get_builder()
1864
builder.build_snapshot(None,
1865
[('add', (u'', b'a-root-id', 'directory', None)),
1866
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1867
revision_id=b'A-id')
1868
builder.build_snapshot([b'A-id'],
1869
[('modify', ('foo', b'B content\n'))],
1870
revision_id=b'B-id')
1871
builder.build_snapshot([b'A-id'],
1872
[('modify', ('foo', b'C content\n'))],
1873
revision_id=b'C-id', )
1874
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1875
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1876
builder.build_snapshot([b'D-id'],
1877
[('modify', ('foo', b'F content\n'))],
1878
revision_id=b'F-id')
1879
merge_obj = self.make_merge_obj(builder, b'E-id')
1881
entries = list(merge_obj._entries_lca())
1882
root_id = b'a-root-id'
1883
self.assertEqual([(b'foo-id', True,
1884
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1885
((root_id, [root_id, root_id]), root_id, root_id),
1886
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1887
((False, [False, False]), False, False)),
1890
def test_same_lca_resolution_one_side_updates_content(self):
1891
# Both sides converge, but then one side updates the text.
1892
# A base, introduces 'foo'
1894
# B C B modifies 'foo', C modifies 'foo'
1896
# D E D and E use C's value
1898
# F F updates to a new value
1899
# I think it is a bug that this conflicts, but we don't have a way to
1900
# detect otherwise. And because of:
1901
# test_different_lca_resolve_one_side_updates_content
1902
# We need to conflict.
1904
builder = self.get_builder()
1905
builder.build_snapshot(None,
1906
[('add', (u'', b'a-root-id', 'directory', None)),
1907
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1908
revision_id=b'A-id')
1909
builder.build_snapshot([b'A-id'],
1910
[('modify', ('foo', b'B content\n'))],
1911
revision_id=b'B-id')
1912
builder.build_snapshot([b'A-id'],
1913
[('modify', ('foo', b'C content\n'))],
1914
revision_id=b'C-id')
1915
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1916
builder.build_snapshot([b'B-id', b'C-id'],
1917
[('modify', ('foo', b'C content\n'))],
1918
revision_id=b'D-id') # Same as E
1919
builder.build_snapshot([b'D-id'],
1920
[('modify', ('foo', b'F content\n'))],
1921
revision_id=b'F-id')
1922
merge_obj = self.make_merge_obj(builder, b'E-id')
1924
entries = list(merge_obj._entries_lca())
1925
self.expectFailure("We don't detect that LCA resolution was the"
1926
" same on both sides",
1927
self.assertEqual, [], entries)
1929
def test_only_path_changed(self):
1930
builder = self.get_builder()
1931
builder.build_snapshot(None,
1932
[('add', (u'', b'a-root-id', 'directory', None)),
1933
('add', (u'a', b'a-id', 'file', b'content\n'))],
1934
revision_id=b'A-id')
1935
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1936
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1937
builder.build_snapshot([b'C-id', b'B-id'],
1938
[('rename', (u'a', u'b'))],
1939
revision_id=b'E-id')
1940
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1941
merge_obj = self.make_merge_obj(builder, b'E-id')
1942
entries = list(merge_obj._entries_lca())
1943
root_id = b'a-root-id'
1944
# The content was not changed, only the path
1945
self.assertEqual([(b'a-id', False,
1946
((u'a', [u'a', u'a']), u'b', u'a'),
1947
((root_id, [root_id, root_id]), root_id, root_id),
1948
((u'a', [u'a', u'a']), u'b', u'a'),
1949
((False, [False, False]), False, False)),
1952
def test_kind_changed(self):
1953
# Identical content, except 'D' changes a-id into a directory
1954
builder = self.get_builder()
1955
builder.build_snapshot(None,
1956
[('add', (u'', b'a-root-id', 'directory', None)),
1957
('add', (u'a', b'a-id', 'file', b'content\n'))],
1958
revision_id=b'A-id')
1959
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1960
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1961
builder.build_snapshot([b'C-id', b'B-id'],
1962
[('unversion', 'a'),
1964
('add', (u'a', b'a-id', 'directory', None))],
1965
revision_id=b'E-id')
1966
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1967
merge_obj = self.make_merge_obj(builder, b'E-id')
1968
entries = list(merge_obj._entries_lca())
1969
root_id = b'a-root-id'
1970
# Only the kind was changed (content)
1971
self.assertEqual([(b'a-id', True,
1972
((u'a', [u'a', u'a']), u'a', u'a'),
1973
((root_id, [root_id, root_id]), root_id, root_id),
1974
((u'a', [u'a', u'a']), u'a', u'a'),
1975
((False, [False, False]), False, False)),
1978
def test_this_changed_kind(self):
1979
# Identical content, but THIS changes a file to a directory
1980
builder = self.get_builder()
1981
builder.build_snapshot(None,
1982
[('add', (u'', b'a-root-id', 'directory', None)),
1983
('add', (u'a', b'a-id', 'file', b'content\n'))],
1984
revision_id=b'A-id')
1985
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1986
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1987
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1988
builder.build_snapshot([b'B-id', b'C-id'],
1989
[('unversion', 'a'),
1991
('add', (u'a', b'a-id', 'directory', None))],
1992
revision_id=b'D-id')
1993
merge_obj = self.make_merge_obj(builder, b'E-id')
1994
entries = list(merge_obj._entries_lca())
1995
# Only the kind was changed (content)
1996
self.assertEqual([], entries)
1998
def test_interesting_files(self):
1999
# Two files modified, but we should filter one of them
2000
builder = self.get_builder()
2001
builder.build_snapshot(None,
2002
[('add', (u'', b'a-root-id', 'directory', None)),
2003
('add', (u'a', b'a-id', 'file', b'content\n')),
2004
('add', (u'b', b'b-id', 'file', b'content\n'))],
2005
revision_id=b'A-id')
2006
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2007
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2008
builder.build_snapshot([b'C-id', b'B-id'],
2009
[('modify', ('a', b'new-content\n')),
2010
('modify', ('b', b'new-content\n'))],
2011
revision_id=b'E-id')
2012
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2013
merge_obj = self.make_merge_obj(builder, b'E-id',
2014
interesting_files=['b'])
2015
entries = list(merge_obj._entries_lca())
2016
root_id = b'a-root-id'
2017
self.assertEqual([(b'b-id', True,
2018
((u'b', [u'b', u'b']), u'b', u'b'),
2019
((root_id, [root_id, root_id]), root_id, root_id),
2020
((u'b', [u'b', u'b']), u'b', u'b'),
2021
((False, [False, False]), False, False)),
2024
def test_interesting_file_in_this(self):
2025
# This renamed the file, but it should still match the entry in other
2026
builder = self.get_builder()
2027
builder.build_snapshot(None,
2028
[('add', (u'', b'a-root-id', 'directory', None)),
2029
('add', (u'a', b'a-id', 'file', b'content\n')),
2030
('add', (u'b', b'b-id', 'file', b'content\n'))],
2031
revision_id=b'A-id')
2032
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2033
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2034
builder.build_snapshot([b'C-id', b'B-id'],
2035
[('modify', ('a', b'new-content\n')),
2036
('modify', ('b', b'new-content\n'))],
2037
revision_id=b'E-id')
2038
builder.build_snapshot([b'B-id', b'C-id'],
2039
[('rename', ('b', 'c'))],
2040
revision_id=b'D-id')
2041
merge_obj = self.make_merge_obj(builder, b'E-id',
2042
interesting_files=['c'])
2043
entries = list(merge_obj._entries_lca())
2044
root_id = b'a-root-id'
2045
self.assertEqual([(b'b-id', True,
2046
((u'b', [u'b', u'b']), u'b', u'c'),
2047
((root_id, [root_id, root_id]), root_id, root_id),
2048
((u'b', [u'b', u'b']), u'b', u'c'),
2049
((False, [False, False]), False, False)),
2052
def test_interesting_file_in_base(self):
2053
# This renamed the file, but it should still match the entry in BASE
2054
builder = self.get_builder()
2055
builder.build_snapshot(None,
2056
[('add', (u'', b'a-root-id', 'directory', None)),
2057
('add', (u'a', b'a-id', 'file', b'content\n')),
2058
('add', (u'c', b'c-id', 'file', b'content\n'))],
2059
revision_id=b'A-id')
2060
builder.build_snapshot([b'A-id'],
2061
[('rename', ('c', 'b'))],
2062
revision_id=b'B-id')
2063
builder.build_snapshot([b'A-id'],
2064
[('rename', ('c', 'b'))],
2065
revision_id=b'C-id')
2066
builder.build_snapshot([b'C-id', b'B-id'],
2067
[('modify', ('a', b'new-content\n')),
2068
('modify', ('b', b'new-content\n'))],
2069
revision_id=b'E-id')
2070
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2071
merge_obj = self.make_merge_obj(builder, b'E-id',
2072
interesting_files=['c'])
2073
entries = list(merge_obj._entries_lca())
2074
root_id = b'a-root-id'
2075
self.assertEqual([(b'c-id', True,
2076
((u'c', [u'b', u'b']), u'b', u'b'),
2077
((root_id, [root_id, root_id]), root_id, root_id),
2078
((u'c', [u'b', u'b']), u'b', u'b'),
2079
((False, [False, False]), False, False)),
2082
def test_interesting_file_in_lca(self):
2083
# This renamed the file, but it should still match the entry in LCA
2084
builder = self.get_builder()
2085
builder.build_snapshot(None,
2086
[('add', (u'', b'a-root-id', 'directory', None)),
2087
('add', (u'a', b'a-id', 'file', b'content\n')),
2088
('add', (u'b', b'b-id', 'file', b'content\n'))],
2089
revision_id=b'A-id')
2090
builder.build_snapshot([b'A-id'],
2091
[('rename', ('b', 'c'))], revision_id=b'B-id')
2092
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2093
builder.build_snapshot([b'C-id', b'B-id'],
2094
[('modify', ('a', b'new-content\n')),
2095
('modify', ('b', b'new-content\n'))],
2096
revision_id=b'E-id')
2097
builder.build_snapshot([b'B-id', b'C-id'],
2098
[('rename', ('c', 'b'))], revision_id=b'D-id')
2099
merge_obj = self.make_merge_obj(builder, b'E-id',
2100
interesting_files=['c'])
2101
entries = list(merge_obj._entries_lca())
2102
root_id = b'a-root-id'
2103
self.assertEqual([(b'b-id', True,
2104
((u'b', [u'c', u'b']), u'b', u'b'),
2105
((root_id, [root_id, root_id]), root_id, root_id),
2106
((u'b', [u'c', u'b']), u'b', u'b'),
2107
((False, [False, False]), False, False)),
2110
def test_interesting_files(self):
2111
# Two files modified, but we should filter one of them
2112
builder = self.get_builder()
2113
builder.build_snapshot(None,
2114
[('add', (u'', b'a-root-id', 'directory', None)),
2115
('add', (u'a', b'a-id', 'file', b'content\n')),
2116
('add', (u'b', b'b-id', 'file', b'content\n'))],
2117
revision_id=b'A-id')
2118
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2119
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2120
builder.build_snapshot([b'C-id', b'B-id'],
2121
[('modify', ('a', b'new-content\n')),
2122
('modify', ('b', b'new-content\n'))], revision_id=b'E-id')
2123
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2124
merge_obj = self.make_merge_obj(builder, b'E-id',
2125
interesting_files=['b'])
2126
entries = list(merge_obj._entries_lca())
2127
root_id = b'a-root-id'
2128
self.assertEqual([(b'b-id', True,
2129
((u'b', [u'b', u'b']), u'b', u'b'),
2130
((root_id, [root_id, root_id]), root_id, root_id),
2131
((u'b', [u'b', u'b']), u'b', u'b'),
2132
((False, [False, False]), False, False)),
2136
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
2138
def get_builder(self):
2139
builder = self.make_branch_builder('path')
2140
builder.start_series()
2141
self.addCleanup(builder.finish_series)
2144
def get_wt_from_builder(self, builder):
2145
"""Get a real WorkingTree from the builder."""
2146
the_branch = builder.get_branch()
2147
wt = the_branch.controldir.create_workingtree()
2148
# Note: This is a little bit ugly, but we are holding the branch
2149
# write-locked as part of the build process, and we would like to
2150
# maintain that. So we just force the WT to re-use the same
2152
wt._branch = the_branch
2154
self.addCleanup(wt.unlock)
2157
def do_merge(self, builder, other_revision_id):
2158
wt = self.get_wt_from_builder(builder)
2159
merger = _mod_merge.Merger.from_revision_ids(
2160
wt, other_revision_id)
2161
merger.merge_type = _mod_merge.Merge3Merger
2162
return wt, merger.do_merge()
2164
def test_simple_lca(self):
2165
builder = self.get_builder()
2166
builder.build_snapshot(None,
2167
[('add', (u'', b'a-root-id', 'directory', None)),
2168
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
2169
revision_id=b'A-id')
2170
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2171
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2172
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2173
builder.build_snapshot([b'B-id', b'C-id'],
2174
[('modify', ('a', b'a\nb\nc\nd\ne\nf\n'))],
2175
revision_id=b'D-id')
2176
wt, conflicts = self.do_merge(builder, b'E-id')
2177
self.assertEqual(0, conflicts)
2178
# The merge should have simply update the contents of 'a'
2179
self.assertEqual(b'a\nb\nc\nd\ne\nf\n', wt.get_file_text('a'))
2181
def test_conflict_without_lca(self):
2182
# This test would cause a merge conflict, unless we use the lca trees
2183
# to determine the real ancestry
2186
# B C Path renamed to 'bar' in B
2190
# D E Path at 'bar' in D and E
2192
# F Path at 'baz' in F, which supersedes 'bar' and 'foo'
2193
builder = self.get_builder()
2194
builder.build_snapshot(None,
2195
[('add', (u'', b'a-root-id', 'directory', None)),
2196
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2197
revision_id=b'A-id')
2198
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2199
builder.build_snapshot([b'A-id'],
2200
[('rename', ('foo', 'bar'))], revision_id=b'B-id', )
2201
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2202
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2203
builder.build_snapshot([b'E-id'],
2204
[('rename', ('bar', 'baz'))], revision_id=b'F-id')
2205
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2206
wt, conflicts = self.do_merge(builder, b'F-id')
2207
self.assertEqual(0, conflicts)
2208
# The merge should simply recognize that the final rename takes
2210
self.assertEqual('baz', wt.id2path(b'foo-id'))
2212
def test_other_deletes_lca_renames(self):
2213
# This test would cause a merge conflict, unless we use the lca trees
2214
# to determine the real ancestry
2217
# B C Path renamed to 'bar' in B
2221
# D E Path at 'bar' in D and E
2224
builder = self.get_builder()
2225
builder.build_snapshot(None,
2226
[('add', (u'', b'a-root-id', 'directory', None)),
2227
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2228
revision_id=b'A-id')
2229
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2230
builder.build_snapshot([b'A-id'],
2231
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2232
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2233
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2234
builder.build_snapshot([b'E-id'],
2235
[('unversion', 'bar')], revision_id=b'F-id')
2236
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2237
wt, conflicts = self.do_merge(builder, b'F-id')
2238
self.assertEqual(0, conflicts)
2239
self.assertRaises(errors.NoSuchId, wt.id2path, b'foo-id')
2241
def test_executable_changes(self):
2250
# F Executable bit changed
2251
builder = self.get_builder()
2252
builder.build_snapshot(None,
2253
[('add', (u'', b'a-root-id', 'directory', None)),
2254
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2255
revision_id=b'A-id')
2256
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2257
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2258
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2259
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2260
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2261
wt = self.get_wt_from_builder(builder)
2262
with wt.get_transform() as tt:
2263
tt.set_executability(True, tt.trans_id_tree_path('foo'))
2265
self.assertTrue(wt.is_executable('foo'))
2266
wt.commit('F-id', rev_id=b'F-id')
2267
# Reset to D, so that we can merge F
2268
wt.set_parent_ids([b'D-id'])
2269
wt.branch.set_last_revision_info(3, b'D-id')
2271
self.assertFalse(wt.is_executable('foo'))
2272
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2273
self.assertEqual(0, conflicts)
2274
self.assertTrue(wt.is_executable('foo'))
2276
def test_create_symlink(self):
2277
self.requireFeature(features.SymlinkFeature)
2286
# F Add a symlink 'foo' => 'bar'
2287
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2288
# have symlink support
2289
builder = self.get_builder()
2290
builder.build_snapshot(None,
2291
[('add', (u'', b'a-root-id', 'directory', None))],
2292
revision_id=b'A-id')
2293
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2294
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2295
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2296
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2297
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2298
wt = self.get_wt_from_builder(builder)
2299
os.symlink('bar', 'path/foo')
2300
wt.add(['foo'], [b'foo-id'])
2301
self.assertEqual('bar', wt.get_symlink_target('foo'))
2302
wt.commit('add symlink', rev_id=b'F-id')
2303
# Reset to D, so that we can merge F
2304
wt.set_parent_ids([b'D-id'])
2305
wt.branch.set_last_revision_info(3, b'D-id')
2307
self.assertFalse(wt.is_versioned('foo'))
2308
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2309
self.assertEqual(0, conflicts)
2310
self.assertEqual(b'foo-id', wt.path2id('foo'))
2311
self.assertEqual('bar', wt.get_symlink_target('foo'))
2313
def test_both_sides_revert(self):
2314
# Both sides of a criss-cross revert the text to the lca
2315
# A base, introduces 'foo'
2317
# B C B modifies 'foo', C modifies 'foo'
2319
# D E D reverts to B, E reverts to C
2320
# This should conflict
2321
# This must be done with a real WorkingTree, because normally their
2322
# inventory contains "None" rather than a real sha1
2323
builder = self.get_builder()
2324
builder.build_snapshot(None,
2325
[('add', (u'', b'a-root-id', 'directory', None)),
2326
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
2327
revision_id=b'A-id')
2328
builder.build_snapshot([b'A-id'],
2329
[('modify', ('foo', b'B content\n'))],
2330
revision_id=b'B-id')
2331
builder.build_snapshot([b'A-id'],
2332
[('modify', ('foo', b'C content\n'))],
2333
revision_id=b'C-id')
2334
builder.build_snapshot([b'C-id', b'B-id'], [],
2335
revision_id=b'E-id')
2336
builder.build_snapshot([b'B-id', b'C-id'], [],
2337
revision_id=b'D-id')
2338
wt, conflicts = self.do_merge(builder, b'E-id')
2339
self.assertEqual(1, conflicts)
2340
self.assertEqualDiff(b'<<<<<<< TREE\n'
2344
b'>>>>>>> MERGE-SOURCE\n',
2345
wt.get_file_text('foo'))
2347
def test_modified_symlink(self):
2348
self.requireFeature(features.SymlinkFeature)
2349
# A Create symlink foo => bar
2351
# B C B relinks foo => baz
2355
# D E D & E have foo => baz
2357
# F F changes it to bing
2359
# Merging D & F should result in F cleanly overriding D, because D's
2360
# value actually comes from B
2362
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2363
# have symlink support
2364
wt = self.make_branch_and_tree('path')
2366
self.addCleanup(wt.unlock)
2367
os.symlink('bar', 'path/foo')
2368
wt.add(['foo'], [b'foo-id'])
2369
wt.commit('add symlink', rev_id=b'A-id')
2370
os.remove('path/foo')
2371
os.symlink('baz', 'path/foo')
2372
wt.commit('foo => baz', rev_id=b'B-id')
2373
wt.set_last_revision(b'A-id')
2374
wt.branch.set_last_revision_info(1, b'A-id')
2376
wt.commit('C', rev_id=b'C-id')
2377
wt.merge_from_branch(wt.branch, b'B-id')
2378
self.assertEqual('baz', wt.get_symlink_target('foo'))
2379
wt.commit('E merges C & B', rev_id=b'E-id')
2380
os.remove('path/foo')
2381
os.symlink('bing', 'path/foo')
2382
wt.commit('F foo => bing', rev_id=b'F-id')
2383
wt.set_last_revision(b'B-id')
2384
wt.branch.set_last_revision_info(2, b'B-id')
2386
wt.merge_from_branch(wt.branch, b'C-id')
2387
wt.commit('D merges B & C', rev_id=b'D-id')
2388
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2389
self.assertEqual(0, conflicts)
2390
self.assertEqual('bing', wt.get_symlink_target('foo'))
2392
def test_renamed_symlink(self):
2393
self.requireFeature(features.SymlinkFeature)
2394
# A Create symlink foo => bar
2396
# B C B renames foo => barry
2400
# D E D & E have barry
2402
# F F renames barry to blah
2404
# Merging D & F should result in F cleanly overriding D, because D's
2405
# value actually comes from B
2407
wt = self.make_branch_and_tree('path')
2409
self.addCleanup(wt.unlock)
2410
os.symlink('bar', 'path/foo')
2411
wt.add(['foo'], [b'foo-id'])
2412
wt.commit('A add symlink', rev_id=b'A-id')
2413
wt.rename_one('foo', 'barry')
2414
wt.commit('B foo => barry', rev_id=b'B-id')
2415
wt.set_last_revision(b'A-id')
2416
wt.branch.set_last_revision_info(1, b'A-id')
2418
wt.commit('C', rev_id=b'C-id')
2419
wt.merge_from_branch(wt.branch, b'B-id')
2420
self.assertEqual('barry', wt.id2path(b'foo-id'))
2421
self.assertEqual('bar', wt.get_symlink_target('barry'))
2422
wt.commit('E merges C & B', rev_id=b'E-id')
2423
wt.rename_one('barry', 'blah')
2424
wt.commit('F barry => blah', rev_id=b'F-id')
2425
wt.set_last_revision(b'B-id')
2426
wt.branch.set_last_revision_info(2, b'B-id')
2428
wt.merge_from_branch(wt.branch, b'C-id')
2429
wt.commit('D merges B & C', rev_id=b'D-id')
2430
self.assertEqual('barry', wt.id2path(b'foo-id'))
2431
# Check the output of the Merger object directly
2432
merger = _mod_merge.Merger.from_revision_ids(wt, b'F-id')
2433
merger.merge_type = _mod_merge.Merge3Merger
2434
merge_obj = merger.make_merger()
2435
root_id = wt.path2id('')
2436
entries = list(merge_obj._entries_lca())
2437
# No content change, just a path change
2438
self.assertEqual([(b'foo-id', False,
2439
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2440
((root_id, [root_id, root_id]), root_id, root_id),
2441
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2442
((False, [False, False]), False, False)),
2444
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2445
self.assertEqual(0, conflicts)
2446
self.assertEqual('blah', wt.id2path(b'foo-id'))
2448
def test_symlink_no_content_change(self):
2449
self.requireFeature(features.SymlinkFeature)
2450
# A Create symlink foo => bar
2452
# B C B relinks foo => baz
2456
# D E D & E have foo => baz
2458
# F F has foo => bing
2460
# Merging E into F should not cause a conflict, because E doesn't have
2461
# a content change relative to the LCAs (it does relative to A)
2462
wt = self.make_branch_and_tree('path')
2464
self.addCleanup(wt.unlock)
2465
os.symlink('bar', 'path/foo')
2466
wt.add(['foo'], [b'foo-id'])
2467
wt.commit('add symlink', rev_id=b'A-id')
2468
os.remove('path/foo')
2469
os.symlink('baz', 'path/foo')
2470
wt.commit('foo => baz', rev_id=b'B-id')
2471
wt.set_last_revision(b'A-id')
2472
wt.branch.set_last_revision_info(1, b'A-id')
2474
wt.commit('C', rev_id=b'C-id')
2475
wt.merge_from_branch(wt.branch, b'B-id')
2476
self.assertEqual('baz', wt.get_symlink_target('foo'))
2477
wt.commit('E merges C & B', rev_id=b'E-id')
2478
wt.set_last_revision(b'B-id')
2479
wt.branch.set_last_revision_info(2, b'B-id')
2481
wt.merge_from_branch(wt.branch, b'C-id')
2482
wt.commit('D merges B & C', rev_id=b'D-id')
2483
os.remove('path/foo')
2484
os.symlink('bing', 'path/foo')
2485
wt.commit('F foo => bing', rev_id=b'F-id')
2487
# Check the output of the Merger object directly
2488
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2489
merger.merge_type = _mod_merge.Merge3Merger
2490
merge_obj = merger.make_merger()
2491
# Nothing interesting happened in OTHER relative to BASE
2492
self.assertEqual([], list(merge_obj._entries_lca()))
2493
# Now do a real merge, just to test the rest of the stack
2494
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'E-id')
2495
self.assertEqual(0, conflicts)
2496
self.assertEqual('bing', wt.get_symlink_target('foo'))
2498
def test_symlink_this_changed_kind(self):
2499
self.requireFeature(features.SymlinkFeature)
2502
# B C B creates symlink foo => bar
2506
# D E D changes foo into a file, E has foo => bing
2508
# Mostly, this is trying to test that we don't try to os.readlink() on
2509
# a file, or when there is nothing there
2510
wt = self.make_branch_and_tree('path')
2512
self.addCleanup(wt.unlock)
2513
wt.commit('base', rev_id=b'A-id')
2514
os.symlink('bar', 'path/foo')
2515
wt.add(['foo'], [b'foo-id'])
2516
wt.commit('add symlink foo => bar', rev_id=b'B-id')
2517
wt.set_last_revision(b'A-id')
2518
wt.branch.set_last_revision_info(1, b'A-id')
2520
wt.commit('C', rev_id=b'C-id')
2521
wt.merge_from_branch(wt.branch, b'B-id')
2522
self.assertEqual('bar', wt.get_symlink_target('foo'))
2523
os.remove('path/foo')
2524
# We have to change the link in E, or it won't try to do a comparison
2525
os.symlink('bing', 'path/foo')
2526
wt.commit('E merges C & B, overrides to bing', rev_id=b'E-id')
2527
wt.set_last_revision(b'B-id')
2528
wt.branch.set_last_revision_info(2, b'B-id')
2530
wt.merge_from_branch(wt.branch, b'C-id')
2531
os.remove('path/foo')
2532
self.build_tree_contents([('path/foo', b'file content\n')])
2533
# XXX: workaround, WT doesn't detect kind changes unless you do
2535
list(wt.iter_changes(wt.basis_tree()))
2536
wt.commit('D merges B & C, makes it a file', rev_id=b'D-id')
2538
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2539
merger.merge_type = _mod_merge.Merge3Merger
2540
merge_obj = merger.make_merger()
2541
entries = list(merge_obj._entries_lca())
2542
root_id = wt.path2id('')
2543
self.assertEqual([(b'foo-id', True,
2544
((None, [u'foo', None]), u'foo', u'foo'),
2545
((None, [root_id, None]), root_id, root_id),
2546
((None, [u'foo', None]), u'foo', u'foo'),
2547
((None, [False, None]), False, False)),
2550
def test_symlink_all_wt(self):
2551
"""Check behavior if all trees are Working Trees."""
2552
self.requireFeature(features.SymlinkFeature)
2553
# The big issue is that entry.symlink_target is None for WorkingTrees.
2554
# So we need to make sure we handle that case correctly.
2557
# B C B relinks foo => baz
2559
# D E D & E have foo => baz
2561
# F F changes it to bing
2562
# Merging D & F should result in F cleanly overriding D, because D's
2563
# value actually comes from B
2565
wt = self.make_branch_and_tree('path')
2567
self.addCleanup(wt.unlock)
2568
os.symlink('bar', 'path/foo')
2569
wt.add(['foo'], [b'foo-id'])
2570
wt.commit('add symlink', rev_id=b'A-id')
2571
os.remove('path/foo')
2572
os.symlink('baz', 'path/foo')
2573
wt.commit('foo => baz', rev_id=b'B-id')
2574
wt.set_last_revision(b'A-id')
2575
wt.branch.set_last_revision_info(1, b'A-id')
2577
wt.commit('C', rev_id=b'C-id')
2578
wt.merge_from_branch(wt.branch, b'B-id')
2579
self.assertEqual('baz', wt.get_symlink_target('foo'))
2580
wt.commit('E merges C & B', rev_id=b'E-id')
2581
os.remove('path/foo')
2582
os.symlink('bing', 'path/foo')
2583
wt.commit('F foo => bing', rev_id=b'F-id')
2584
wt.set_last_revision(b'B-id')
2585
wt.branch.set_last_revision_info(2, b'B-id')
2587
wt.merge_from_branch(wt.branch, b'C-id')
2588
wt.commit('D merges B & C', rev_id=b'D-id')
2589
wt_base = wt.controldir.sprout('base', b'A-id').open_workingtree()
2591
self.addCleanup(wt_base.unlock)
2592
wt_lca1 = wt.controldir.sprout('b-tree', b'B-id').open_workingtree()
2594
self.addCleanup(wt_lca1.unlock)
2595
wt_lca2 = wt.controldir.sprout('c-tree', b'C-id').open_workingtree()
2597
self.addCleanup(wt_lca2.unlock)
2598
wt_other = wt.controldir.sprout('other', b'F-id').open_workingtree()
2599
wt_other.lock_read()
2600
self.addCleanup(wt_other.unlock)
2601
merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
2602
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2603
entries = list(merge_obj._entries_lca())
2604
root_id = wt.path2id('')
2605
self.assertEqual([(b'foo-id', True,
2606
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2607
((root_id, [root_id, root_id]), root_id, root_id),
2608
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2609
((False, [False, False]), False, False)),
2612
def test_other_reverted_path_to_base(self):
2615
# B C Path at 'bar' in B
2622
builder = self.get_builder()
2623
builder.build_snapshot(None,
2624
[('add', (u'', b'a-root-id', 'directory', None)),
2625
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2626
revision_id=b'A-id')
2627
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2628
builder.build_snapshot([b'A-id'],
2629
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2630
builder.build_snapshot([b'C-id', b'B-id'],
2631
[('rename', ('foo', 'bar'))], revision_id=b'E-id') # merge the rename
2632
builder.build_snapshot([b'E-id'],
2633
[('rename', ('bar', 'foo'))], revision_id=b'F-id') # Rename back to BASE
2634
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2635
wt, conflicts = self.do_merge(builder, b'F-id')
2636
self.assertEqual(0, conflicts)
2637
self.assertEqual('foo', wt.id2path(b'foo-id'))
2639
def test_other_reverted_content_to_base(self):
2640
builder = self.get_builder()
2641
builder.build_snapshot(None,
2642
[('add', (u'', b'a-root-id', 'directory', None)),
2643
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2644
revision_id=b'A-id')
2645
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2646
builder.build_snapshot([b'A-id'],
2647
[('modify', ('foo', b'B content\n'))],
2648
revision_id=b'B-id')
2649
builder.build_snapshot([b'C-id', b'B-id'],
2650
[('modify', ('foo', b'B content\n'))],
2651
revision_id=b'E-id') # merge the content
2652
builder.build_snapshot([b'E-id'],
2653
[('modify', ('foo', b'base content\n'))],
2654
revision_id=b'F-id') # Revert back to BASE
2655
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2656
wt, conflicts = self.do_merge(builder, b'F-id')
2657
self.assertEqual(0, conflicts)
2658
# TODO: We need to use the per-file graph to properly select a BASE
2659
# before this will work. Or at least use the LCA trees to find
2660
# the appropriate content base. (which is B, not A).
2661
self.assertEqual(b'base content\n', wt.get_file_text('foo'))
2663
def test_other_modified_content(self):
2664
builder = self.get_builder()
2665
builder.build_snapshot(None,
2666
[('add', (u'', b'a-root-id', 'directory', None)),
2667
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2668
revision_id=b'A-id')
2669
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2670
builder.build_snapshot([b'A-id'],
2671
[('modify', ('foo', b'B content\n'))],
2672
revision_id=b'B-id')
2673
builder.build_snapshot([b'C-id', b'B-id'],
2674
[('modify', ('foo', b'B content\n'))],
2675
revision_id=b'E-id') # merge the content
2676
builder.build_snapshot([b'E-id'],
2677
[('modify', ('foo', b'F content\n'))],
2678
revision_id=b'F-id') # Override B content
2679
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2680
wt, conflicts = self.do_merge(builder, b'F-id')
2681
self.assertEqual(0, conflicts)
2682
self.assertEqual(b'F content\n', wt.get_file_text('foo'))
2684
def test_all_wt(self):
2685
"""Check behavior if all trees are Working Trees."""
2686
# The big issue is that entry.revision is None for WorkingTrees. (as is
2687
# entry.text_sha1, etc. So we need to make sure we handle that case
2689
# A Content of 'foo', path of 'a'
2691
# B C B modifies content, C renames 'a' => 'b'
2693
# D E E updates content, renames 'b' => 'c'
2694
builder = self.get_builder()
2695
builder.build_snapshot(None,
2696
[('add', (u'', b'a-root-id', 'directory', None)),
2697
('add', (u'a', b'a-id', 'file', b'base content\n')),
2698
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2699
revision_id=b'A-id')
2700
builder.build_snapshot([b'A-id'],
2701
[('modify', ('foo', b'B content\n'))],
2702
revision_id=b'B-id')
2703
builder.build_snapshot([b'A-id'],
2704
[('rename', ('a', 'b'))],
2705
revision_id=b'C-id')
2706
builder.build_snapshot([b'C-id', b'B-id'],
2707
[('rename', ('b', 'c')),
2708
('modify', ('foo', b'E content\n'))],
2709
revision_id=b'E-id')
2710
builder.build_snapshot([b'B-id', b'C-id'],
2711
[('rename', ('a', 'b'))], revision_id=b'D-id') # merged change
2712
wt_this = self.get_wt_from_builder(builder)
2713
wt_base = wt_this.controldir.sprout('base', b'A-id').open_workingtree()
2715
self.addCleanup(wt_base.unlock)
2716
wt_lca1 = wt_this.controldir.sprout(
2717
'b-tree', b'B-id').open_workingtree()
2719
self.addCleanup(wt_lca1.unlock)
2720
wt_lca2 = wt_this.controldir.sprout(
2721
'c-tree', b'C-id').open_workingtree()
2723
self.addCleanup(wt_lca2.unlock)
2724
wt_other = wt_this.controldir.sprout(
2725
'other', b'E-id').open_workingtree()
2726
wt_other.lock_read()
2727
self.addCleanup(wt_other.unlock)
2728
merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
2729
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2730
entries = list(merge_obj._entries_lca())
2731
root_id = b'a-root-id'
2732
self.assertEqual([(b'a-id', False,
2733
((u'a', [u'a', u'b']), u'c', u'b'),
2734
((root_id, [root_id, root_id]), root_id, root_id),
2735
((u'a', [u'a', u'b']), u'c', u'b'),
2736
((False, [False, False]), False, False)),
2738
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2739
((root_id, [root_id, root_id]), root_id, root_id),
2740
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2741
((False, [False, False]), False, False)),
2744
def test_nested_tree_unmodified(self):
2745
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2747
wt = self.make_branch_and_tree('tree',
2748
format='development-subtree')
2750
self.addCleanup(wt.unlock)
2751
sub_tree = self.make_branch_and_tree('tree/sub-tree',
2752
format='development-subtree')
2753
wt.set_root_id(b'a-root-id')
2754
sub_tree.set_root_id(b'sub-tree-root')
2755
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
2756
sub_tree.add('file')
2757
sub_tree.commit('foo', rev_id=b'sub-A-id')
2758
wt.add_reference(sub_tree)
2759
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2760
# Now create a criss-cross merge in the parent, without modifying the
2762
wt.commit('B', rev_id=b'B-id', recursive=None)
2763
wt.set_last_revision(b'A-id')
2764
wt.branch.set_last_revision_info(1, b'A-id')
2765
wt.commit('C', rev_id=b'C-id', recursive=None)
2766
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2767
wt.commit('E', rev_id=b'E-id', recursive=None)
2768
wt.set_parent_ids([b'B-id', b'C-id'])
2769
wt.branch.set_last_revision_info(2, b'B-id')
2770
wt.commit('D', rev_id=b'D-id', recursive=None)
2772
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2773
merger.merge_type = _mod_merge.Merge3Merger
2774
merge_obj = merger.make_merger()
2775
entries = list(merge_obj._entries_lca())
2776
self.assertEqual([], entries)
2778
def test_nested_tree_subtree_modified(self):
2779
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2781
wt = self.make_branch_and_tree('tree',
2782
format='development-subtree')
2784
self.addCleanup(wt.unlock)
2785
sub_tree = self.make_branch_and_tree('tree/sub',
2786
format='development-subtree')
2787
wt.set_root_id(b'a-root-id')
2788
sub_tree.set_root_id(b'sub-tree-root')
2789
self.build_tree_contents([('tree/sub/file', b'text1')])
2790
sub_tree.add('file')
2791
sub_tree.commit('foo', rev_id=b'sub-A-id')
2792
wt.add_reference(sub_tree)
2793
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2794
# Now create a criss-cross merge in the parent, without modifying the
2796
wt.commit('B', rev_id=b'B-id', recursive=None)
2797
wt.set_last_revision(b'A-id')
2798
wt.branch.set_last_revision_info(1, b'A-id')
2799
wt.commit('C', rev_id=b'C-id', recursive=None)
2800
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2801
self.build_tree_contents([('tree/sub/file', b'text2')])
2802
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2803
wt.commit('E', rev_id=b'E-id', recursive=None)
2804
wt.set_parent_ids([b'B-id', b'C-id'])
2805
wt.branch.set_last_revision_info(2, b'B-id')
2806
wt.commit('D', rev_id=b'D-id', recursive=None)
2808
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2809
merger.merge_type = _mod_merge.Merge3Merger
2810
merge_obj = merger.make_merger()
2811
entries = list(merge_obj._entries_lca())
2812
# Nothing interesting about this sub-tree, because content changes are
2813
# computed at a higher level
2814
self.assertEqual([], entries)
2816
def test_nested_tree_subtree_renamed(self):
2817
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2819
wt = self.make_branch_and_tree('tree',
2820
format='development-subtree')
2822
self.addCleanup(wt.unlock)
2823
sub_tree = self.make_branch_and_tree('tree/sub',
2824
format='development-subtree')
2825
wt.set_root_id(b'a-root-id')
2826
sub_tree.set_root_id(b'sub-tree-root')
2827
self.build_tree_contents([('tree/sub/file', b'text1')])
2828
sub_tree.add('file')
2829
sub_tree.commit('foo', rev_id=b'sub-A-id')
2830
wt.add_reference(sub_tree)
2831
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2832
# Now create a criss-cross merge in the parent, without modifying the
2834
wt.commit('B', rev_id=b'B-id', recursive=None)
2835
wt.set_last_revision(b'A-id')
2836
wt.branch.set_last_revision_info(1, b'A-id')
2837
wt.commit('C', rev_id=b'C-id', recursive=None)
2838
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2839
wt.rename_one('sub', 'alt_sub')
2840
wt.commit('E', rev_id=b'E-id', recursive=None)
2841
wt.set_last_revision(b'B-id')
2843
wt.set_parent_ids([b'B-id', b'C-id'])
2844
wt.branch.set_last_revision_info(2, b'B-id')
2845
wt.commit('D', rev_id=b'D-id', recursive=None)
2847
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2848
merger.merge_type = _mod_merge.Merge3Merger
2849
merge_obj = merger.make_merger()
2850
entries = list(merge_obj._entries_lca())
2851
root_id = b'a-root-id'
2852
self.assertEqual([(b'sub-tree-root', False,
2853
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2854
((root_id, [root_id, root_id]), root_id, root_id),
2855
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2856
((False, [False, False]), False, False)),
2859
def test_nested_tree_subtree_renamed_and_modified(self):
2860
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2862
wt = self.make_branch_and_tree('tree',
2863
format='development-subtree')
2865
self.addCleanup(wt.unlock)
2866
sub_tree = self.make_branch_and_tree('tree/sub',
2867
format='development-subtree')
2868
wt.set_root_id(b'a-root-id')
2869
sub_tree.set_root_id(b'sub-tree-root')
2870
self.build_tree_contents([('tree/sub/file', b'text1')])
2871
sub_tree.add('file')
2872
sub_tree.commit('foo', rev_id=b'sub-A-id')
2873
wt.add_reference(sub_tree)
2874
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2875
# Now create a criss-cross merge in the parent, without modifying the
2877
wt.commit('B', rev_id=b'B-id', recursive=None)
2878
wt.set_last_revision(b'A-id')
2879
wt.branch.set_last_revision_info(1, b'A-id')
2880
wt.commit('C', rev_id=b'C-id', recursive=None)
2881
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2882
self.build_tree_contents([('tree/sub/file', b'text2')])
2883
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2884
wt.rename_one('sub', 'alt_sub')
2885
wt.commit('E', rev_id=b'E-id', recursive=None)
2886
wt.set_last_revision(b'B-id')
2888
wt.set_parent_ids([b'B-id', b'C-id'])
2889
wt.branch.set_last_revision_info(2, b'B-id')
2890
wt.commit('D', rev_id=b'D-id', recursive=None)
2892
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2893
merger.merge_type = _mod_merge.Merge3Merger
2894
merge_obj = merger.make_merger()
2895
entries = list(merge_obj._entries_lca())
2896
root_id = b'a-root-id'
2897
self.assertEqual([(b'sub-tree-root', False,
2898
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2899
((root_id, [root_id, root_id]), root_id, root_id),
2900
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2901
((False, [False, False]), False, False)),
2905
class TestLCAMultiWay(tests.TestCase):
2907
def assertLCAMultiWay(self, expected, base, lcas, other, this,
2908
allow_overriding_lca=True):
2909
self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
2910
(base, lcas), other, this,
2911
allow_overriding_lca=allow_overriding_lca))
2913
def test_other_equal_equal_lcas(self):
2914
"""Test when OTHER=LCA and all LCAs are identical."""
2915
self.assertLCAMultiWay('this',
2916
'bval', ['bval', 'bval'], 'bval', 'bval')
2917
self.assertLCAMultiWay('this',
2918
'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
2919
self.assertLCAMultiWay('this',
2920
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
2921
self.assertLCAMultiWay('this',
2922
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
2923
self.assertLCAMultiWay('this',
2924
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
2926
def test_other_equal_this(self):
2927
"""Test when other and this are identical."""
2928
self.assertLCAMultiWay('this',
2929
'bval', ['bval', 'bval'], 'oval', 'oval')
2930
self.assertLCAMultiWay('this',
2931
'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
2932
self.assertLCAMultiWay('this',
2933
'bval', ['cval', 'dval'], 'oval', 'oval')
2934
self.assertLCAMultiWay('this',
2935
'bval', [None, 'lcaval'], 'oval', 'oval')
2936
self.assertLCAMultiWay('this',
2937
None, [None, 'lcaval'], 'oval', 'oval')
2938
self.assertLCAMultiWay('this',
2939
None, ['lcaval', 'lcaval'], 'oval', 'oval')
2940
self.assertLCAMultiWay('this',
2941
None, ['cval', 'dval'], 'oval', 'oval')
2942
self.assertLCAMultiWay('this',
2943
None, ['cval', 'dval'], None, None)
2944
self.assertLCAMultiWay('this',
2945
None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
2947
def test_no_lcas(self):
2948
self.assertLCAMultiWay('this',
2949
'bval', [], 'bval', 'tval')
2950
self.assertLCAMultiWay('other',
2951
'bval', [], 'oval', 'bval')
2952
self.assertLCAMultiWay('conflict',
2953
'bval', [], 'oval', 'tval')
2954
self.assertLCAMultiWay('this',
2955
'bval', [], 'oval', 'oval')
2957
def test_lca_supersedes_other_lca(self):
2958
"""If one lca == base, the other lca takes precedence"""
2959
self.assertLCAMultiWay('this',
2960
'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
2961
self.assertLCAMultiWay('this',
2962
'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
2963
# This is actually considered a 'revert' because the 'lcaval' in LCAS
2964
# supersedes the BASE val (in the other LCA) but then OTHER reverts it
2966
self.assertLCAMultiWay('other',
2967
'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
2968
self.assertLCAMultiWay('conflict',
2969
'bval', ['bval', 'lcaval'], 'bval', 'tval')
2971
def test_other_and_this_pick_different_lca(self):
2972
# OTHER and THIS resolve the lca conflict in different ways
2973
self.assertLCAMultiWay('conflict',
2974
'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
2975
self.assertLCAMultiWay('conflict',
2976
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
2977
self.assertLCAMultiWay('conflict',
2978
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
2980
def test_other_in_lca(self):
2981
# OTHER takes a value of one of the LCAs, THIS takes a new value, which
2982
# theoretically supersedes both LCA values and 'wins'
2983
self.assertLCAMultiWay(
2984
'this', 'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
2985
self.assertLCAMultiWay(
2986
'this', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val',
2988
self.assertLCAMultiWay('conflict',
2990
'lca2val'], 'lca1val', 'newval',
2991
allow_overriding_lca=False)
2992
self.assertLCAMultiWay('conflict',
2993
'bval', ['lca1val', 'lca2val',
2994
'lca3val'], 'lca1val', 'newval',
2995
allow_overriding_lca=False)
2996
# THIS reverted back to BASE, but that is an explicit supersede of all
2998
self.assertLCAMultiWay(
2999
'this', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val',
3001
self.assertLCAMultiWay(
3002
'this', 'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
3003
self.assertLCAMultiWay('conflict',
3004
'bval', ['lca1val', 'lca2val',
3005
'lca3val'], 'lca1val', 'bval',
3006
allow_overriding_lca=False)
3007
self.assertLCAMultiWay('conflict',
3008
'bval', ['lca1val', 'lca2val',
3009
'bval'], 'lca1val', 'bval',
3010
allow_overriding_lca=False)
3012
def test_this_in_lca(self):
3013
# THIS takes a value of one of the LCAs, OTHER takes a new value, which
3014
# theoretically supersedes both LCA values and 'wins'
3015
self.assertLCAMultiWay(
3016
'other', 'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
3017
self.assertLCAMultiWay(
3018
'other', 'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
3019
self.assertLCAMultiWay('conflict',
3021
'lca2val'], 'oval', 'lca1val',
3022
allow_overriding_lca=False)
3023
self.assertLCAMultiWay('conflict',
3025
'lca2val'], 'oval', 'lca2val',
3026
allow_overriding_lca=False)
3027
# OTHER reverted back to BASE, but that is an explicit supersede of all
3029
self.assertLCAMultiWay(
3030
'other', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval',
3032
self.assertLCAMultiWay(
3033
'conflict', 'bval', ['lca1val', 'lca2val', 'lca3val'],
3034
'bval', 'lca3val', allow_overriding_lca=False)
3036
def test_all_differ(self):
3037
self.assertLCAMultiWay(
3038
'conflict', 'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
3039
self.assertLCAMultiWay(
3040
'conflict', 'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval',
3042
self.assertLCAMultiWay(
3043
'conflict', 'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval',
3047
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
3050
super(TestConfigurableFileMerger, self).setUp()
3053
def get_merger_factory(self):
3054
# Allows the inner methods to access the test attributes
3057
class FooMerger(_mod_merge.ConfigurableFileMerger):
3059
default_files = ['bar']
3061
def merge_text(self, params):
3062
calls.append('merge_text')
3063
return ('not_applicable', None)
3065
def factory(merger):
3066
result = FooMerger(merger)
3067
# Make sure we start with a clean slate
3068
self.assertEqual(None, result.affected_files)
3069
# Track the original merger
3070
self.merger = result
3075
def _install_hook(self, factory):
3076
_mod_merge.Merger.hooks.install_named_hook('merge_file_content',
3077
factory, 'test factory')
3079
def make_builder(self):
3080
builder = test_merge_core.MergeBuilder(self.test_base_dir)
3081
self.addCleanup(builder.cleanup)
3084
def make_text_conflict(self, file_name='bar'):
3085
factory = self.get_merger_factory()
3086
self._install_hook(factory)
3087
builder = self.make_builder()
3088
builder.add_file(b'bar-id', builder.tree_root,
3089
file_name, b'text1', True)
3090
builder.change_contents(b'bar-id', other=b'text4', this=b'text3')
3093
def make_kind_change(self):
3094
factory = self.get_merger_factory()
3095
self._install_hook(factory)
3096
builder = self.make_builder()
3097
builder.add_file(b'bar-id', builder.tree_root, 'bar', b'text1', True,
3099
builder.add_dir(b'bar-dir', builder.tree_root, 'bar-id',
3100
base=False, other=False)
3103
def test_uses_this_branch(self):
3104
builder = self.make_text_conflict()
3105
with builder.make_preview_transform() as tt:
3108
def test_affected_files_cached(self):
3109
"""Ensures that the config variable is cached"""
3110
builder = self.make_text_conflict()
3111
conflicts = builder.merge()
3112
# The hook should set the variable
3113
self.assertEqual(['bar'], self.merger.affected_files)
3114
self.assertEqual(1, len(conflicts))
3116
def test_hook_called_for_text_conflicts(self):
3117
builder = self.make_text_conflict()
3119
# The hook should call the merge_text() method
3120
self.assertEqual(['merge_text'], self.calls)
3122
def test_hook_not_called_for_kind_change(self):
3123
builder = self.make_kind_change()
3125
# The hook should not call the merge_text() method
3126
self.assertEqual([], self.calls)
3128
def test_hook_not_called_for_other_files(self):
3129
builder = self.make_text_conflict('foobar')
3131
# The hook should not call the merge_text() method
3132
self.assertEqual([], self.calls)
3135
class TestMergeIntoBase(tests.TestCaseWithTransport):
3137
def setup_simple_branch(self, relpath, shape=None, root_id=None):
3138
"""One commit, containing tree specified by optional shape.
3140
Default is empty tree (just root entry).
3143
root_id = b'%s-root-id' % (relpath.encode('ascii'),)
3144
wt = self.make_branch_and_tree(relpath)
3145
wt.set_root_id(root_id)
3146
if shape is not None:
3147
adjusted_shape = [relpath + '/' + elem for elem in shape]
3148
self.build_tree(adjusted_shape)
3150
(b'%s-%s-id' % (relpath.encode('utf-8'),
3151
basename(elem.rstrip('/')).encode('ascii')))
3153
wt.add(shape, ids=ids)
3154
rev_id = b'r1-%s' % (relpath.encode('utf-8'),)
3155
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
3156
self.assertEqual(root_id, wt.path2id(''))
3159
def setup_two_branches(self, custom_root_ids=True):
3160
"""Setup 2 branches, one will be a library, the other a project."""
3164
root_id = inventory.ROOT_ID
3165
project_wt = self.setup_simple_branch(
3166
'project', ['README', 'dir/', 'dir/file.c'],
3168
lib_wt = self.setup_simple_branch(
3169
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3171
return project_wt, lib_wt
3173
def do_merge_into(self, location, merge_as):
3174
"""Helper for using MergeIntoMerger.
3176
:param location: location of directory to merge from, either the
3177
location of a branch or of a path inside a branch.
3178
:param merge_as: the path in a tree to add the new directory as.
3179
:returns: the conflicts from 'do_merge'.
3181
with contextlib.ExitStack() as stack:
3182
# Open and lock the various tree and branch objects
3183
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3184
stack.enter_context(wt.lock_write())
3185
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3187
stack.enter_context(branch_to_merge.lock_read())
3188
other_tree = branch_to_merge.basis_tree()
3189
stack.enter_context(other_tree.lock_read())
3191
merger = _mod_merge.MergeIntoMerger(
3192
this_tree=wt, other_tree=other_tree, other_branch=branch_to_merge,
3193
target_subdir=subdir_relpath, source_subpath=subdir_to_merge)
3194
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3195
conflicts = merger.do_merge()
3196
merger.set_pending()
3199
def assertTreeEntriesEqual(self, expected_entries, tree):
3200
"""Assert that 'tree' contains the expected inventory entries.
3202
:param expected_entries: sequence of (path, file-id) pairs.
3204
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3205
self.assertEqual(expected_entries, files)
3208
class TestMergeInto(TestMergeIntoBase):
3210
def test_newdir_with_unique_roots(self):
3211
"""Merge a branch with a unique root into a new directory."""
3212
project_wt, lib_wt = self.setup_two_branches()
3213
self.do_merge_into('lib1', 'project/lib1')
3214
project_wt.lock_read()
3215
self.addCleanup(project_wt.unlock)
3216
# The r1-lib1 revision should be merged into this one
3217
self.assertEqual([b'r1-project', b'r1-lib1'],
3218
project_wt.get_parent_ids())
3219
self.assertTreeEntriesEqual(
3220
[('', b'project-root-id'),
3221
('README', b'project-README-id'),
3222
('dir', b'project-dir-id'),
3223
('lib1', b'lib1-root-id'),
3224
('dir/file.c', b'project-file.c-id'),
3225
('lib1/Makefile', b'lib1-Makefile-id'),
3226
('lib1/README', b'lib1-README-id'),
3227
('lib1/foo.c', b'lib1-foo.c-id'),
3230
def test_subdir(self):
3231
"""Merge a branch into a subdirectory of an existing directory."""
3232
project_wt, lib_wt = self.setup_two_branches()
3233
self.do_merge_into('lib1', 'project/dir/lib1')
3234
project_wt.lock_read()
3235
self.addCleanup(project_wt.unlock)
3236
# The r1-lib1 revision should be merged into this one
3237
self.assertEqual([b'r1-project', b'r1-lib1'],
3238
project_wt.get_parent_ids())
3239
self.assertTreeEntriesEqual(
3240
[('', b'project-root-id'),
3241
('README', b'project-README-id'),
3242
('dir', b'project-dir-id'),
3243
('dir/file.c', b'project-file.c-id'),
3244
('dir/lib1', b'lib1-root-id'),
3245
('dir/lib1/Makefile', b'lib1-Makefile-id'),
3246
('dir/lib1/README', b'lib1-README-id'),
3247
('dir/lib1/foo.c', b'lib1-foo.c-id'),
3250
def test_newdir_with_repeat_roots(self):
3251
"""If the file-id of the dir to be merged already exists a new ID will
3252
be allocated to let the merge happen.
3254
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3255
root_id = project_wt.path2id('')
3256
self.do_merge_into('lib1', 'project/lib1')
3257
project_wt.lock_read()
3258
self.addCleanup(project_wt.unlock)
3259
# The r1-lib1 revision should be merged into this one
3260
self.assertEqual([b'r1-project', b'r1-lib1'],
3261
project_wt.get_parent_ids())
3262
new_lib1_id = project_wt.path2id('lib1')
3263
self.assertNotEqual(None, new_lib1_id)
3264
self.assertTreeEntriesEqual(
3266
('README', b'project-README-id'),
3267
('dir', b'project-dir-id'),
3268
('lib1', new_lib1_id),
3269
('dir/file.c', b'project-file.c-id'),
3270
('lib1/Makefile', b'lib1-Makefile-id'),
3271
('lib1/README', b'lib1-README-id'),
3272
('lib1/foo.c', b'lib1-foo.c-id'),
3275
def test_name_conflict(self):
3276
"""When the target directory name already exists a conflict is
3277
generated and the original directory is renamed to foo.moved.
3279
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3280
self.setup_simple_branch('src', ['README'])
3281
conflicts = self.do_merge_into('src', 'dest/dir')
3282
self.assertEqual(1, conflicts)
3284
self.addCleanup(dest_wt.unlock)
3285
# The r1-lib1 revision should be merged into this one
3286
self.assertEqual([b'r1-dest', b'r1-src'], dest_wt.get_parent_ids())
3287
self.assertTreeEntriesEqual(
3288
[('', b'dest-root-id'),
3289
('dir', b'src-root-id'),
3290
('dir.moved', b'dest-dir-id'),
3291
('dir/README', b'src-README-id'),
3292
('dir.moved/file.txt', b'dest-file.txt-id'),
3295
def test_file_id_conflict(self):
3296
"""A conflict is generated if the merge-into adds a file (or other
3297
inventory entry) with a file-id that already exists in the target tree.
3299
self.setup_simple_branch('dest', ['file.txt'])
3300
# Make a second tree with a file-id that will clash with file.txt in
3302
src_wt = self.make_branch_and_tree('src')
3303
self.build_tree(['src/README'])
3304
src_wt.add(['README'], ids=[b'dest-file.txt-id'])
3305
src_wt.commit("Rev 1 of src.", rev_id=b'r1-src')
3306
conflicts = self.do_merge_into('src', 'dest/dir')
3307
# This is an edge case that shouldn't happen to users very often. So
3308
# we don't care really about the exact presentation of the conflict,
3309
# just that there is one.
3310
self.assertEqual(1, conflicts)
3312
def test_only_subdir(self):
3313
"""When the location points to just part of a tree, merge just that
3316
dest_wt = self.setup_simple_branch('dest')
3317
self.setup_simple_branch('src', ['hello.txt', 'dir/', 'dir/foo.c'])
3318
self.do_merge_into('src/dir', 'dest/dir')
3320
self.addCleanup(dest_wt.unlock)
3321
# The r1-lib1 revision should NOT be merged into this one (this is a
3323
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3324
self.assertTreeEntriesEqual(
3325
[('', b'dest-root-id'),
3326
('dir', b'src-dir-id'),
3327
('dir/foo.c', b'src-foo.c-id'),
3330
def test_only_file(self):
3331
"""An edge case: merge just one file, not a whole dir."""
3332
dest_wt = self.setup_simple_branch('dest')
3333
self.setup_simple_branch('two-file', ['file1.txt', 'file2.txt'])
3334
self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3336
self.addCleanup(dest_wt.unlock)
3337
# The r1-lib1 revision should NOT be merged into this one
3338
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3339
self.assertTreeEntriesEqual(
3340
[('', b'dest-root-id'), ('file1.txt', b'two-file-file1.txt-id')],
3343
def test_no_such_source_path(self):
3344
"""PathNotInTree is raised if the specified path in the source tree
3347
dest_wt = self.setup_simple_branch('dest')
3348
self.setup_simple_branch('src', ['dir/'])
3349
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3350
'src/no-such-dir', 'dest/foo')
3352
self.addCleanup(dest_wt.unlock)
3353
# The dest tree is unmodified.
3354
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3355
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3357
def test_no_such_target_path(self):
3358
"""PathNotInTree is also raised if the specified path in the target
3359
tree does not exist.
3361
dest_wt = self.setup_simple_branch('dest')
3362
self.setup_simple_branch('src', ['file.txt'])
3363
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3364
'src', 'dest/no-such-dir/foo')
3366
self.addCleanup(dest_wt.unlock)
3367
# The dest tree is unmodified.
3368
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3369
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3372
class TestMergeHooks(TestCaseWithTransport):
3375
super(TestMergeHooks, self).setUp()
3376
self.tree_a = self.make_branch_and_tree('tree_a')
3377
self.build_tree_contents([('tree_a/file', b'content_1')])
3378
self.tree_a.add('file', b'file-id')
3379
self.tree_a.commit('added file')
3381
self.tree_b = self.tree_a.controldir.sprout(
3382
'tree_b').open_workingtree()
3383
self.build_tree_contents([('tree_b/file', b'content_2')])
3384
self.tree_b.commit('modify file')
3386
def test_pre_merge_hook_inject_different_tree(self):
3387
tree_c = self.tree_b.controldir.sprout('tree_c').open_workingtree()
3388
self.build_tree_contents([('tree_c/file', b'content_3')])
3389
tree_c.commit("more content")
3392
def factory(merger):
3393
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3394
merger.other_tree = tree_c
3395
calls.append(merger)
3396
_mod_merge.Merger.hooks.install_named_hook('pre_merge',
3397
factory, 'test factory')
3398
self.tree_a.merge_from_branch(self.tree_b.branch)
3400
self.assertFileEqual(b"content_3", 'tree_a/file')
3401
self.assertLength(1, calls)
3403
def test_post_merge_hook_called(self):
3406
def factory(merger):
3407
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3408
calls.append(merger)
3409
_mod_merge.Merger.hooks.install_named_hook('post_merge',
3410
factory, 'test factory')
3412
self.tree_a.merge_from_branch(self.tree_b.branch)
3414
self.assertFileEqual(b"content_2", 'tree_a/file')
3415
self.assertLength(1, calls)