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
20
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
40
from ..sixish import int2byte
43
TestCaseWithMemoryTransport,
44
TestCaseWithTransport,
47
from ..workingtree import WorkingTree
50
class TestMerge(TestCaseWithTransport):
51
"""Test appending more than one revision"""
53
def test_pending(self):
54
wt = self.make_branch_and_tree('.')
55
rev_a = wt.commit("lala!")
56
self.assertEqual([rev_a], wt.get_parent_ids())
57
self.assertRaises(errors.PointlessMerge, wt.merge_from_branch,
59
self.assertEqual([rev_a], wt.get_parent_ids())
63
wt = self.make_branch_and_tree('.')
67
wt.merge_from_branch(wt.branch, wt.branch.get_rev_id(2),
68
wt.branch.get_rev_id(1))
70
def test_nocommits(self):
71
wt = self.test_pending()
72
wt2 = self.make_branch_and_tree('branch2')
73
self.assertRaises(NoCommits, wt.merge_from_branch, wt2.branch)
76
def test_unrelated(self):
77
wt, wt2 = self.test_nocommits()
79
self.assertRaises(UnrelatedBranches, wt.merge_from_branch, wt2.branch)
82
def test_merge_one_file(self):
83
"""Do a partial merge of a tree which should not affect tree parents."""
84
wt1 = self.make_branch_and_tree('branch1')
85
tip = wt1.commit('empty commit')
86
wt2 = self.make_branch_and_tree('branch2')
88
with open('branch1/foo', 'wb') as f:
90
with open('branch1/bar', 'wb') as f:
94
wt1.commit('add foobar')
95
self.run_bzr('merge ../branch1/baz', retcode=3, working_dir='branch2')
96
self.run_bzr('merge ../branch1/foo', working_dir='branch2')
97
self.assertPathExists('branch2/foo')
98
self.assertPathDoesNotExist('branch2/bar')
99
wt2 = WorkingTree.open('branch2')
100
self.assertEqual([tip], wt2.get_parent_ids())
102
def test_pending_with_null(self):
103
"""When base is forced to revno 0, parent_ids are set"""
104
wt2 = self.test_unrelated()
105
wt1 = WorkingTree.open('.')
107
br1.fetch(wt2.branch)
108
# merge all of branch 2 into branch 1 even though they
110
wt1.merge_from_branch(wt2.branch, wt2.last_revision(), b'null:')
111
self.assertEqual([br1.last_revision(), wt2.branch.last_revision()],
112
wt1.get_parent_ids())
113
return (wt1, wt2.branch)
115
def test_two_roots(self):
116
"""Merge base is sane when two unrelated branches are merged"""
117
wt1, br2 = self.test_pending_with_null()
121
last = wt1.branch.last_revision()
122
last2 = br2.last_revision()
123
graph = wt1.branch.repository.get_graph()
124
self.assertEqual(last2, graph.find_unique_lca(last, last2))
128
def test_merge_into_null_tree(self):
129
wt = self.make_branch_and_tree('tree')
130
null_tree = wt.basis_tree()
131
self.build_tree(['tree/file'])
133
wt.commit('tree with root')
134
merger = _mod_merge.Merge3Merger(null_tree, null_tree, null_tree, wt,
135
this_branch=wt.branch,
137
with merger.make_preview_transform() as tt:
138
self.assertEqual([], tt.find_conflicts())
139
preview = tt.get_preview_tree()
140
self.assertEqual(wt.get_root_id(), preview.get_root_id())
142
def test_merge_unrelated_retains_root(self):
143
wt = self.make_branch_and_tree('tree')
144
other_tree = self.make_branch_and_tree('other')
145
self.addCleanup(other_tree.lock_read().unlock)
146
merger = _mod_merge.Merge3Merger(wt, wt, wt.basis_tree(), other_tree,
147
this_branch=wt.branch,
149
with transform.TransformPreview(wt) as merger.tt:
150
merger._compute_transform()
151
new_root_id = merger.tt.final_file_id(merger.tt.root)
152
self.assertEqual(wt.get_root_id(), new_root_id)
154
def test_create_rename(self):
155
"""Rename an inventory entry while creating the file"""
156
tree =self.make_branch_and_tree('.')
157
with open('name1', 'wb') as f: f.write(b'Hello')
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: f.write(b'Hello')
172
tree.commit(message="hello")
173
filename2 = pathjoin('dirname1', 'name2')
174
tree.rename_one(filename, filename2)
175
tree.rename_one('dirname1', 'dirname2')
176
transform_tree(tree, tree.branch.basis_tree())
178
def test_ignore_zero_merge_inner(self):
179
# Test that merge_inner's ignore zero parameter is effective
180
tree_a =self.make_branch_and_tree('a')
181
tree_a.commit(message="hello")
182
dir_b = tree_a.controldir.sprout('b')
183
tree_b = dir_b.open_workingtree()
185
self.addCleanup(tree_b.unlock)
186
tree_a.commit(message="hello again")
187
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
188
this_tree=tree_b, ignore_zero=True)
189
self.assertTrue('All changes applied successfully.\n' not in
192
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
193
this_tree=tree_b, ignore_zero=False)
194
self.assertTrue('All changes applied successfully.\n' in self.get_log())
196
def test_merge_inner_conflicts(self):
197
tree_a = self.make_branch_and_tree('a')
198
tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
199
merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
200
self.assertEqual(1, len(tree_a.conflicts()))
202
def test_rmdir_conflict(self):
203
tree_a = self.make_branch_and_tree('a')
204
self.build_tree(['a/b/'])
205
tree_a.add('b', b'b-id')
206
tree_a.commit('added b')
207
# basis_tree() is only guaranteed to be valid as long as it is actually
208
# the basis tree. This mutates the tree after grabbing basis, so go to
210
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
211
tree_z = tree_a.controldir.sprout('z').open_workingtree()
212
self.build_tree(['a/b/c'])
214
tree_a.commit('added c')
216
tree_z.commit('removed b')
217
merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
219
conflicts.MissingParent('Created directory', 'b', b'b-id'),
220
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
222
merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
225
conflicts.DeletingParent('Not deleting', 'b', b'b-id'),
226
conflicts.UnversionedParent('Versioned directory', 'b', b'b-id')],
229
def test_nested_merge(self):
231
'iter_changes doesn\'t work with changes in nested trees')
232
tree = self.make_branch_and_tree('tree',
233
format='development-subtree')
234
sub_tree = self.make_branch_and_tree('tree/sub-tree',
235
format='development-subtree')
236
sub_tree.set_root_id(b'sub-tree-root')
237
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
239
sub_tree.commit('foo')
240
tree.add_reference(sub_tree)
241
tree.commit('set text to 1')
242
tree2 = tree.controldir.sprout('tree2').open_workingtree()
243
# modify the file in the subtree
244
self.build_tree_contents([('tree2/sub-tree/file', b'text2')])
245
# and merge the changes from the diverged subtree into the containing
247
tree2.commit('changed file text')
248
tree.merge_from_branch(tree2.branch)
249
self.assertFileEqual(b'text2', 'tree/sub-tree/file')
251
def test_merge_with_missing(self):
252
tree_a = self.make_branch_and_tree('tree_a')
253
self.build_tree_contents([('tree_a/file', b'content_1')])
255
tree_a.commit('commit base')
256
# basis_tree() is only guaranteed to be valid as long as it is actually
257
# the basis tree. This test commits to the tree after grabbing basis,
258
# so we go to the repository.
259
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
260
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
261
self.build_tree_contents([('tree_a/file', b'content_2')])
262
tree_a.commit('commit other')
263
other_tree = tree_a.basis_tree()
264
# 'file' is now missing but isn't altered in any commit in b so no
265
# change should be applied.
266
os.unlink('tree_b/file')
267
merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
269
def test_merge_kind_change(self):
270
tree_a = self.make_branch_and_tree('tree_a')
271
self.build_tree_contents([('tree_a/file', b'content_1')])
272
tree_a.add('file', b'file-id')
273
tree_a.commit('added file')
274
tree_b = tree_a.controldir.sprout('tree_b').open_workingtree()
275
os.unlink('tree_a/file')
276
self.build_tree(['tree_a/file/'])
277
tree_a.commit('changed file to directory')
278
tree_b.merge_from_branch(tree_a.branch)
279
self.assertEqual('directory', file_kind('tree_b/file'))
281
self.assertEqual('file', file_kind('tree_b/file'))
282
self.build_tree_contents([('tree_b/file', b'content_2')])
283
tree_b.commit('content change')
284
tree_b.merge_from_branch(tree_a.branch)
285
self.assertEqual(tree_b.conflicts(),
286
[conflicts.ContentsConflict('file',
287
file_id=b'file-id')])
289
def test_merge_type_registry(self):
290
merge_type_option = option.Option.OPTIONS['merge-type']
291
self.assertFalse('merge4' in [x[0] for x in
292
merge_type_option.iter_switches()])
293
registry = _mod_merge.get_merge_type_registry()
294
registry.register_lazy('merge4', 'breezy.merge', 'Merge4Merger',
295
'time-travelling merge')
296
self.assertTrue('merge4' in [x[0] for x in
297
merge_type_option.iter_switches()])
298
registry.remove('merge4')
299
self.assertFalse('merge4' in [x[0] for x in
300
merge_type_option.iter_switches()])
302
def test_merge_other_moves_we_deleted(self):
303
tree_a = self.make_branch_and_tree('A')
305
self.addCleanup(tree_a.unlock)
306
self.build_tree(['A/a'])
308
tree_a.commit('1', rev_id=b'rev-1')
310
tree_a.rename_one('a', 'b')
312
bzrdir_b = tree_a.controldir.sprout('B', revision_id=b'rev-1')
313
tree_b = bzrdir_b.open_workingtree()
315
self.addCleanup(tree_b.unlock)
319
tree_b.merge_from_branch(tree_a.branch)
320
except AttributeError:
321
self.fail('tried to join a path when name was None')
323
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
324
tree_a = self.make_branch_and_tree('a')
325
self.build_tree(['a/file_1', 'a/file_2'])
326
tree_a.add(['file_1'])
327
tree_a.commit('commit 1')
328
tree_a.add(['file_2'])
329
tree_a.commit('commit 2')
330
tree_b = tree_a.controldir.sprout('b').open_workingtree()
331
tree_b.rename_one('file_1', 'renamed')
332
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
333
merger.merge_type = _mod_merge.Merge3Merger
335
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
337
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
338
tree_a = self.make_branch_and_tree('a')
339
self.build_tree(['a/file_1', 'a/file_2'])
340
tree_a.add(['file_1'])
341
tree_a.commit('commit 1')
342
tree_a.add(['file_2'])
343
tree_a.commit('commit 2')
344
tree_b = tree_a.controldir.sprout('b').open_workingtree()
345
tree_b.rename_one('file_1', 'renamed')
346
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
347
merger.merge_type = _mod_merge.WeaveMerger
349
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
351
def prepare_cherrypick(self):
352
"""Prepare a pair of trees for cherrypicking tests.
354
Both trees have a file, 'file'.
355
rev1 sets content to 'a'.
358
A full merge of rev2b and rev3b into this_tree would add both 'b' and
359
'c'. A successful cherrypick of rev2b-rev3b into this_tree will add
362
this_tree = self.make_branch_and_tree('this')
363
self.build_tree_contents([('this/file', b"a\n")])
364
this_tree.add('file')
365
this_tree.commit('rev1')
366
other_tree = this_tree.controldir.sprout('other').open_workingtree()
367
self.build_tree_contents([('other/file', b"a\nb\n")])
368
other_tree.commit('rev2b', rev_id=b'rev2b')
369
self.build_tree_contents([('other/file', b"c\na\nb\n")])
370
other_tree.commit('rev3b', rev_id=b'rev3b')
371
this_tree.lock_write()
372
self.addCleanup(this_tree.unlock)
373
return this_tree, other_tree
375
def test_weave_cherrypick(self):
376
this_tree, other_tree = self.prepare_cherrypick()
377
merger = _mod_merge.Merger.from_revision_ids(
378
this_tree, b'rev3b', b'rev2b', other_tree.branch)
379
merger.merge_type = _mod_merge.WeaveMerger
381
self.assertFileEqual(b'c\na\n', 'this/file')
383
def test_weave_cannot_reverse_cherrypick(self):
384
this_tree, other_tree = self.prepare_cherrypick()
385
merger = _mod_merge.Merger.from_revision_ids(
386
this_tree, b'rev2b', b'rev3b', other_tree.branch)
387
merger.merge_type = _mod_merge.WeaveMerger
388
self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
390
def test_merge3_can_reverse_cherrypick(self):
391
this_tree, other_tree = self.prepare_cherrypick()
392
merger = _mod_merge.Merger.from_revision_ids(
393
this_tree, b'rev2b', b'rev3b', other_tree.branch)
394
merger.merge_type = _mod_merge.Merge3Merger
397
def test_merge3_will_detect_cherrypick(self):
398
this_tree = self.make_branch_and_tree('this')
399
self.build_tree_contents([('this/file', b"a\n")])
400
this_tree.add('file')
401
this_tree.commit('rev1')
402
other_tree = this_tree.controldir.sprout('other').open_workingtree()
403
self.build_tree_contents([('other/file', b"a\nb\n")])
404
other_tree.commit('rev2b', rev_id=b'rev2b')
405
self.build_tree_contents([('other/file', b"a\nb\nc\n")])
406
other_tree.commit('rev3b', rev_id=b'rev3b')
407
this_tree.lock_write()
408
self.addCleanup(this_tree.unlock)
410
merger = _mod_merge.Merger.from_revision_ids(
411
this_tree, b'rev3b', b'rev2b', other_tree.branch)
412
merger.merge_type = _mod_merge.Merge3Merger
414
self.assertFileEqual(b'a\n'
418
b'>>>>>>> MERGE-SOURCE\n',
421
def test_merge_reverse_revision_range(self):
422
tree = self.make_branch_and_tree(".")
424
self.addCleanup(tree.unlock)
425
self.build_tree(['a'])
427
first_rev = tree.commit("added a")
428
merger = _mod_merge.Merger.from_revision_ids(tree,
429
_mod_revision.NULL_REVISION,
431
merger.merge_type = _mod_merge.Merge3Merger
432
merger.interesting_files = 'a'
433
conflict_count = merger.do_merge()
434
self.assertEqual(0, conflict_count)
436
self.assertPathDoesNotExist("a")
438
self.assertPathExists("a")
440
def test_make_merger(self):
441
this_tree = self.make_branch_and_tree('this')
442
this_tree.commit('rev1', rev_id=b'rev1')
443
other_tree = this_tree.controldir.sprout('other').open_workingtree()
444
this_tree.commit('rev2', rev_id=b'rev2a')
445
other_tree.commit('rev2', rev_id=b'rev2b')
446
this_tree.lock_write()
447
self.addCleanup(this_tree.unlock)
448
merger = _mod_merge.Merger.from_revision_ids(
449
this_tree, b'rev2b', other_branch=other_tree.branch)
450
merger.merge_type = _mod_merge.Merge3Merger
451
tree_merger = merger.make_merger()
452
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
453
self.assertEqual(b'rev2b',
454
tree_merger.other_tree.get_revision_id())
455
self.assertEqual(b'rev1',
456
tree_merger.base_tree.get_revision_id())
457
self.assertEqual(other_tree.branch, tree_merger.other_branch)
459
def test_make_preview_transform(self):
460
this_tree = self.make_branch_and_tree('this')
461
self.build_tree_contents([('this/file', b'1\n')])
462
this_tree.add('file', b'file-id')
463
this_tree.commit('rev1', rev_id=b'rev1')
464
other_tree = this_tree.controldir.sprout('other').open_workingtree()
465
self.build_tree_contents([('this/file', b'1\n2a\n')])
466
this_tree.commit('rev2', rev_id=b'rev2a')
467
self.build_tree_contents([('other/file', b'2b\n1\n')])
468
other_tree.commit('rev2', rev_id=b'rev2b')
469
this_tree.lock_write()
470
self.addCleanup(this_tree.unlock)
471
merger = _mod_merge.Merger.from_revision_ids(
472
this_tree, b'rev2b', other_branch=other_tree.branch)
473
merger.merge_type = _mod_merge.Merge3Merger
474
tree_merger = merger.make_merger()
475
tt = tree_merger.make_preview_transform()
476
self.addCleanup(tt.finalize)
477
preview_tree = tt.get_preview_tree()
478
with this_tree.get_file('file') as tree_file:
479
self.assertEqual(b'1\n2a\n', tree_file.read())
480
with preview_tree.get_file('file') as preview_file:
481
self.assertEqual(b'2b\n1\n2a\n', preview_file.read())
483
def test_do_merge(self):
484
this_tree = self.make_branch_and_tree('this')
485
self.build_tree_contents([('this/file', b'1\n')])
486
this_tree.add('file', b'file-id')
487
this_tree.commit('rev1', rev_id=b'rev1')
488
other_tree = this_tree.controldir.sprout('other').open_workingtree()
489
self.build_tree_contents([('this/file', b'1\n2a\n')])
490
this_tree.commit('rev2', rev_id=b'rev2a')
491
self.build_tree_contents([('other/file', b'2b\n1\n')])
492
other_tree.commit('rev2', rev_id=b'rev2b')
493
this_tree.lock_write()
494
self.addCleanup(this_tree.unlock)
495
merger = _mod_merge.Merger.from_revision_ids(
496
this_tree, b'rev2b', other_branch=other_tree.branch)
497
merger.merge_type = _mod_merge.Merge3Merger
498
tree_merger = merger.make_merger()
499
tt = tree_merger.do_merge()
500
with this_tree.get_file('file') as tree_file:
501
self.assertEqual(b'2b\n1\n2a\n', tree_file.read())
503
def test_merge_require_tree_root(self):
504
tree = self.make_branch_and_tree(".")
506
self.addCleanup(tree.unlock)
507
self.build_tree(['a'])
509
first_rev = tree.commit("added a")
510
old_root_id = tree.get_root_id()
511
merger = _mod_merge.Merger.from_revision_ids(tree,
512
_mod_revision.NULL_REVISION,
514
merger.merge_type = _mod_merge.Merge3Merger
515
conflict_count = merger.do_merge()
516
self.assertEqual(0, conflict_count)
517
self.assertEqual({''}, set(tree.all_versioned_paths()))
518
tree.set_parent_ids([])
520
def test_merge_add_into_deleted_root(self):
521
# Yes, people actually do this. And report bugs if it breaks.
522
source = self.make_branch_and_tree('source', format='rich-root-pack')
523
self.build_tree(['source/foo/'])
524
source.add('foo', b'foo-id')
525
source.commit('Add foo')
526
target = source.controldir.sprout('target').open_workingtree()
527
subtree = target.extract('foo', b'foo-id')
528
subtree.commit('Delete root')
529
self.build_tree(['source/bar'])
530
source.add('bar', b'bar-id')
531
source.commit('Add bar')
532
subtree.merge_from_branch(source.branch)
534
def test_merge_joined_branch(self):
535
source = self.make_branch_and_tree('source', format='rich-root-pack')
536
self.build_tree(['source/foo'])
538
source.commit('Add foo')
539
target = self.make_branch_and_tree('target', format='rich-root-pack')
540
self.build_tree(['target/bla'])
542
target.commit('Add bla')
543
nested = source.controldir.sprout('target/subtree').open_workingtree()
544
target.subsume(nested)
545
target.commit('Join nested')
546
self.build_tree(['source/bar'])
548
source.commit('Add bar')
549
target.merge_from_branch(source.branch)
550
target.commit('Merge source')
553
class TestPlanMerge(TestCaseWithMemoryTransport):
556
super(TestPlanMerge, self).setUp()
557
mapper = versionedfile.PrefixMapper()
558
factory = knit.make_file_factory(True, mapper)
559
self.vf = factory(self.get_transport())
560
self.plan_merge_vf = versionedfile._PlanMergeVersionedFile(b'root')
561
self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
563
def add_version(self, key, parents, text):
564
self.vf.add_lines(key, parents, [int2byte(c)+b'\n' for c in bytearray(text)])
566
def add_rev(self, prefix, revision_id, parents, text):
567
self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
570
def add_uncommitted_version(self, key, parents, text):
571
self.plan_merge_vf.add_lines(key, parents,
572
[int2byte(c)+b'\n' for c in bytearray(text)])
574
def setup_plan_merge(self):
575
self.add_rev(b'root', b'A', [], b'abc')
576
self.add_rev(b'root', b'B', [b'A'], b'acehg')
577
self.add_rev(b'root', b'C', [b'A'], b'fabg')
578
return _PlanMerge(b'B', b'C', self.plan_merge_vf, (b'root',))
580
def setup_plan_merge_uncommitted(self):
581
self.add_version((b'root', b'A'), [], b'abc')
582
self.add_uncommitted_version((b'root', b'B:'), [(b'root', b'A')], b'acehg')
583
self.add_uncommitted_version((b'root', b'C:'), [(b'root', b'A')], b'fabg')
584
return _PlanMerge(b'B:', b'C:', self.plan_merge_vf, (b'root',))
586
def test_base_from_plan(self):
587
self.setup_plan_merge()
588
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
589
pwm = versionedfile.PlanWeaveMerge(plan)
590
self.assertEqual([b'a\n', b'b\n', b'c\n'], pwm.base_from_plan())
592
def test_unique_lines(self):
593
plan = self.setup_plan_merge()
594
self.assertEqual(plan._unique_lines(
595
plan._get_matching_blocks(b'B', b'C')),
598
def test_plan_merge(self):
599
self.setup_plan_merge()
600
plan = self.plan_merge_vf.plan_merge(b'B', b'C')
603
('unchanged', b'a\n'),
604
('killed-a', b'b\n'),
605
('killed-b', b'c\n'),
612
def test_plan_merge_cherrypick(self):
613
self.add_rev(b'root', b'A', [], b'abc')
614
self.add_rev(b'root', b'B', [b'A'], b'abcde')
615
self.add_rev(b'root', b'C', [b'A'], b'abcefg')
616
self.add_rev(b'root', b'D', [b'A', b'B', b'C'], b'abcdegh')
617
my_plan = _PlanMerge(b'B', b'D', self.plan_merge_vf, (b'root',))
618
# We shortcut when one text supersedes the other in the per-file graph.
619
# We don't actually need to compare the texts at this point.
628
list(my_plan.plan_merge()))
630
def test_plan_merge_no_common_ancestor(self):
631
self.add_rev(b'root', b'A', [], b'abc')
632
self.add_rev(b'root', b'B', [], b'xyz')
633
my_plan = _PlanMerge(b'A', b'B', self.plan_merge_vf, (b'root',))
641
list(my_plan.plan_merge()))
643
def test_plan_merge_tail_ancestors(self):
644
# The graph looks like this:
645
# A # Common to all ancestors
647
# B C # Ancestors of E, only common to one side
649
# D E F # D, F are unique to G, H respectively
650
# |/ \| # E is the LCA for G & H, and the unique LCA for
655
# I J # criss-cross merge of G, H
657
# In this situation, a simple pruning of ancestors of E will leave D &
658
# F "dangling", which looks like they introduce lines different from
659
# the ones in E, but in actuality C&B introduced the lines, and they
660
# are already present in E
662
# Introduce the base text
663
self.add_rev(b'root', b'A', [], b'abc')
664
# Introduces a new line B
665
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
666
# Introduces a new line C
667
self.add_rev(b'root', b'C', [b'A'], b'abCc')
668
# Introduce new line D
669
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
670
# Merges B and C by just incorporating both
671
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
672
# Introduce new line F
673
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
674
# Merge D & E by just combining the texts
675
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
676
# Merge F & E by just combining the texts
677
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
678
# Merge G & H by just combining texts
679
self.add_rev(b'root', b'I', [b'G', b'H'], b'DaBbCcF')
680
# Merge G & H but supersede an old line in B
681
self.add_rev(b'root', b'J', [b'H', b'G'], b'DaJbCcF')
682
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
684
('unchanged', b'D\n'),
685
('unchanged', b'a\n'),
686
('killed-b', b'B\n'),
688
('unchanged', b'b\n'),
689
('unchanged', b'C\n'),
690
('unchanged', b'c\n'),
691
('unchanged', b'F\n')],
694
def test_plan_merge_tail_triple_ancestors(self):
695
# The graph looks like this:
696
# A # Common to all ancestors
698
# B C # Ancestors of E, only common to one side
700
# D E F # D, F are unique to G, H respectively
701
# |/|\| # E is the LCA for G & H, and the unique LCA for
703
# |\ /| # Q is just an extra node which is merged into both
706
# I J # criss-cross merge of G, H
708
# This is the same as the test_plan_merge_tail_ancestors, except we add
709
# a third LCA that doesn't add new lines, but will trigger our more
710
# involved ancestry logic
712
self.add_rev(b'root', b'A', [], b'abc')
713
self.add_rev(b'root', b'B', [b'A'], b'aBbc')
714
self.add_rev(b'root', b'C', [b'A'], b'abCc')
715
self.add_rev(b'root', b'D', [b'B'], b'DaBbc')
716
self.add_rev(b'root', b'E', [b'B', b'C'], b'aBbCc')
717
self.add_rev(b'root', b'F', [b'C'], b'abCcF')
718
self.add_rev(b'root', b'G', [b'D', b'E'], b'DaBbCc')
719
self.add_rev(b'root', b'H', [b'F', b'E'], b'aBbCcF')
720
self.add_rev(b'root', b'Q', [b'E'], b'aBbCc')
721
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DaBbCcF')
722
# Merge G & H but supersede an old line in B
723
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DaJbCcF')
724
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
726
('unchanged', b'D\n'),
727
('unchanged', b'a\n'),
728
('killed-b', b'B\n'),
730
('unchanged', b'b\n'),
731
('unchanged', b'C\n'),
732
('unchanged', b'c\n'),
733
('unchanged', b'F\n')],
736
def test_plan_merge_2_tail_triple_ancestors(self):
737
# The graph looks like this:
738
# A B # 2 tails going back to NULL
740
# D E F # D, is unique to G, F to H
741
# |/|\| # E is the LCA for G & H, and the unique LCA for
743
# |\ /| # Q is just an extra node which is merged into both
746
# I J # criss-cross merge of G, H (and Q)
749
# This is meant to test after hitting a 3-way LCA, and multiple tail
750
# ancestors (only have NULL_REVISION in common)
752
self.add_rev(b'root', b'A', [], b'abc')
753
self.add_rev(b'root', b'B', [], b'def')
754
self.add_rev(b'root', b'D', [b'A'], b'Dabc')
755
self.add_rev(b'root', b'E', [b'A', b'B'], b'abcdef')
756
self.add_rev(b'root', b'F', [b'B'], b'defF')
757
self.add_rev(b'root', b'G', [b'D', b'E'], b'Dabcdef')
758
self.add_rev(b'root', b'H', [b'F', b'E'], b'abcdefF')
759
self.add_rev(b'root', b'Q', [b'E'], b'abcdef')
760
self.add_rev(b'root', b'I', [b'G', b'Q', b'H'], b'DabcdefF')
761
# Merge G & H but supersede an old line in B
762
self.add_rev(b'root', b'J', [b'H', b'Q', b'G'], b'DabcdJfF')
763
plan = self.plan_merge_vf.plan_merge(b'I', b'J')
765
('unchanged', b'D\n'),
766
('unchanged', b'a\n'),
767
('unchanged', b'b\n'),
768
('unchanged', b'c\n'),
769
('unchanged', b'd\n'),
770
('killed-b', b'e\n'),
772
('unchanged', b'f\n'),
773
('unchanged', b'F\n')],
776
def test_plan_merge_uncommitted_files(self):
777
self.setup_plan_merge_uncommitted()
778
plan = self.plan_merge_vf.plan_merge(b'B:', b'C:')
781
('unchanged', b'a\n'),
782
('killed-a', b'b\n'),
783
('killed-b', b'c\n'),
790
def test_plan_merge_insert_order(self):
791
"""Weave merges are sensitive to the order of insertion.
793
Specifically for overlapping regions, it effects which region gets put
794
'first'. And when a user resolves an overlapping merge, if they use the
795
same ordering, then the lines match the parents, if they don't only
796
*some* of the lines match.
798
self.add_rev(b'root', b'A', [], b'abcdef')
799
self.add_rev(b'root', b'B', [b'A'], b'abwxcdef')
800
self.add_rev(b'root', b'C', [b'A'], b'abyzcdef')
801
# Merge, and resolve the conflict by adding *both* sets of lines
802
# If we get the ordering wrong, these will look like new lines in D,
803
# rather than carried over from B, C
804
self.add_rev(b'root', b'D', [b'B', b'C'],
806
# Supersede the lines in B and delete the lines in C, which will
807
# conflict if they are treated as being in D
808
self.add_rev(b'root', b'E', [b'C', b'B'],
810
# Same thing for the lines in C
811
self.add_rev(b'root', b'F', [b'C'], b'abpqcdef')
812
plan = self.plan_merge_vf.plan_merge(b'D', b'E')
814
('unchanged', b'a\n'),
815
('unchanged', b'b\n'),
816
('killed-b', b'w\n'),
817
('killed-b', b'x\n'),
818
('killed-b', b'y\n'),
819
('killed-b', b'z\n'),
822
('unchanged', b'c\n'),
823
('unchanged', b'd\n'),
824
('unchanged', b'e\n'),
825
('unchanged', b'f\n')],
827
plan = self.plan_merge_vf.plan_merge(b'E', b'D')
828
# Going in the opposite direction shows the effect of the opposite plan
830
('unchanged', b'a\n'),
831
('unchanged', b'b\n'),
834
('killed-a', b'y\n'),
835
('killed-a', b'z\n'),
836
('killed-both', b'w\n'),
837
('killed-both', b'x\n'),
840
('unchanged', b'c\n'),
841
('unchanged', b'd\n'),
842
('unchanged', b'e\n'),
843
('unchanged', b'f\n')],
846
def test_plan_merge_criss_cross(self):
847
# This is specificly trying to trigger problems when using limited
848
# ancestry and weaves. The ancestry graph looks like:
849
# XX unused ancestor, should not show up in the weave
853
# B \ Introduces a line 'foo'
855
# C D E C & D both have 'foo', E has different changes
859
# F G All of C, D, E are merged into F and G, so they are
860
# all common ancestors.
862
# The specific issue with weaves:
863
# B introduced a text ('foo') that is present in both C and D.
864
# If we do not include B (because it isn't an ancestor of E), then
865
# the A=>C and A=>D look like both sides independently introduce the
866
# text ('foo'). If F does not modify the text, it would still appear
867
# to have deleted on of the versions from C or D. If G then modifies
868
# 'foo', it should appear as superseding the value in F (since it
869
# came from B), rather than conflict because of the resolution during
871
self.add_rev(b'root', b'XX', [], b'qrs')
872
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
873
self.add_rev(b'root', b'B', [b'A'], b'axcdef')
874
self.add_rev(b'root', b'C', [b'B'], b'axcdefg')
875
self.add_rev(b'root', b'D', [b'B'], b'haxcdef')
876
self.add_rev(b'root', b'E', [b'A'], b'abcdyf')
877
# Simple combining of all texts
878
self.add_rev(b'root', b'F', [b'C', b'D', b'E'], b'haxcdyfg')
879
# combine and supersede 'x'
880
self.add_rev(b'root', b'G', [b'C', b'D', b'E'], b'hazcdyfg')
881
plan = self.plan_merge_vf.plan_merge(b'F', b'G')
883
('unchanged', b'h\n'),
884
('unchanged', b'a\n'),
885
('killed-base', b'b\n'),
886
('killed-b', b'x\n'),
888
('unchanged', b'c\n'),
889
('unchanged', b'd\n'),
890
('killed-base', b'e\n'),
891
('unchanged', b'y\n'),
892
('unchanged', b'f\n'),
893
('unchanged', b'g\n')],
895
plan = self.plan_merge_vf.plan_lca_merge(b'F', b'G')
896
# This is one of the main differences between plan_merge and
897
# plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
898
# because 'x' was not present in one of the bases. However, in this
899
# case it is spurious because 'x' does not exist in the global base A.
901
('unchanged', b'h\n'),
902
('unchanged', b'a\n'),
903
('conflicted-a', b'x\n'),
905
('unchanged', b'c\n'),
906
('unchanged', b'd\n'),
907
('unchanged', b'y\n'),
908
('unchanged', b'f\n'),
909
('unchanged', b'g\n')],
912
def test_criss_cross_flip_flop(self):
913
# This is specificly trying to trigger problems when using limited
914
# ancestry and weaves. The ancestry graph looks like:
915
# XX unused ancestor, should not show up in the weave
919
# B C B & C both introduce a new line
923
# D E B & C are both merged, so both are common ancestors
924
# In the process of merging, both sides order the new
927
self.add_rev(b'root', b'XX', [], b'qrs')
928
self.add_rev(b'root', b'A', [b'XX'], b'abcdef')
929
self.add_rev(b'root', b'B', [b'A'], b'abcdgef')
930
self.add_rev(b'root', b'C', [b'A'], b'abcdhef')
931
self.add_rev(b'root', b'D', [b'B', b'C'], b'abcdghef')
932
self.add_rev(b'root', b'E', [b'C', b'B'], b'abcdhgef')
933
plan = list(self.plan_merge_vf.plan_merge(b'D', b'E'))
935
('unchanged', b'a\n'),
936
('unchanged', b'b\n'),
937
('unchanged', b'c\n'),
938
('unchanged', b'd\n'),
940
('unchanged', b'g\n'),
941
('killed-b', b'h\n'),
942
('unchanged', b'e\n'),
943
('unchanged', b'f\n'),
945
pwm = versionedfile.PlanWeaveMerge(plan)
946
self.assertEqualDiff(b'a\nb\nc\nd\ng\nh\ne\nf\n',
947
b''.join(pwm.base_from_plan()))
948
# Reversing the order reverses the merge plan, and final order of 'hg'
950
plan = list(self.plan_merge_vf.plan_merge(b'E', b'D'))
952
('unchanged', b'a\n'),
953
('unchanged', b'b\n'),
954
('unchanged', b'c\n'),
955
('unchanged', b'd\n'),
957
('unchanged', b'h\n'),
958
('killed-b', b'g\n'),
959
('unchanged', b'e\n'),
960
('unchanged', b'f\n'),
962
pwm = versionedfile.PlanWeaveMerge(plan)
963
self.assertEqualDiff(b'a\nb\nc\nd\nh\ng\ne\nf\n',
964
b''.join(pwm.base_from_plan()))
965
# This is where lca differs, in that it (fairly correctly) determines
966
# that there is a conflict because both sides resolved the merge
968
plan = list(self.plan_merge_vf.plan_lca_merge(b'D', b'E'))
970
('unchanged', b'a\n'),
971
('unchanged', b'b\n'),
972
('unchanged', b'c\n'),
973
('unchanged', b'd\n'),
974
('conflicted-b', b'h\n'),
975
('unchanged', b'g\n'),
976
('conflicted-a', b'h\n'),
977
('unchanged', b'e\n'),
978
('unchanged', b'f\n'),
980
pwm = versionedfile.PlanWeaveMerge(plan)
981
self.assertEqualDiff(b'a\nb\nc\nd\ng\ne\nf\n',
982
b''.join(pwm.base_from_plan()))
983
# Reversing it changes what line is doubled, but still gives a
985
plan = list(self.plan_merge_vf.plan_lca_merge(b'E', b'D'))
987
('unchanged', b'a\n'),
988
('unchanged', b'b\n'),
989
('unchanged', b'c\n'),
990
('unchanged', b'd\n'),
991
('conflicted-b', b'g\n'),
992
('unchanged', b'h\n'),
993
('conflicted-a', b'g\n'),
994
('unchanged', b'e\n'),
995
('unchanged', b'f\n'),
997
pwm = versionedfile.PlanWeaveMerge(plan)
998
self.assertEqualDiff(b'a\nb\nc\nd\nh\ne\nf\n',
999
b''.join(pwm.base_from_plan()))
1001
def assertRemoveExternalReferences(self, filtered_parent_map,
1002
child_map, tails, parent_map):
1003
"""Assert results for _PlanMerge._remove_external_references."""
1004
(act_filtered_parent_map, act_child_map,
1005
act_tails) = _PlanMerge._remove_external_references(parent_map)
1007
# The parent map *should* preserve ordering, but the ordering of
1008
# children is not strictly defined
1009
# child_map = dict((k, sorted(children))
1010
# for k, children in child_map.iteritems())
1011
# act_child_map = dict(k, sorted(children)
1012
# for k, children in act_child_map.iteritems())
1013
self.assertEqual(filtered_parent_map, act_filtered_parent_map)
1014
self.assertEqual(child_map, act_child_map)
1015
self.assertEqual(sorted(tails), sorted(act_tails))
1017
def test__remove_external_references(self):
1018
# First, nothing to remove
1019
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1020
{1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
1021
# The reverse direction
1022
self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
1023
{3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
1025
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1026
{1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
1028
self.assertRemoveExternalReferences(
1029
{4: [2, 3], 3: [], 2: [1], 1: []},
1030
{1: [2], 2: [4], 3: [4], 4: []},
1032
{4: [2, 3], 3: [5], 2: [1], 1: [6]})
1034
self.assertRemoveExternalReferences(
1035
{1: [3], 2: [3, 4], 3: [], 4: []},
1036
{1: [], 2: [], 3: [1, 2], 4: [2]},
1038
{1: [3], 2: [3, 4], 3: [5], 4: []})
1040
def assertPruneTails(self, pruned_map, tails, parent_map):
1042
for key, parent_keys in parent_map.items():
1043
child_map.setdefault(key, [])
1044
for pkey in parent_keys:
1045
child_map.setdefault(pkey, []).append(key)
1046
_PlanMerge._prune_tails(parent_map, child_map, tails)
1047
self.assertEqual(pruned_map, parent_map)
1049
def test__prune_tails(self):
1050
# Nothing requested to prune
1051
self.assertPruneTails({1: [], 2: [], 3: []}, [],
1052
{1: [], 2: [], 3: []})
1053
# Prune a single entry
1054
self.assertPruneTails({1: [], 3: []}, [2],
1055
{1: [], 2: [], 3: []})
1057
self.assertPruneTails({1: []}, [3],
1058
{1: [], 2: [3], 3: []})
1059
# Prune a chain with a diamond
1060
self.assertPruneTails({1: []}, [5],
1061
{1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
1062
# Prune a partial chain
1063
self.assertPruneTails({1: [6], 6:[]}, [5],
1064
{1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
1066
# Prune a chain with multiple tips, that pulls out intermediates
1067
self.assertPruneTails({1:[3], 3:[]}, [4, 5],
1068
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1069
self.assertPruneTails({1:[3], 3:[]}, [5, 4],
1070
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1072
def test_subtract_plans(self):
1074
('unchanged', b'a\n'),
1076
('killed-a', b'c\n'),
1079
('killed-b', b'f\n'),
1080
('killed-b', b'g\n'),
1083
('unchanged', b'a\n'),
1085
('killed-a', b'c\n'),
1088
('killed-b', b'f\n'),
1089
('killed-b', b'i\n'),
1092
('unchanged', b'a\n'),
1094
('killed-a', b'c\n'),
1096
('unchanged', b'f\n'),
1097
('killed-b', b'i\n'),
1099
self.assertEqual(subtracted_plan,
1100
list(_PlanMerge._subtract_plans(old_plan, new_plan)))
1102
def setup_merge_with_base(self):
1103
self.add_rev(b'root', b'COMMON', [], b'abc')
1104
self.add_rev(b'root', b'THIS', [b'COMMON'], b'abcd')
1105
self.add_rev(b'root', b'BASE', [b'COMMON'], b'eabc')
1106
self.add_rev(b'root', b'OTHER', [b'BASE'], b'eafb')
1108
def test_plan_merge_with_base(self):
1109
self.setup_merge_with_base()
1110
plan = self.plan_merge_vf.plan_merge(b'THIS', b'OTHER', b'BASE')
1111
self.assertEqual([('unchanged', b'a\n'),
1113
('unchanged', b'b\n'),
1114
('killed-b', b'c\n'),
1118
def test_plan_lca_merge(self):
1119
self.setup_plan_merge()
1120
plan = self.plan_merge_vf.plan_lca_merge(b'B', b'C')
1123
('unchanged', b'a\n'),
1124
('killed-b', b'c\n'),
1127
('killed-a', b'b\n'),
1128
('unchanged', b'g\n')],
1131
def test_plan_lca_merge_uncommitted_files(self):
1132
self.setup_plan_merge_uncommitted()
1133
plan = self.plan_merge_vf.plan_lca_merge(b'B:', b'C:')
1136
('unchanged', b'a\n'),
1137
('killed-b', b'c\n'),
1140
('killed-a', b'b\n'),
1141
('unchanged', b'g\n')],
1144
def test_plan_lca_merge_with_base(self):
1145
self.setup_merge_with_base()
1146
plan = self.plan_merge_vf.plan_lca_merge(b'THIS', b'OTHER', b'BASE')
1147
self.assertEqual([('unchanged', b'a\n'),
1149
('unchanged', b'b\n'),
1150
('killed-b', b'c\n'),
1154
def test_plan_lca_merge_with_criss_cross(self):
1155
self.add_version((b'root', b'ROOT'), [], b'abc')
1156
# each side makes a change
1157
self.add_version((b'root', b'REV1'), [(b'root', b'ROOT')], b'abcd')
1158
self.add_version((b'root', b'REV2'), [(b'root', b'ROOT')], b'abce')
1159
# both sides merge, discarding others' changes
1160
self.add_version((b'root', b'LCA1'),
1161
[(b'root', b'REV1'), (b'root', b'REV2')], b'abcd')
1162
self.add_version((b'root', b'LCA2'),
1163
[(b'root', b'REV1'), (b'root', b'REV2')], b'fabce')
1164
plan = self.plan_merge_vf.plan_lca_merge(b'LCA1', b'LCA2')
1165
self.assertEqual([('new-b', b'f\n'),
1166
('unchanged', b'a\n'),
1167
('unchanged', b'b\n'),
1168
('unchanged', b'c\n'),
1169
('conflicted-a', b'd\n'),
1170
('conflicted-b', b'e\n'),
1173
def test_plan_lca_merge_with_null(self):
1174
self.add_version((b'root', b'A'), [], b'ab')
1175
self.add_version((b'root', b'B'), [], b'bc')
1176
plan = self.plan_merge_vf.plan_lca_merge(b'A', b'B')
1177
self.assertEqual([('new-a', b'a\n'),
1178
('unchanged', b'b\n'),
1182
def test_plan_merge_with_delete_and_change(self):
1183
self.add_rev(b'root', b'C', [], b'a')
1184
self.add_rev(b'root', b'A', [b'C'], b'b')
1185
self.add_rev(b'root', b'B', [b'C'], b'')
1186
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1187
self.assertEqual([('killed-both', b'a\n'),
1191
def test_plan_merge_with_move_and_change(self):
1192
self.add_rev(b'root', b'C', [], b'abcd')
1193
self.add_rev(b'root', b'A', [b'C'], b'acbd')
1194
self.add_rev(b'root', b'B', [b'C'], b'aBcd')
1195
plan = self.plan_merge_vf.plan_merge(b'A', b'B')
1196
self.assertEqual([('unchanged', b'a\n'),
1198
('killed-b', b'b\n'),
1200
('killed-a', b'c\n'),
1201
('unchanged', b'd\n'),
1205
class LoggingMerger(object):
1206
# These seem to be the required attributes
1207
requires_base = False
1208
supports_reprocess = False
1209
supports_show_base = False
1210
supports_cherrypick = False
1211
# We intentionally do not define supports_lca_trees
1213
def __init__(self, *args, **kwargs):
1215
self.kwargs = kwargs
1218
class TestMergerBase(TestCaseWithMemoryTransport):
1219
"""Common functionality for Merger tests that don't write to disk."""
1221
def get_builder(self):
1222
builder = self.make_branch_builder('path')
1223
builder.start_series()
1224
self.addCleanup(builder.finish_series)
1227
def setup_simple_graph(self):
1228
"""Create a simple 3-node graph.
1230
:return: A BranchBuilder
1237
builder = self.get_builder()
1238
builder.build_snapshot(None,
1239
[('add', ('', None, 'directory', None))],
1240
revision_id=b'A-id' )
1241
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1242
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1245
def setup_criss_cross_graph(self):
1246
"""Create a 5-node graph with a criss-cross.
1248
:return: A BranchBuilder
1255
builder = self.setup_simple_graph()
1256
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1257
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1260
def make_Merger(self, builder, other_revision_id, interesting_files=None):
1261
"""Make a Merger object from a branch builder"""
1262
mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1263
mem_tree.lock_write()
1264
self.addCleanup(mem_tree.unlock)
1265
merger = _mod_merge.Merger.from_revision_ids(
1266
mem_tree, other_revision_id)
1267
merger.set_interesting_files(interesting_files)
1268
merger.merge_type = _mod_merge.Merge3Merger
1272
class TestMergerInMemory(TestMergerBase):
1274
def test_cache_trees_with_revision_ids_None(self):
1275
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1276
original_cache = dict(merger._cached_trees)
1277
merger.cache_trees_with_revision_ids([None])
1278
self.assertEqual(original_cache, merger._cached_trees)
1280
def test_cache_trees_with_revision_ids_no_revision_id(self):
1281
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1282
original_cache = dict(merger._cached_trees)
1283
tree = self.make_branch_and_memory_tree('tree')
1284
merger.cache_trees_with_revision_ids([tree])
1285
self.assertEqual(original_cache, merger._cached_trees)
1287
def test_cache_trees_with_revision_ids_having_revision_id(self):
1288
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1289
original_cache = dict(merger._cached_trees)
1290
tree = merger.this_branch.repository.revision_tree(b'B-id')
1291
original_cache[b'B-id'] = tree
1292
merger.cache_trees_with_revision_ids([tree])
1293
self.assertEqual(original_cache, merger._cached_trees)
1295
def test_find_base(self):
1296
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1297
self.assertEqual(b'A-id', merger.base_rev_id)
1298
self.assertFalse(merger._is_criss_cross)
1299
self.assertIs(None, merger._lca_trees)
1301
def test_find_base_criss_cross(self):
1302
builder = self.setup_criss_cross_graph()
1303
merger = self.make_Merger(builder, b'E-id')
1304
self.assertEqual(b'A-id', merger.base_rev_id)
1305
self.assertTrue(merger._is_criss_cross)
1306
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1307
for t in merger._lca_trees])
1308
# If we swap the order, we should get a different lca order
1309
builder.build_snapshot([b'E-id'], [], revision_id=b'F-id')
1310
merger = self.make_Merger(builder, b'D-id')
1311
self.assertEqual([b'C-id', b'B-id'], [t.get_revision_id()
1312
for t in merger._lca_trees])
1314
def test_find_base_triple_criss_cross(self):
1317
# B C F # F is merged into both branches
1324
builder = self.setup_criss_cross_graph()
1325
builder.build_snapshot([b'A-id'], [], revision_id=b'F-id')
1326
builder.build_snapshot([b'E-id', b'F-id'], [], revision_id=b'H-id')
1327
builder.build_snapshot([b'D-id', b'F-id'], [], revision_id=b'G-id')
1328
merger = self.make_Merger(builder, b'H-id')
1329
self.assertEqual([b'B-id', b'C-id', b'F-id'],
1330
[t.get_revision_id() for t in merger._lca_trees])
1332
def test_find_base_new_root_criss_cross(self):
1338
builder = self.get_builder()
1339
builder.build_snapshot(None,
1340
[('add', ('', None, 'directory', None))],
1341
revision_id=b'A-id')
1342
builder.build_snapshot([],
1343
[('add', ('', None, 'directory', None))],
1344
revision_id=b'B-id')
1345
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'D-id')
1346
builder.build_snapshot([b'A-id', b'B-id'], [], revision_id=b'C-id')
1347
merger = self.make_Merger(builder, b'D-id')
1348
self.assertEqual(b'A-id', merger.base_rev_id)
1349
self.assertTrue(merger._is_criss_cross)
1350
self.assertEqual([b'A-id', b'B-id'], [t.get_revision_id()
1351
for t in merger._lca_trees])
1353
def test_no_criss_cross_passed_to_merge_type(self):
1354
class LCATreesMerger(LoggingMerger):
1355
supports_lca_trees = True
1357
merger = self.make_Merger(self.setup_simple_graph(), b'C-id')
1358
merger.merge_type = LCATreesMerger
1359
merge_obj = merger.make_merger()
1360
self.assertIsInstance(merge_obj, LCATreesMerger)
1361
self.assertFalse('lca_trees' in merge_obj.kwargs)
1363
def test_criss_cross_passed_to_merge_type(self):
1364
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1365
merger.merge_type = _mod_merge.Merge3Merger
1366
merge_obj = merger.make_merger()
1367
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1368
for t in merger._lca_trees])
1370
def test_criss_cross_not_supported_merge_type(self):
1371
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1372
# We explicitly do not define supports_lca_trees
1373
merger.merge_type = LoggingMerger
1374
merge_obj = merger.make_merger()
1375
self.assertIsInstance(merge_obj, LoggingMerger)
1376
self.assertFalse('lca_trees' in merge_obj.kwargs)
1378
def test_criss_cross_unsupported_merge_type(self):
1379
class UnsupportedLCATreesMerger(LoggingMerger):
1380
supports_lca_trees = False
1382
merger = self.make_Merger(self.setup_criss_cross_graph(), b'E-id')
1383
merger.merge_type = UnsupportedLCATreesMerger
1384
merge_obj = merger.make_merger()
1385
self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
1386
self.assertFalse('lca_trees' in merge_obj.kwargs)
1389
class TestMergerEntriesLCA(TestMergerBase):
1391
def make_merge_obj(self, builder, other_revision_id,
1392
interesting_files=None):
1393
merger = self.make_Merger(builder, other_revision_id,
1394
interesting_files=interesting_files)
1395
return merger.make_merger()
1397
def test_simple(self):
1398
builder = self.get_builder()
1399
builder.build_snapshot(None,
1400
[('add', (u'', b'a-root-id', 'directory', None)),
1401
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1402
revision_id=b'A-id')
1403
builder.build_snapshot([b'A-id'],
1404
[('modify', ('a', b'a\nb\nC\nc\n'))],
1405
revision_id=b'C-id')
1406
builder.build_snapshot([b'A-id'],
1407
[('modify', ('a', b'a\nB\nb\nc\n'))],
1408
revision_id=b'B-id')
1409
builder.build_snapshot([b'C-id', b'B-id'],
1410
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1411
revision_id=b'E-id')
1412
builder.build_snapshot([b'B-id', b'C-id'],
1413
[('modify', ('a', b'a\nB\nb\nC\nc\n'))],
1414
revision_id=b'D-id', )
1415
merge_obj = self.make_merge_obj(builder, b'E-id')
1417
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1418
for t in merge_obj._lca_trees])
1419
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1420
entries = list(merge_obj._entries_lca())
1422
# (file_id, changed, parents, names, executable)
1423
# BASE, lca1, lca2, OTHER, THIS
1424
root_id = b'a-root-id'
1425
self.assertEqual([(b'a-id', True,
1426
((u'a', [u'a', u'a']), u'a', u'a'),
1427
((root_id, [root_id, root_id]), root_id, root_id),
1428
((u'a', [u'a', u'a']), u'a', u'a'),
1429
((False, [False, False]), False, False)),
1432
def test_not_in_base(self):
1433
# LCAs all have the same last-modified revision for the file, as do
1434
# the tips, but the base has something different
1435
# A base, doesn't have the file
1437
# B C B introduces 'foo', C introduces 'bar'
1439
# D E D and E now both have 'foo' and 'bar'
1441
# F G the files are now in F, G, D and E, but not in A
1444
builder = self.get_builder()
1445
builder.build_snapshot(None,
1446
[('add', (u'', b'a-root-id', 'directory', None))],
1447
revision_id=b'A-id')
1448
builder.build_snapshot([b'A-id'],
1449
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1450
revision_id=b'B-id')
1451
builder.build_snapshot([b'A-id'],
1452
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1453
revision_id=b'C-id')
1454
builder.build_snapshot([b'B-id', b'C-id'],
1455
[('add', (u'bar', b'bar-id', 'file', b'd\ne\nf\n'))],
1456
revision_id=b'D-id')
1457
builder.build_snapshot([b'C-id', b'B-id'],
1458
[('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
1459
revision_id=b'E-id')
1460
builder.build_snapshot([b'E-id', b'D-id'],
1461
[('modify', (u'bar', b'd\ne\nf\nG\n'))],
1462
revision_id=b'G-id')
1463
builder.build_snapshot([b'D-id', b'E-id'], [], revision_id=b'F-id')
1464
merge_obj = self.make_merge_obj(builder, b'G-id')
1466
self.assertEqual([b'D-id', b'E-id'], [t.get_revision_id()
1467
for t in merge_obj._lca_trees])
1468
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1469
entries = list(merge_obj._entries_lca())
1470
root_id = b'a-root-id'
1471
self.assertEqual([(b'bar-id', True,
1472
((None, [u'bar', u'bar']), u'bar', u'bar'),
1473
((None, [root_id, root_id]), root_id, root_id),
1474
((None, [u'bar', u'bar']), u'bar', u'bar'),
1475
((None, [False, False]), False, False)),
1478
def test_not_in_this(self):
1479
builder = self.get_builder()
1480
builder.build_snapshot(None,
1481
[('add', (u'', b'a-root-id', 'directory', None)),
1482
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1483
revision_id=b'A-id')
1484
builder.build_snapshot([b'A-id'],
1485
[('modify', ('a', b'a\nB\nb\nc\n'))],
1486
revision_id=b'B-id')
1487
builder.build_snapshot([b'A-id'],
1488
[('modify', ('a', b'a\nb\nC\nc\n'))],
1489
revision_id=b'C-id')
1490
builder.build_snapshot([b'C-id', b'B-id'],
1491
[('modify', ('a', b'a\nB\nb\nC\nc\nE\n'))],
1492
revision_id=b'E-id')
1493
builder.build_snapshot([b'B-id', b'C-id'],
1494
[('unversion', 'a')],
1495
revision_id=b'D-id')
1496
merge_obj = self.make_merge_obj(builder, b'E-id')
1498
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1499
for t in merge_obj._lca_trees])
1500
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1502
entries = list(merge_obj._entries_lca())
1503
root_id = b'a-root-id'
1504
self.assertEqual([(b'a-id', True,
1505
((u'a', [u'a', u'a']), u'a', None),
1506
((root_id, [root_id, root_id]), root_id, None),
1507
((u'a', [u'a', u'a']), u'a', None),
1508
((False, [False, False]), False, None)),
1511
def test_file_not_in_one_lca(self):
1514
# B C # B no file, C introduces a file
1516
# D E # D and E both have the file, unchanged from C
1517
builder = self.get_builder()
1518
builder.build_snapshot(None,
1519
[('add', (u'', b'a-root-id', 'directory', None))],
1520
revision_id=b'A-id')
1521
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1522
builder.build_snapshot([b'A-id'],
1523
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1524
revision_id=b'C-id')
1525
builder.build_snapshot([b'C-id', b'B-id'],
1526
[], revision_id=b'E-id') # Inherited from C
1527
builder.build_snapshot([b'B-id', b'C-id'], # Merged from C
1528
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1529
revision_id=b'D-id')
1530
merge_obj = self.make_merge_obj(builder, b'E-id')
1532
self.assertEqual([b'B-id', b'C-id'], [t.get_revision_id()
1533
for t in merge_obj._lca_trees])
1534
self.assertEqual(b'A-id', merge_obj.base_tree.get_revision_id())
1536
entries = list(merge_obj._entries_lca())
1537
self.assertEqual([], entries)
1539
def test_not_in_other(self):
1540
builder = self.get_builder()
1541
builder.build_snapshot(None,
1542
[('add', (u'', b'a-root-id', 'directory', None)),
1543
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1544
revision_id=b'A-id')
1545
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1546
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1547
builder.build_snapshot(
1549
[('unversion', 'a')], revision_id=b'E-id')
1550
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1551
merge_obj = self.make_merge_obj(builder, b'E-id')
1553
entries = list(merge_obj._entries_lca())
1554
root_id = b'a-root-id'
1555
self.assertEqual([(b'a-id', True,
1556
((u'a', [u'a', u'a']), None, u'a'),
1557
((root_id, [root_id, root_id]), None, root_id),
1558
((u'a', [u'a', u'a']), None, u'a'),
1559
((False, [False, False]), None, False)),
1562
def test_not_in_other_or_lca(self):
1563
# A base, introduces 'foo'
1565
# B C B nothing, C deletes foo
1567
# D E D restores foo (same as B), E leaves it deleted
1569
# A => B, no changes
1570
# A => C, delete foo (C should supersede B)
1571
# C => D, restore foo
1572
# C => E, no changes
1573
# D would then win 'cleanly' and no record would be given
1574
builder = self.get_builder()
1575
builder.build_snapshot(None,
1576
[('add', (u'', b'a-root-id', 'directory', None)),
1577
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1578
revision_id=b'A-id')
1579
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1580
builder.build_snapshot([b'A-id'],
1581
[('unversion', 'foo')], revision_id=b'C-id')
1582
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1583
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1584
merge_obj = self.make_merge_obj(builder, b'E-id')
1586
entries = list(merge_obj._entries_lca())
1587
self.assertEqual([], entries)
1589
def test_not_in_other_mod_in_lca1_not_in_lca2(self):
1590
# A base, introduces 'foo'
1592
# B C B changes 'foo', C deletes foo
1594
# D E D restores foo (same as B), E leaves it deleted (as C)
1596
# A => B, modified foo
1597
# A => C, delete foo, C does not supersede B
1598
# B => D, no changes
1599
# C => D, resolve in favor of B
1600
# B => E, resolve in favor of E
1601
# C => E, no changes
1602
# In this case, we have a conflict of how the changes were resolved. E
1603
# picked C and D picked B, so we should issue a conflict
1604
builder = self.get_builder()
1605
builder.build_snapshot(None,
1606
[('add', (u'', b'a-root-id', 'directory', None)),
1607
('add', (u'foo', b'foo-id', 'file', b'content\n'))],
1608
revision_id=b'A-id')
1609
builder.build_snapshot([b'A-id'], [
1610
('modify', ('foo', b'new-content\n'))],
1611
revision_id=b'B-id')
1612
builder.build_snapshot([b'A-id'],
1613
[('unversion', 'foo')],
1614
revision_id=b'C-id')
1615
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1616
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1617
merge_obj = self.make_merge_obj(builder, b'E-id')
1619
entries = list(merge_obj._entries_lca())
1620
root_id = b'a-root-id'
1621
self.assertEqual([(b'foo-id', True,
1622
((u'foo', [u'foo', None]), None, u'foo'),
1623
((root_id, [root_id, None]), None, root_id),
1624
((u'foo', [u'foo', None]), None, 'foo'),
1625
((False, [False, None]), None, False)),
1628
def test_only_in_one_lca(self):
1631
# B C B nothing, C add file
1633
# D E D still has nothing, E removes file
1636
# C => D, removed the file
1638
# C => E, removed the file
1639
# Thus D & E have identical changes, and this is a no-op
1642
# A => C, add file, thus C supersedes B
1643
# w/ C=BASE, D=THIS, E=OTHER we have 'happy convergence'
1644
builder = self.get_builder()
1645
builder.build_snapshot(None,
1646
[('add', (u'', b'a-root-id', 'directory', None))],
1647
revision_id=b'A-id')
1648
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1649
builder.build_snapshot([b'A-id'],
1650
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1651
revision_id=b'C-id')
1652
builder.build_snapshot([b'C-id', b'B-id'],
1653
[('unversion', 'a')],
1654
revision_id=b'E-id')
1655
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1656
merge_obj = self.make_merge_obj(builder, b'E-id')
1658
entries = list(merge_obj._entries_lca())
1659
self.assertEqual([], entries)
1661
def test_only_in_other(self):
1662
builder = self.get_builder()
1663
builder.build_snapshot(None,
1664
[('add', (u'', b'a-root-id', 'directory', None))],
1665
revision_id=b'A-id')
1666
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1667
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1668
builder.build_snapshot([b'C-id', b'B-id'],
1669
[('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
1670
revision_id=b'E-id')
1671
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1672
merge_obj = self.make_merge_obj(builder, b'E-id')
1674
entries = list(merge_obj._entries_lca())
1675
root_id = b'a-root-id'
1676
self.assertEqual([(b'a-id', True,
1677
((None, [None, None]), u'a', None),
1678
((None, [None, None]), root_id, None),
1679
((None, [None, None]), u'a', None),
1680
((None, [None, None]), False, None)),
1683
def test_one_lca_supersedes(self):
1684
# One LCA supersedes the other LCAs last modified value, but the
1685
# value is not the same as BASE.
1686
# A base, introduces 'foo', last mod A
1688
# B C B modifies 'foo' (mod B), C does nothing (mod A)
1690
# D E D does nothing (mod B), E updates 'foo' (mod E)
1692
# F G F updates 'foo' (mod F). G does nothing (mod E)
1694
# At this point, G should not be considered to modify 'foo', even
1695
# though its LCAs disagree. This is because the modification in E
1696
# completely supersedes the value in D.
1697
builder = self.get_builder()
1698
builder.build_snapshot(None,
1699
[('add', (u'', b'a-root-id', 'directory', None)),
1700
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1701
revision_id=b'A-id')
1702
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1703
builder.build_snapshot([b'A-id'],
1704
[('modify', ('foo', b'B content\n'))],
1705
revision_id=b'B-id')
1706
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1707
builder.build_snapshot([b'C-id', b'B-id'],
1708
[('modify', ('foo', b'E content\n'))],
1709
revision_id=b'E-id')
1710
builder.build_snapshot([b'E-id', b'D-id'], [], revision_id=b'G-id')
1711
builder.build_snapshot([b'D-id', b'E-id'],
1712
[('modify', ('foo', b'F content\n'))],
1713
revision_id=b'F-id')
1714
merge_obj = self.make_merge_obj(builder, b'G-id')
1716
self.assertEqual([], list(merge_obj._entries_lca()))
1718
def test_one_lca_supersedes_path(self):
1719
# Double-criss-cross merge, the ultimate base value is different from
1723
# B C B value 'bar', C = 'foo'
1725
# D E D = 'bar', E supersedes to 'bing'
1727
# F G F = 'bing', G supersedes to 'barry'
1729
# In this case, we technically should not care about the value 'bar' for
1730
# D, because it was clearly superseded by E's 'bing'. The
1731
# per-file/attribute graph would actually look like:
1740
# Because the other side of the merge never modifies the value, it just
1741
# takes the value from the merge.
1743
# ATM this fails because we will prune 'foo' from the LCAs, but we
1744
# won't prune 'bar'. This is getting far off into edge-case land, so we
1745
# aren't supporting it yet.
1747
builder = self.get_builder()
1748
builder.build_snapshot(None,
1749
[('add', (u'', b'a-root-id', 'directory', None)),
1750
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1751
revision_id=b'A-id')
1752
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1753
builder.build_snapshot([b'A-id'],
1754
[('rename', ('foo', 'bar'))],
1755
revision_id=b'B-id')
1756
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1757
builder.build_snapshot([b'C-id', b'B-id'],
1758
[('rename', ('foo', 'bing'))],
1759
revision_id=b'E-id') # override to bing
1760
builder.build_snapshot([b'E-id', b'D-id'],
1761
[('rename', ('bing', 'barry'))],
1762
revision_id=b'G-id') # override to barry
1763
builder.build_snapshot([b'D-id', b'E-id'],
1764
[('rename', ('bar', 'bing'))],
1765
revision_id=b'F-id') # Merge in E's change
1766
merge_obj = self.make_merge_obj(builder, b'G-id')
1768
self.expectFailure("We don't do an actual heads() check on lca values,"
1769
" or use the per-attribute graph",
1770
self.assertEqual, [], list(merge_obj._entries_lca()))
1772
def test_one_lca_accidentally_pruned(self):
1773
# Another incorrect resolution from the same basic flaw:
1776
# B C B value 'bar', C = 'foo'
1778
# D E D = 'bar', E reverts to 'foo'
1780
# F G F = 'bing', G switches to 'bar'
1782
# 'bar' will not be seen as an interesting change, because 'foo' will
1783
# be pruned from the LCAs, even though it was newly introduced by E
1785
builder = self.get_builder()
1786
builder.build_snapshot(None,
1787
[('add', (u'', b'a-root-id', 'directory', None)),
1788
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1789
revision_id=b'A-id')
1790
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1791
builder.build_snapshot([b'A-id'],
1792
[('rename', ('foo', 'bar'))],
1793
revision_id=b'B-id')
1794
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1795
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1796
builder.build_snapshot([b'E-id', b'D-id'],
1797
[('rename', ('foo', 'bar'))],
1798
revision_id=b'G-id')
1799
builder.build_snapshot([b'D-id', b'E-id'],
1800
[('rename', ('bar', 'bing'))],
1801
revision_id=b'F-id') # should end up conflicting
1802
merge_obj = self.make_merge_obj(builder, b'G-id')
1804
entries = list(merge_obj._entries_lca())
1805
root_id = b'a-root-id'
1806
self.expectFailure("We prune values from BASE even when relevant.",
1809
((root_id, [root_id, root_id]), root_id, root_id),
1810
((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
1811
((False, [False, False]), False, False)),
1814
def test_both_sides_revert(self):
1815
# Both sides of a criss-cross revert the text to the lca
1816
# A base, introduces 'foo'
1818
# B C B modifies 'foo', C modifies 'foo'
1820
# D E D reverts to B, E reverts to C
1821
# This should conflict
1822
builder = self.get_builder()
1823
builder.build_snapshot(None,
1824
[('add', (u'', b'a-root-id', 'directory', None)),
1825
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1826
revision_id=b'A-id')
1827
builder.build_snapshot([b'A-id'],
1828
[('modify', ('foo', b'B content\n'))],
1829
revision_id=b'B-id')
1830
builder.build_snapshot([b'A-id'],
1831
[('modify', ('foo', b'C content\n'))],
1832
revision_id=b'C-id')
1833
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1834
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1835
merge_obj = self.make_merge_obj(builder, b'E-id')
1837
entries = list(merge_obj._entries_lca())
1838
root_id = b'a-root-id'
1839
self.assertEqual([(b'foo-id', True,
1840
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1841
((root_id, [root_id, root_id]), root_id, root_id),
1842
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1843
((False, [False, False]), False, False)),
1846
def test_different_lca_resolve_one_side_updates_content(self):
1847
# Both sides converge, but then one side updates the text.
1848
# A base, introduces 'foo'
1850
# B C B modifies 'foo', C modifies 'foo'
1852
# D E D reverts to B, E reverts to C
1854
# F F updates to a new value
1855
# We need to emit an entry for 'foo', because D & E differed on the
1857
builder = self.get_builder()
1858
builder.build_snapshot(None,
1859
[('add', (u'', b'a-root-id', 'directory', None)),
1860
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1861
revision_id=b'A-id')
1862
builder.build_snapshot([b'A-id'],
1863
[('modify', ('foo', b'B content\n'))],
1864
revision_id=b'B-id')
1865
builder.build_snapshot([b'A-id'],
1866
[('modify', ('foo', b'C content\n'))],
1867
revision_id=b'C-id', )
1868
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1869
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1870
builder.build_snapshot([b'D-id'],
1871
[('modify', ('foo', b'F content\n'))],
1872
revision_id=b'F-id')
1873
merge_obj = self.make_merge_obj(builder, b'E-id')
1875
entries = list(merge_obj._entries_lca())
1876
root_id = b'a-root-id'
1877
self.assertEqual([(b'foo-id', True,
1878
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1879
((root_id, [root_id, root_id]), root_id, root_id),
1880
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1881
((False, [False, False]), False, False)),
1884
def test_same_lca_resolution_one_side_updates_content(self):
1885
# Both sides converge, but then one side updates the text.
1886
# A base, introduces 'foo'
1888
# B C B modifies 'foo', C modifies 'foo'
1890
# D E D and E use C's value
1892
# F F updates to a new value
1893
# I think it is a bug that this conflicts, but we don't have a way to
1894
# detect otherwise. And because of:
1895
# test_different_lca_resolve_one_side_updates_content
1896
# We need to conflict.
1898
builder = self.get_builder()
1899
builder.build_snapshot(None,
1900
[('add', (u'', b'a-root-id', 'directory', None)),
1901
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
1902
revision_id=b'A-id')
1903
builder.build_snapshot([b'A-id'],
1904
[('modify', ('foo', b'B content\n'))],
1905
revision_id=b'B-id')
1906
builder.build_snapshot([b'A-id'],
1907
[('modify', ('foo', b'C content\n'))],
1908
revision_id=b'C-id')
1909
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1910
builder.build_snapshot([b'B-id', b'C-id'],
1911
[('modify', ('foo', b'C content\n'))],
1912
revision_id=b'D-id') # Same as E
1913
builder.build_snapshot([b'D-id'],
1914
[('modify', ('foo', b'F content\n'))],
1915
revision_id=b'F-id')
1916
merge_obj = self.make_merge_obj(builder, b'E-id')
1918
entries = list(merge_obj._entries_lca())
1919
self.expectFailure("We don't detect that LCA resolution was the"
1920
" same on both sides",
1921
self.assertEqual, [], entries)
1923
def test_only_path_changed(self):
1924
builder = self.get_builder()
1925
builder.build_snapshot(None,
1926
[('add', (u'', b'a-root-id', 'directory', None)),
1927
('add', (u'a', b'a-id', 'file', b'content\n'))],
1928
revision_id=b'A-id')
1929
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1930
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1931
builder.build_snapshot([b'C-id', b'B-id'],
1932
[('rename', (u'a', u'b'))],
1933
revision_id=b'E-id')
1934
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1935
merge_obj = self.make_merge_obj(builder, b'E-id')
1936
entries = list(merge_obj._entries_lca())
1937
root_id = b'a-root-id'
1938
# The content was not changed, only the path
1939
self.assertEqual([(b'a-id', False,
1940
((u'a', [u'a', u'a']), u'b', u'a'),
1941
((root_id, [root_id, root_id]), root_id, root_id),
1942
((u'a', [u'a', u'a']), u'b', u'a'),
1943
((False, [False, False]), False, False)),
1946
def test_kind_changed(self):
1947
# Identical content, except 'D' changes a-id into a directory
1948
builder = self.get_builder()
1949
builder.build_snapshot(None,
1950
[('add', (u'', b'a-root-id', 'directory', None)),
1951
('add', (u'a', b'a-id', 'file', b'content\n'))],
1952
revision_id=b'A-id')
1953
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1954
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1955
builder.build_snapshot([b'C-id', b'B-id'],
1956
[('unversion', 'a'),
1958
('add', (u'a', b'a-id', 'directory', None))],
1959
revision_id=b'E-id')
1960
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
1961
merge_obj = self.make_merge_obj(builder, b'E-id')
1962
entries = list(merge_obj._entries_lca())
1963
root_id = b'a-root-id'
1964
# Only the kind was changed (content)
1965
self.assertEqual([(b'a-id', True,
1966
((u'a', [u'a', u'a']), u'a', u'a'),
1967
((root_id, [root_id, root_id]), root_id, root_id),
1968
((u'a', [u'a', u'a']), u'a', u'a'),
1969
((False, [False, False]), False, False)),
1972
def test_this_changed_kind(self):
1973
# Identical content, but THIS changes a file to a directory
1974
builder = self.get_builder()
1975
builder.build_snapshot(None,
1976
[('add', (u'', b'a-root-id', 'directory', None)),
1977
('add', (u'a', b'a-id', 'file', b'content\n'))],
1978
revision_id=b'A-id')
1979
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
1980
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
1981
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
1982
builder.build_snapshot([b'B-id', b'C-id'],
1983
[('unversion', 'a'),
1985
('add', (u'a', b'a-id', 'directory', None))],
1986
revision_id=b'D-id')
1987
merge_obj = self.make_merge_obj(builder, b'E-id')
1988
entries = list(merge_obj._entries_lca())
1989
# Only the kind was changed (content)
1990
self.assertEqual([], entries)
1992
def test_interesting_files(self):
1993
# Two files modified, but we should filter one of them
1994
builder = self.get_builder()
1995
builder.build_snapshot(None,
1996
[('add', (u'', b'a-root-id', 'directory', None)),
1997
('add', (u'a', b'a-id', 'file', b'content\n')),
1998
('add', (u'b', b'b-id', 'file', b'content\n'))],
1999
revision_id=b'A-id')
2000
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2001
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2002
builder.build_snapshot([b'C-id', b'B-id'],
2003
[('modify', ('a', b'new-content\n')),
2004
('modify', ('b', b'new-content\n'))],
2005
revision_id=b'E-id')
2006
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2007
merge_obj = self.make_merge_obj(builder, b'E-id',
2008
interesting_files=['b'])
2009
entries = list(merge_obj._entries_lca())
2010
root_id = b'a-root-id'
2011
self.assertEqual([(b'b-id', True,
2012
((u'b', [u'b', u'b']), u'b', u'b'),
2013
((root_id, [root_id, root_id]), root_id, root_id),
2014
((u'b', [u'b', u'b']), u'b', u'b'),
2015
((False, [False, False]), False, False)),
2018
def test_interesting_file_in_this(self):
2019
# This renamed the file, but it should still match the entry in other
2020
builder = self.get_builder()
2021
builder.build_snapshot(None,
2022
[('add', (u'', b'a-root-id', 'directory', None)),
2023
('add', (u'a', b'a-id', 'file', b'content\n')),
2024
('add', (u'b', b'b-id', 'file', b'content\n'))],
2025
revision_id=b'A-id')
2026
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2027
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2028
builder.build_snapshot([b'C-id', b'B-id'],
2029
[('modify', ('a', b'new-content\n')),
2030
('modify', ('b', b'new-content\n'))],
2031
revision_id=b'E-id')
2032
builder.build_snapshot([b'B-id', b'C-id'],
2033
[('rename', ('b', 'c'))],
2034
revision_id=b'D-id')
2035
merge_obj = self.make_merge_obj(builder, b'E-id',
2036
interesting_files=['c'])
2037
entries = list(merge_obj._entries_lca())
2038
root_id = b'a-root-id'
2039
self.assertEqual([(b'b-id', True,
2040
((u'b', [u'b', u'b']), u'b', u'c'),
2041
((root_id, [root_id, root_id]), root_id, root_id),
2042
((u'b', [u'b', u'b']), u'b', u'c'),
2043
((False, [False, False]), False, False)),
2046
def test_interesting_file_in_base(self):
2047
# This renamed the file, but it should still match the entry in BASE
2048
builder = self.get_builder()
2049
builder.build_snapshot(None,
2050
[('add', (u'', b'a-root-id', 'directory', None)),
2051
('add', (u'a', b'a-id', 'file', b'content\n')),
2052
('add', (u'c', b'c-id', 'file', b'content\n'))],
2053
revision_id=b'A-id')
2054
builder.build_snapshot([b'A-id'],
2055
[('rename', ('c', 'b'))],
2056
revision_id=b'B-id')
2057
builder.build_snapshot([b'A-id'],
2058
[('rename', ('c', 'b'))],
2059
revision_id=b'C-id')
2060
builder.build_snapshot([b'C-id', b'B-id'],
2061
[('modify', ('a', b'new-content\n')),
2062
('modify', ('b', b'new-content\n'))],
2063
revision_id=b'E-id')
2064
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2065
merge_obj = self.make_merge_obj(builder, b'E-id',
2066
interesting_files=['c'])
2067
entries = list(merge_obj._entries_lca())
2068
root_id = b'a-root-id'
2069
self.assertEqual([(b'c-id', True,
2070
((u'c', [u'b', u'b']), u'b', u'b'),
2071
((root_id, [root_id, root_id]), root_id, root_id),
2072
((u'c', [u'b', u'b']), u'b', u'b'),
2073
((False, [False, False]), False, False)),
2076
def test_interesting_file_in_lca(self):
2077
# This renamed the file, but it should still match the entry in LCA
2078
builder = self.get_builder()
2079
builder.build_snapshot(None,
2080
[('add', (u'', b'a-root-id', 'directory', None)),
2081
('add', (u'a', b'a-id', 'file', b'content\n')),
2082
('add', (u'b', b'b-id', 'file', b'content\n'))],
2083
revision_id=b'A-id')
2084
builder.build_snapshot([b'A-id'],
2085
[('rename', ('b', 'c'))], revision_id=b'B-id')
2086
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2087
builder.build_snapshot([b'C-id', b'B-id'],
2088
[('modify', ('a', b'new-content\n')),
2089
('modify', ('b', b'new-content\n'))],
2090
revision_id=b'E-id')
2091
builder.build_snapshot([b'B-id', b'C-id'],
2092
[('rename', ('c', 'b'))], revision_id=b'D-id')
2093
merge_obj = self.make_merge_obj(builder, b'E-id',
2094
interesting_files=['c'])
2095
entries = list(merge_obj._entries_lca())
2096
root_id = b'a-root-id'
2097
self.assertEqual([(b'b-id', True,
2098
((u'b', [u'c', u'b']), u'b', u'b'),
2099
((root_id, [root_id, root_id]), root_id, root_id),
2100
((u'b', [u'c', u'b']), u'b', u'b'),
2101
((False, [False, False]), False, False)),
2104
def test_interesting_files(self):
2105
# Two files modified, but we should filter one of them
2106
builder = self.get_builder()
2107
builder.build_snapshot(None,
2108
[('add', (u'', b'a-root-id', 'directory', None)),
2109
('add', (u'a', b'a-id', 'file', b'content\n')),
2110
('add', (u'b', b'b-id', 'file', b'content\n'))],
2111
revision_id=b'A-id')
2112
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2113
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2114
builder.build_snapshot([b'C-id', b'B-id'],
2115
[('modify', ('a', b'new-content\n')),
2116
('modify', ('b', b'new-content\n'))], revision_id=b'E-id')
2117
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2118
merge_obj = self.make_merge_obj(builder, b'E-id',
2119
interesting_files=['b'])
2120
entries = list(merge_obj._entries_lca())
2121
root_id = b'a-root-id'
2122
self.assertEqual([(b'b-id', True,
2123
((u'b', [u'b', u'b']), u'b', u'b'),
2124
((root_id, [root_id, root_id]), root_id, root_id),
2125
((u'b', [u'b', u'b']), u'b', u'b'),
2126
((False, [False, False]), False, False)),
2131
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
2133
def get_builder(self):
2134
builder = self.make_branch_builder('path')
2135
builder.start_series()
2136
self.addCleanup(builder.finish_series)
2139
def get_wt_from_builder(self, builder):
2140
"""Get a real WorkingTree from the builder."""
2141
the_branch = builder.get_branch()
2142
wt = the_branch.controldir.create_workingtree()
2143
# Note: This is a little bit ugly, but we are holding the branch
2144
# write-locked as part of the build process, and we would like to
2145
# maintain that. So we just force the WT to re-use the same
2147
wt._branch = the_branch
2149
self.addCleanup(wt.unlock)
2152
def do_merge(self, builder, other_revision_id):
2153
wt = self.get_wt_from_builder(builder)
2154
merger = _mod_merge.Merger.from_revision_ids(
2155
wt, other_revision_id)
2156
merger.merge_type = _mod_merge.Merge3Merger
2157
return wt, merger.do_merge()
2159
def test_simple_lca(self):
2160
builder = self.get_builder()
2161
builder.build_snapshot(None,
2162
[('add', (u'', b'a-root-id', 'directory', None)),
2163
('add', (u'a', b'a-id', 'file', b'a\nb\nc\n'))],
2164
revision_id=b'A-id')
2165
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2166
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2167
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2168
builder.build_snapshot([b'B-id', b'C-id'],
2169
[('modify', ('a', b'a\nb\nc\nd\ne\nf\n'))],
2170
revision_id=b'D-id')
2171
wt, conflicts = self.do_merge(builder, b'E-id')
2172
self.assertEqual(0, conflicts)
2173
# The merge should have simply update the contents of 'a'
2174
self.assertEqual(b'a\nb\nc\nd\ne\nf\n', wt.get_file_text('a'))
2176
def test_conflict_without_lca(self):
2177
# This test would cause a merge conflict, unless we use the lca trees
2178
# to determine the real ancestry
2181
# B C Path renamed to 'bar' in B
2185
# D E Path at 'bar' in D and E
2187
# F Path at 'baz' in F, which supersedes 'bar' and 'foo'
2188
builder = self.get_builder()
2189
builder.build_snapshot(None,
2190
[('add', (u'', b'a-root-id', 'directory', None)),
2191
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2192
revision_id=b'A-id')
2193
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2194
builder.build_snapshot([b'A-id'],
2195
[('rename', ('foo', 'bar'))], revision_id=b'B-id', )
2196
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2197
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2198
builder.build_snapshot([b'E-id'],
2199
[('rename', ('bar', 'baz'))], revision_id=b'F-id')
2200
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2201
wt, conflicts = self.do_merge(builder, b'F-id')
2202
self.assertEqual(0, conflicts)
2203
# The merge should simply recognize that the final rename takes
2205
self.assertEqual('baz', wt.id2path(b'foo-id'))
2207
def test_other_deletes_lca_renames(self):
2208
# This test would cause a merge conflict, unless we use the lca trees
2209
# to determine the real ancestry
2212
# B C Path renamed to 'bar' in B
2216
# D E Path at 'bar' in D and E
2219
builder = self.get_builder()
2220
builder.build_snapshot(None,
2221
[('add', (u'', b'a-root-id', 'directory', None)),
2222
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2223
revision_id=b'A-id')
2224
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2225
builder.build_snapshot([b'A-id'],
2226
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2227
builder.build_snapshot([b'C-id', b'B-id'], # merge the rename
2228
[('rename', ('foo', 'bar'))], revision_id=b'E-id')
2229
builder.build_snapshot([b'E-id'],
2230
[('unversion', 'bar')], revision_id=b'F-id')
2231
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2232
wt, conflicts = self.do_merge(builder, b'F-id')
2233
self.assertEqual(0, conflicts)
2234
self.assertRaises(errors.NoSuchId, wt.id2path, b'foo-id')
2236
def test_executable_changes(self):
2245
# F Executable bit changed
2246
builder = self.get_builder()
2247
builder.build_snapshot(None,
2248
[('add', (u'', b'a-root-id', 'directory', None)),
2249
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2250
revision_id=b'A-id')
2251
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2252
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2253
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2254
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2255
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2256
wt = self.get_wt_from_builder(builder)
2257
tt = transform.TreeTransform(wt)
2259
tt.set_executability(True, tt.trans_id_tree_path('foo'))
2264
self.assertTrue(wt.is_executable('foo'))
2265
wt.commit('F-id', rev_id=b'F-id')
2266
# Reset to D, so that we can merge F
2267
wt.set_parent_ids([b'D-id'])
2268
wt.branch.set_last_revision_info(3, b'D-id')
2270
self.assertFalse(wt.is_executable('foo'))
2271
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2272
self.assertEqual(0, conflicts)
2273
self.assertTrue(wt.is_executable('foo'))
2275
def test_create_symlink(self):
2276
self.requireFeature(features.SymlinkFeature)
2285
# F Add a symlink 'foo' => 'bar'
2286
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2287
# have symlink support
2288
builder = self.get_builder()
2289
builder.build_snapshot(None,
2290
[('add', (u'', b'a-root-id', 'directory', None))],
2291
revision_id=b'A-id')
2292
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2293
builder.build_snapshot([b'A-id'], [], revision_id=b'B-id')
2294
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2295
builder.build_snapshot([b'C-id', b'B-id'], [], revision_id=b'E-id')
2296
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2297
wt = self.get_wt_from_builder(builder)
2298
os.symlink('bar', 'path/foo')
2299
wt.add(['foo'], [b'foo-id'])
2300
self.assertEqual('bar', wt.get_symlink_target('foo'))
2301
wt.commit('add symlink', rev_id=b'F-id')
2302
# Reset to D, so that we can merge F
2303
wt.set_parent_ids([b'D-id'])
2304
wt.branch.set_last_revision_info(3, b'D-id')
2306
self.assertFalse(wt.is_versioned('foo'))
2307
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2308
self.assertEqual(0, conflicts)
2309
self.assertEqual(b'foo-id', wt.path2id('foo'))
2310
self.assertEqual('bar', wt.get_symlink_target('foo'))
2312
def test_both_sides_revert(self):
2313
# Both sides of a criss-cross revert the text to the lca
2314
# A base, introduces 'foo'
2316
# B C B modifies 'foo', C modifies 'foo'
2318
# D E D reverts to B, E reverts to C
2319
# This should conflict
2320
# This must be done with a real WorkingTree, because normally their
2321
# inventory contains "None" rather than a real sha1
2322
builder = self.get_builder()
2323
builder.build_snapshot(None,
2324
[('add', (u'', b'a-root-id', 'directory', None)),
2325
('add', (u'foo', b'foo-id', 'file', b'A content\n'))],
2326
revision_id=b'A-id')
2327
builder.build_snapshot([b'A-id'],
2328
[('modify', ('foo', b'B content\n'))],
2329
revision_id=b'B-id')
2330
builder.build_snapshot([b'A-id'],
2331
[('modify', ('foo', b'C content\n'))],
2332
revision_id=b'C-id')
2333
builder.build_snapshot([b'C-id', b'B-id'], [],
2334
revision_id=b'E-id')
2335
builder.build_snapshot([b'B-id', b'C-id'], [],
2336
revision_id=b'D-id')
2337
wt, conflicts = self.do_merge(builder, b'E-id')
2338
self.assertEqual(1, conflicts)
2339
self.assertEqualDiff(b'<<<<<<< TREE\n'
2343
b'>>>>>>> MERGE-SOURCE\n',
2344
wt.get_file_text('foo'))
2346
def test_modified_symlink(self):
2347
self.requireFeature(features.SymlinkFeature)
2348
# A Create symlink foo => bar
2350
# B C B relinks foo => baz
2354
# D E D & E have foo => baz
2356
# F F changes it to bing
2358
# Merging D & F should result in F cleanly overriding D, because D's
2359
# value actually comes from B
2361
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2362
# have symlink support
2363
wt = self.make_branch_and_tree('path')
2365
self.addCleanup(wt.unlock)
2366
os.symlink('bar', 'path/foo')
2367
wt.add(['foo'], [b'foo-id'])
2368
wt.commit('add symlink', rev_id=b'A-id')
2369
os.remove('path/foo')
2370
os.symlink('baz', 'path/foo')
2371
wt.commit('foo => baz', rev_id=b'B-id')
2372
wt.set_last_revision(b'A-id')
2373
wt.branch.set_last_revision_info(1, b'A-id')
2375
wt.commit('C', rev_id=b'C-id')
2376
wt.merge_from_branch(wt.branch, b'B-id')
2377
self.assertEqual('baz', wt.get_symlink_target('foo'))
2378
wt.commit('E merges C & B', rev_id=b'E-id')
2379
os.remove('path/foo')
2380
os.symlink('bing', 'path/foo')
2381
wt.commit('F foo => bing', rev_id=b'F-id')
2382
wt.set_last_revision(b'B-id')
2383
wt.branch.set_last_revision_info(2, b'B-id')
2385
wt.merge_from_branch(wt.branch, b'C-id')
2386
wt.commit('D merges B & C', rev_id=b'D-id')
2387
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2388
self.assertEqual(0, conflicts)
2389
self.assertEqual('bing', wt.get_symlink_target('foo'))
2391
def test_renamed_symlink(self):
2392
self.requireFeature(features.SymlinkFeature)
2393
# A Create symlink foo => bar
2395
# B C B renames foo => barry
2399
# D E D & E have barry
2401
# F F renames barry to blah
2403
# Merging D & F should result in F cleanly overriding D, because D's
2404
# value actually comes from B
2406
wt = self.make_branch_and_tree('path')
2408
self.addCleanup(wt.unlock)
2409
os.symlink('bar', 'path/foo')
2410
wt.add(['foo'], [b'foo-id'])
2411
wt.commit('A add symlink', rev_id=b'A-id')
2412
wt.rename_one('foo', 'barry')
2413
wt.commit('B foo => barry', rev_id=b'B-id')
2414
wt.set_last_revision(b'A-id')
2415
wt.branch.set_last_revision_info(1, b'A-id')
2417
wt.commit('C', rev_id=b'C-id')
2418
wt.merge_from_branch(wt.branch, b'B-id')
2419
self.assertEqual('barry', wt.id2path(b'foo-id'))
2420
self.assertEqual('bar', wt.get_symlink_target('barry'))
2421
wt.commit('E merges C & B', rev_id=b'E-id')
2422
wt.rename_one('barry', 'blah')
2423
wt.commit('F barry => blah', rev_id=b'F-id')
2424
wt.set_last_revision(b'B-id')
2425
wt.branch.set_last_revision_info(2, b'B-id')
2427
wt.merge_from_branch(wt.branch, b'C-id')
2428
wt.commit('D merges B & C', rev_id=b'D-id')
2429
self.assertEqual('barry', wt.id2path(b'foo-id'))
2430
# Check the output of the Merger object directly
2431
merger = _mod_merge.Merger.from_revision_ids(wt, b'F-id')
2432
merger.merge_type = _mod_merge.Merge3Merger
2433
merge_obj = merger.make_merger()
2434
root_id = wt.path2id('')
2435
entries = list(merge_obj._entries_lca())
2436
# No content change, just a path change
2437
self.assertEqual([(b'foo-id', False,
2438
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2439
((root_id, [root_id, root_id]), root_id, root_id),
2440
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2441
((False, [False, False]), False, False)),
2443
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
2444
self.assertEqual(0, conflicts)
2445
self.assertEqual('blah', wt.id2path(b'foo-id'))
2447
def test_symlink_no_content_change(self):
2448
self.requireFeature(features.SymlinkFeature)
2449
# A Create symlink foo => bar
2451
# B C B relinks foo => baz
2455
# D E D & E have foo => baz
2457
# F F has foo => bing
2459
# Merging E into F should not cause a conflict, because E doesn't have
2460
# a content change relative to the LCAs (it does relative to A)
2461
wt = self.make_branch_and_tree('path')
2463
self.addCleanup(wt.unlock)
2464
os.symlink('bar', 'path/foo')
2465
wt.add(['foo'], [b'foo-id'])
2466
wt.commit('add symlink', rev_id=b'A-id')
2467
os.remove('path/foo')
2468
os.symlink('baz', 'path/foo')
2469
wt.commit('foo => baz', rev_id=b'B-id')
2470
wt.set_last_revision(b'A-id')
2471
wt.branch.set_last_revision_info(1, b'A-id')
2473
wt.commit('C', rev_id=b'C-id')
2474
wt.merge_from_branch(wt.branch, b'B-id')
2475
self.assertEqual('baz', wt.get_symlink_target('foo'))
2476
wt.commit('E merges C & B', rev_id=b'E-id')
2477
wt.set_last_revision(b'B-id')
2478
wt.branch.set_last_revision_info(2, b'B-id')
2480
wt.merge_from_branch(wt.branch, b'C-id')
2481
wt.commit('D merges B & C', rev_id=b'D-id')
2482
os.remove('path/foo')
2483
os.symlink('bing', 'path/foo')
2484
wt.commit('F foo => bing', rev_id=b'F-id')
2486
# Check the output of the Merger object directly
2487
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2488
merger.merge_type = _mod_merge.Merge3Merger
2489
merge_obj = merger.make_merger()
2490
# Nothing interesting happened in OTHER relative to BASE
2491
self.assertEqual([], list(merge_obj._entries_lca()))
2492
# Now do a real merge, just to test the rest of the stack
2493
conflicts = wt.merge_from_branch(wt.branch, to_revision=b'E-id')
2494
self.assertEqual(0, conflicts)
2495
self.assertEqual('bing', wt.get_symlink_target('foo'))
2497
def test_symlink_this_changed_kind(self):
2498
self.requireFeature(features.SymlinkFeature)
2501
# B C B creates symlink foo => bar
2505
# D E D changes foo into a file, E has foo => bing
2507
# Mostly, this is trying to test that we don't try to os.readlink() on
2508
# a file, or when there is nothing there
2509
wt = self.make_branch_and_tree('path')
2511
self.addCleanup(wt.unlock)
2512
wt.commit('base', rev_id=b'A-id')
2513
os.symlink('bar', 'path/foo')
2514
wt.add(['foo'], [b'foo-id'])
2515
wt.commit('add symlink foo => bar', rev_id=b'B-id')
2516
wt.set_last_revision(b'A-id')
2517
wt.branch.set_last_revision_info(1, b'A-id')
2519
wt.commit('C', rev_id=b'C-id')
2520
wt.merge_from_branch(wt.branch, b'B-id')
2521
self.assertEqual('bar', wt.get_symlink_target('foo'))
2522
os.remove('path/foo')
2523
# We have to change the link in E, or it won't try to do a comparison
2524
os.symlink('bing', 'path/foo')
2525
wt.commit('E merges C & B, overrides to bing', rev_id=b'E-id')
2526
wt.set_last_revision(b'B-id')
2527
wt.branch.set_last_revision_info(2, b'B-id')
2529
wt.merge_from_branch(wt.branch, b'C-id')
2530
os.remove('path/foo')
2531
self.build_tree_contents([('path/foo', b'file content\n')])
2532
# XXX: workaround, WT doesn't detect kind changes unless you do
2534
list(wt.iter_changes(wt.basis_tree()))
2535
wt.commit('D merges B & C, makes it a file', rev_id=b'D-id')
2537
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2538
merger.merge_type = _mod_merge.Merge3Merger
2539
merge_obj = merger.make_merger()
2540
entries = list(merge_obj._entries_lca())
2541
root_id = wt.path2id('')
2542
self.assertEqual([(b'foo-id', True,
2543
((None, [u'foo', None]), u'foo', u'foo'),
2544
((None, [root_id, None]), root_id, root_id),
2545
((None, [u'foo', None]), u'foo', u'foo'),
2546
((None, [False, None]), False, False)),
2549
def test_symlink_all_wt(self):
2550
"""Check behavior if all trees are Working Trees."""
2551
self.requireFeature(features.SymlinkFeature)
2552
# The big issue is that entry.symlink_target is None for WorkingTrees.
2553
# So we need to make sure we handle that case correctly.
2556
# B C B relinks foo => baz
2558
# D E D & E have foo => baz
2560
# F F changes it to bing
2561
# Merging D & F should result in F cleanly overriding D, because D's
2562
# value actually comes from B
2564
wt = self.make_branch_and_tree('path')
2566
self.addCleanup(wt.unlock)
2567
os.symlink('bar', 'path/foo')
2568
wt.add(['foo'], [b'foo-id'])
2569
wt.commit('add symlink', rev_id=b'A-id')
2570
os.remove('path/foo')
2571
os.symlink('baz', 'path/foo')
2572
wt.commit('foo => baz', rev_id=b'B-id')
2573
wt.set_last_revision(b'A-id')
2574
wt.branch.set_last_revision_info(1, b'A-id')
2576
wt.commit('C', rev_id=b'C-id')
2577
wt.merge_from_branch(wt.branch, b'B-id')
2578
self.assertEqual('baz', wt.get_symlink_target('foo'))
2579
wt.commit('E merges C & B', rev_id=b'E-id')
2580
os.remove('path/foo')
2581
os.symlink('bing', 'path/foo')
2582
wt.commit('F foo => bing', rev_id=b'F-id')
2583
wt.set_last_revision(b'B-id')
2584
wt.branch.set_last_revision_info(2, b'B-id')
2586
wt.merge_from_branch(wt.branch, b'C-id')
2587
wt.commit('D merges B & C', rev_id=b'D-id')
2588
wt_base = wt.controldir.sprout('base', b'A-id').open_workingtree()
2590
self.addCleanup(wt_base.unlock)
2591
wt_lca1 = wt.controldir.sprout('b-tree', b'B-id').open_workingtree()
2593
self.addCleanup(wt_lca1.unlock)
2594
wt_lca2 = wt.controldir.sprout('c-tree', b'C-id').open_workingtree()
2596
self.addCleanup(wt_lca2.unlock)
2597
wt_other = wt.controldir.sprout('other', b'F-id').open_workingtree()
2598
wt_other.lock_read()
2599
self.addCleanup(wt_other.unlock)
2600
merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
2601
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2602
entries = list(merge_obj._entries_lca())
2603
root_id = wt.path2id('')
2604
self.assertEqual([(b'foo-id', True,
2605
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2606
((root_id, [root_id, root_id]), root_id, root_id),
2607
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2608
((False, [False, False]), False, False)),
2611
def test_other_reverted_path_to_base(self):
2614
# B C Path at 'bar' in B
2621
builder = self.get_builder()
2622
builder.build_snapshot(None,
2623
[('add', (u'', b'a-root-id', 'directory', None)),
2624
('add', (u'foo', b'foo-id', 'file', b'a\nb\nc\n'))],
2625
revision_id=b'A-id')
2626
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2627
builder.build_snapshot([b'A-id'],
2628
[('rename', ('foo', 'bar'))], revision_id=b'B-id')
2629
builder.build_snapshot([b'C-id', b'B-id'],
2630
[('rename', ('foo', 'bar'))], revision_id=b'E-id') # merge the rename
2631
builder.build_snapshot([b'E-id'],
2632
[('rename', ('bar', 'foo'))], revision_id=b'F-id') # Rename back to BASE
2633
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2634
wt, conflicts = self.do_merge(builder, b'F-id')
2635
self.assertEqual(0, conflicts)
2636
self.assertEqual('foo', wt.id2path(b'foo-id'))
2638
def test_other_reverted_content_to_base(self):
2639
builder = self.get_builder()
2640
builder.build_snapshot(None,
2641
[('add', (u'', b'a-root-id', 'directory', None)),
2642
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2643
revision_id=b'A-id')
2644
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2645
builder.build_snapshot([b'A-id'],
2646
[('modify', ('foo', b'B content\n'))],
2647
revision_id=b'B-id')
2648
builder.build_snapshot([b'C-id', b'B-id'],
2649
[('modify', ('foo', b'B content\n'))],
2650
revision_id=b'E-id') # merge the content
2651
builder.build_snapshot([b'E-id'],
2652
[('modify', ('foo', b'base content\n'))],
2653
revision_id=b'F-id') # Revert back to BASE
2654
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2655
wt, conflicts = self.do_merge(builder, b'F-id')
2656
self.assertEqual(0, conflicts)
2657
# TODO: We need to use the per-file graph to properly select a BASE
2658
# before this will work. Or at least use the LCA trees to find
2659
# the appropriate content base. (which is B, not A).
2660
self.assertEqual(b'base content\n', wt.get_file_text('foo'))
2662
def test_other_modified_content(self):
2663
builder = self.get_builder()
2664
builder.build_snapshot(None,
2665
[('add', (u'', b'a-root-id', 'directory', None)),
2666
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2667
revision_id=b'A-id')
2668
builder.build_snapshot([b'A-id'], [], revision_id=b'C-id')
2669
builder.build_snapshot([b'A-id'],
2670
[('modify', ('foo', b'B content\n'))],
2671
revision_id=b'B-id')
2672
builder.build_snapshot([b'C-id', b'B-id'],
2673
[('modify', ('foo', b'B content\n'))],
2674
revision_id=b'E-id') # merge the content
2675
builder.build_snapshot([b'E-id'],
2676
[('modify', ('foo', b'F content\n'))],
2677
revision_id=b'F-id') # Override B content
2678
builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
2679
wt, conflicts = self.do_merge(builder, b'F-id')
2680
self.assertEqual(0, conflicts)
2681
self.assertEqual(b'F content\n', wt.get_file_text('foo'))
2683
def test_all_wt(self):
2684
"""Check behavior if all trees are Working Trees."""
2685
# The big issue is that entry.revision is None for WorkingTrees. (as is
2686
# entry.text_sha1, etc. So we need to make sure we handle that case
2688
# A Content of 'foo', path of 'a'
2690
# B C B modifies content, C renames 'a' => 'b'
2692
# D E E updates content, renames 'b' => 'c'
2693
builder = self.get_builder()
2694
builder.build_snapshot(None,
2695
[('add', (u'', b'a-root-id', 'directory', None)),
2696
('add', (u'a', b'a-id', 'file', b'base content\n')),
2697
('add', (u'foo', b'foo-id', 'file', b'base content\n'))],
2698
revision_id=b'A-id')
2699
builder.build_snapshot([b'A-id'],
2700
[('modify', ('foo', b'B content\n'))],
2701
revision_id=b'B-id')
2702
builder.build_snapshot([b'A-id'],
2703
[('rename', ('a', 'b'))],
2704
revision_id=b'C-id')
2705
builder.build_snapshot([b'C-id', b'B-id'],
2706
[('rename', ('b', 'c')),
2707
('modify', ('foo', b'E content\n'))],
2708
revision_id=b'E-id')
2709
builder.build_snapshot([b'B-id', b'C-id'],
2710
[('rename', ('a', 'b'))], revision_id=b'D-id') # merged change
2711
wt_this = self.get_wt_from_builder(builder)
2712
wt_base = wt_this.controldir.sprout('base', b'A-id').open_workingtree()
2714
self.addCleanup(wt_base.unlock)
2715
wt_lca1 = wt_this.controldir.sprout('b-tree', b'B-id').open_workingtree()
2717
self.addCleanup(wt_lca1.unlock)
2718
wt_lca2 = wt_this.controldir.sprout('c-tree', b'C-id').open_workingtree()
2720
self.addCleanup(wt_lca2.unlock)
2721
wt_other = wt_this.controldir.sprout('other', b'E-id').open_workingtree()
2722
wt_other.lock_read()
2723
self.addCleanup(wt_other.unlock)
2724
merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
2725
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2726
entries = list(merge_obj._entries_lca())
2727
root_id = b'a-root-id'
2728
self.assertEqual([(b'a-id', False,
2729
((u'a', [u'a', u'b']), u'c', u'b'),
2730
((root_id, [root_id, root_id]), root_id, root_id),
2731
((u'a', [u'a', u'b']), u'c', u'b'),
2732
((False, [False, False]), False, False)),
2734
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2735
((root_id, [root_id, root_id]), root_id, root_id),
2736
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2737
((False, [False, False]), False, False)),
2740
def test_nested_tree_unmodified(self):
2741
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2743
wt = self.make_branch_and_tree('tree',
2744
format='development-subtree')
2746
self.addCleanup(wt.unlock)
2747
sub_tree = self.make_branch_and_tree('tree/sub-tree',
2748
format='development-subtree')
2749
wt.set_root_id(b'a-root-id')
2750
sub_tree.set_root_id(b'sub-tree-root')
2751
self.build_tree_contents([('tree/sub-tree/file', b'text1')])
2752
sub_tree.add('file')
2753
sub_tree.commit('foo', rev_id=b'sub-A-id')
2754
wt.add_reference(sub_tree)
2755
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2756
# Now create a criss-cross merge in the parent, without modifying the
2758
wt.commit('B', rev_id=b'B-id', recursive=None)
2759
wt.set_last_revision(b'A-id')
2760
wt.branch.set_last_revision_info(1, b'A-id')
2761
wt.commit('C', rev_id=b'C-id', recursive=None)
2762
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2763
wt.commit('E', rev_id=b'E-id', recursive=None)
2764
wt.set_parent_ids([b'B-id', b'C-id'])
2765
wt.branch.set_last_revision_info(2, b'B-id')
2766
wt.commit('D', rev_id=b'D-id', recursive=None)
2768
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2769
merger.merge_type = _mod_merge.Merge3Merger
2770
merge_obj = merger.make_merger()
2771
entries = list(merge_obj._entries_lca())
2772
self.assertEqual([], entries)
2774
def test_nested_tree_subtree_modified(self):
2775
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2777
wt = self.make_branch_and_tree('tree',
2778
format='development-subtree')
2780
self.addCleanup(wt.unlock)
2781
sub_tree = self.make_branch_and_tree('tree/sub',
2782
format='development-subtree')
2783
wt.set_root_id(b'a-root-id')
2784
sub_tree.set_root_id(b'sub-tree-root')
2785
self.build_tree_contents([('tree/sub/file', b'text1')])
2786
sub_tree.add('file')
2787
sub_tree.commit('foo', rev_id=b'sub-A-id')
2788
wt.add_reference(sub_tree)
2789
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2790
# Now create a criss-cross merge in the parent, without modifying the
2792
wt.commit('B', rev_id=b'B-id', recursive=None)
2793
wt.set_last_revision(b'A-id')
2794
wt.branch.set_last_revision_info(1, b'A-id')
2795
wt.commit('C', rev_id=b'C-id', recursive=None)
2796
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2797
self.build_tree_contents([('tree/sub/file', b'text2')])
2798
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2799
wt.commit('E', rev_id=b'E-id', recursive=None)
2800
wt.set_parent_ids([b'B-id', b'C-id'])
2801
wt.branch.set_last_revision_info(2, b'B-id')
2802
wt.commit('D', rev_id=b'D-id', recursive=None)
2804
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2805
merger.merge_type = _mod_merge.Merge3Merger
2806
merge_obj = merger.make_merger()
2807
entries = list(merge_obj._entries_lca())
2808
# Nothing interesting about this sub-tree, because content changes are
2809
# computed at a higher level
2810
self.assertEqual([], entries)
2812
def test_nested_tree_subtree_renamed(self):
2813
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2815
wt = self.make_branch_and_tree('tree',
2816
format='development-subtree')
2818
self.addCleanup(wt.unlock)
2819
sub_tree = self.make_branch_and_tree('tree/sub',
2820
format='development-subtree')
2821
wt.set_root_id(b'a-root-id')
2822
sub_tree.set_root_id(b'sub-tree-root')
2823
self.build_tree_contents([('tree/sub/file', b'text1')])
2824
sub_tree.add('file')
2825
sub_tree.commit('foo', rev_id=b'sub-A-id')
2826
wt.add_reference(sub_tree)
2827
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2828
# Now create a criss-cross merge in the parent, without modifying the
2830
wt.commit('B', rev_id=b'B-id', recursive=None)
2831
wt.set_last_revision(b'A-id')
2832
wt.branch.set_last_revision_info(1, b'A-id')
2833
wt.commit('C', rev_id=b'C-id', recursive=None)
2834
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2835
wt.rename_one('sub', 'alt_sub')
2836
wt.commit('E', rev_id=b'E-id', recursive=None)
2837
wt.set_last_revision(b'B-id')
2839
wt.set_parent_ids([b'B-id', b'C-id'])
2840
wt.branch.set_last_revision_info(2, b'B-id')
2841
wt.commit('D', rev_id=b'D-id', recursive=None)
2843
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2844
merger.merge_type = _mod_merge.Merge3Merger
2845
merge_obj = merger.make_merger()
2846
entries = list(merge_obj._entries_lca())
2847
root_id = b'a-root-id'
2848
self.assertEqual([(b'sub-tree-root', False,
2849
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2850
((root_id, [root_id, root_id]), root_id, root_id),
2851
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2852
((False, [False, False]), False, False)),
2855
def test_nested_tree_subtree_renamed_and_modified(self):
2856
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2858
wt = self.make_branch_and_tree('tree',
2859
format='development-subtree')
2861
self.addCleanup(wt.unlock)
2862
sub_tree = self.make_branch_and_tree('tree/sub',
2863
format='development-subtree')
2864
wt.set_root_id(b'a-root-id')
2865
sub_tree.set_root_id(b'sub-tree-root')
2866
self.build_tree_contents([('tree/sub/file', b'text1')])
2867
sub_tree.add('file')
2868
sub_tree.commit('foo', rev_id=b'sub-A-id')
2869
wt.add_reference(sub_tree)
2870
wt.commit('set text to 1', rev_id=b'A-id', recursive=None)
2871
# Now create a criss-cross merge in the parent, without modifying the
2873
wt.commit('B', rev_id=b'B-id', recursive=None)
2874
wt.set_last_revision(b'A-id')
2875
wt.branch.set_last_revision_info(1, b'A-id')
2876
wt.commit('C', rev_id=b'C-id', recursive=None)
2877
wt.merge_from_branch(wt.branch, to_revision=b'B-id')
2878
self.build_tree_contents([('tree/sub/file', b'text2')])
2879
sub_tree.commit('modify contents', rev_id=b'sub-B-id')
2880
wt.rename_one('sub', 'alt_sub')
2881
wt.commit('E', rev_id=b'E-id', recursive=None)
2882
wt.set_last_revision(b'B-id')
2884
wt.set_parent_ids([b'B-id', b'C-id'])
2885
wt.branch.set_last_revision_info(2, b'B-id')
2886
wt.commit('D', rev_id=b'D-id', recursive=None)
2888
merger = _mod_merge.Merger.from_revision_ids(wt, b'E-id')
2889
merger.merge_type = _mod_merge.Merge3Merger
2890
merge_obj = merger.make_merger()
2891
entries = list(merge_obj._entries_lca())
2892
root_id = b'a-root-id'
2893
self.assertEqual([(b'sub-tree-root', False,
2894
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2895
((root_id, [root_id, root_id]), root_id, root_id),
2896
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2897
((False, [False, False]), False, False)),
2901
class TestLCAMultiWay(tests.TestCase):
2903
def assertLCAMultiWay(self, expected, base, lcas, other, this,
2904
allow_overriding_lca=True):
2905
self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
2906
(base, lcas), other, this,
2907
allow_overriding_lca=allow_overriding_lca))
2909
def test_other_equal_equal_lcas(self):
2910
"""Test when OTHER=LCA and all LCAs are identical."""
2911
self.assertLCAMultiWay('this',
2912
'bval', ['bval', 'bval'], 'bval', 'bval')
2913
self.assertLCAMultiWay('this',
2914
'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
2915
self.assertLCAMultiWay('this',
2916
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
2917
self.assertLCAMultiWay('this',
2918
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
2919
self.assertLCAMultiWay('this',
2920
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
2922
def test_other_equal_this(self):
2923
"""Test when other and this are identical."""
2924
self.assertLCAMultiWay('this',
2925
'bval', ['bval', 'bval'], 'oval', 'oval')
2926
self.assertLCAMultiWay('this',
2927
'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
2928
self.assertLCAMultiWay('this',
2929
'bval', ['cval', 'dval'], 'oval', 'oval')
2930
self.assertLCAMultiWay('this',
2931
'bval', [None, 'lcaval'], 'oval', 'oval')
2932
self.assertLCAMultiWay('this',
2933
None, [None, 'lcaval'], 'oval', 'oval')
2934
self.assertLCAMultiWay('this',
2935
None, ['lcaval', 'lcaval'], 'oval', 'oval')
2936
self.assertLCAMultiWay('this',
2937
None, ['cval', 'dval'], 'oval', 'oval')
2938
self.assertLCAMultiWay('this',
2939
None, ['cval', 'dval'], None, None)
2940
self.assertLCAMultiWay('this',
2941
None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
2943
def test_no_lcas(self):
2944
self.assertLCAMultiWay('this',
2945
'bval', [], 'bval', 'tval')
2946
self.assertLCAMultiWay('other',
2947
'bval', [], 'oval', 'bval')
2948
self.assertLCAMultiWay('conflict',
2949
'bval', [], 'oval', 'tval')
2950
self.assertLCAMultiWay('this',
2951
'bval', [], 'oval', 'oval')
2953
def test_lca_supersedes_other_lca(self):
2954
"""If one lca == base, the other lca takes precedence"""
2955
self.assertLCAMultiWay('this',
2956
'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
2957
self.assertLCAMultiWay('this',
2958
'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
2959
# This is actually considered a 'revert' because the 'lcaval' in LCAS
2960
# supersedes the BASE val (in the other LCA) but then OTHER reverts it
2962
self.assertLCAMultiWay('other',
2963
'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
2964
self.assertLCAMultiWay('conflict',
2965
'bval', ['bval', 'lcaval'], 'bval', 'tval')
2967
def test_other_and_this_pick_different_lca(self):
2968
# OTHER and THIS resolve the lca conflict in different ways
2969
self.assertLCAMultiWay('conflict',
2970
'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
2971
self.assertLCAMultiWay('conflict',
2972
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
2973
self.assertLCAMultiWay('conflict',
2974
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
2976
def test_other_in_lca(self):
2977
# OTHER takes a value of one of the LCAs, THIS takes a new value, which
2978
# theoretically supersedes both LCA values and 'wins'
2979
self.assertLCAMultiWay('this',
2980
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
2981
self.assertLCAMultiWay('this',
2982
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval')
2983
self.assertLCAMultiWay('conflict',
2984
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval',
2985
allow_overriding_lca=False)
2986
self.assertLCAMultiWay('conflict',
2987
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval',
2988
allow_overriding_lca=False)
2989
# THIS reverted back to BASE, but that is an explicit supersede of all
2991
self.assertLCAMultiWay('this',
2992
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval')
2993
self.assertLCAMultiWay('this',
2994
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
2995
self.assertLCAMultiWay('conflict',
2996
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval',
2997
allow_overriding_lca=False)
2998
self.assertLCAMultiWay('conflict',
2999
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval',
3000
allow_overriding_lca=False)
3002
def test_this_in_lca(self):
3003
# THIS takes a value of one of the LCAs, OTHER takes a new value, which
3004
# theoretically supersedes both LCA values and 'wins'
3005
self.assertLCAMultiWay('other',
3006
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
3007
self.assertLCAMultiWay('other',
3008
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
3009
self.assertLCAMultiWay('conflict',
3010
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val',
3011
allow_overriding_lca=False)
3012
self.assertLCAMultiWay('conflict',
3013
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val',
3014
allow_overriding_lca=False)
3015
# OTHER reverted back to BASE, but that is an explicit supersede of all
3017
self.assertLCAMultiWay('other',
3018
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val')
3019
self.assertLCAMultiWay('conflict',
3020
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val',
3021
allow_overriding_lca=False)
3023
def test_all_differ(self):
3024
self.assertLCAMultiWay('conflict',
3025
'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
3026
self.assertLCAMultiWay('conflict',
3027
'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval', 'tval')
3028
self.assertLCAMultiWay('conflict',
3029
'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval', 'tval')
3032
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
3035
super(TestConfigurableFileMerger, self).setUp()
3038
def get_merger_factory(self):
3039
# Allows the inner methods to access the test attributes
3042
class FooMerger(_mod_merge.ConfigurableFileMerger):
3044
default_files = ['bar']
3046
def merge_text(self, params):
3047
calls.append('merge_text')
3048
return ('not_applicable', None)
3050
def factory(merger):
3051
result = FooMerger(merger)
3052
# Make sure we start with a clean slate
3053
self.assertEqual(None, result.affected_files)
3054
# Track the original merger
3055
self.merger = result
3060
def _install_hook(self, factory):
3061
_mod_merge.Merger.hooks.install_named_hook('merge_file_content',
3062
factory, 'test factory')
3064
def make_builder(self):
3065
builder = test_merge_core.MergeBuilder(self.test_base_dir)
3066
self.addCleanup(builder.cleanup)
3069
def make_text_conflict(self, file_name='bar'):
3070
factory = self.get_merger_factory()
3071
self._install_hook(factory)
3072
builder = self.make_builder()
3073
builder.add_file(b'bar-id', builder.tree_root, file_name, b'text1', True)
3074
builder.change_contents(b'bar-id', other=b'text4', this=b'text3')
3077
def make_kind_change(self):
3078
factory = self.get_merger_factory()
3079
self._install_hook(factory)
3080
builder = self.make_builder()
3081
builder.add_file(b'bar-id', builder.tree_root, 'bar', b'text1', True,
3083
builder.add_dir(b'bar-dir', builder.tree_root, b'bar-id',
3084
base=False, other=False)
3087
def test_uses_this_branch(self):
3088
builder = self.make_text_conflict()
3089
tt = builder.make_preview_transform()
3090
self.addCleanup(tt.finalize)
3092
def test_affected_files_cached(self):
3093
"""Ensures that the config variable is cached"""
3094
builder = self.make_text_conflict()
3095
conflicts = builder.merge()
3096
# The hook should set the variable
3097
self.assertEqual(['bar'], self.merger.affected_files)
3098
self.assertEqual(1, len(conflicts))
3100
def test_hook_called_for_text_conflicts(self):
3101
builder = self.make_text_conflict()
3102
conflicts = builder.merge()
3103
# The hook should call the merge_text() method
3104
self.assertEqual(['merge_text'], self.calls)
3106
def test_hook_not_called_for_kind_change(self):
3107
builder = self.make_kind_change()
3108
conflicts = builder.merge()
3109
# The hook should not call the merge_text() method
3110
self.assertEqual([], self.calls)
3112
def test_hook_not_called_for_other_files(self):
3113
builder = self.make_text_conflict('foobar')
3114
conflicts = builder.merge()
3115
# The hook should not call the merge_text() method
3116
self.assertEqual([], self.calls)
3119
class TestMergeIntoBase(tests.TestCaseWithTransport):
3121
def setup_simple_branch(self, relpath, shape=None, root_id=None):
3122
"""One commit, containing tree specified by optional shape.
3124
Default is empty tree (just root entry).
3127
root_id = b'%s-root-id' % (relpath.encode('ascii'),)
3128
wt = self.make_branch_and_tree(relpath)
3129
wt.set_root_id(root_id)
3130
if shape is not None:
3131
adjusted_shape = [relpath + '/' + elem for elem in shape]
3132
self.build_tree(adjusted_shape)
3134
(b'%s-%s-id' % (relpath.encode('utf-8'), basename(elem.rstrip('/')).encode('ascii')))
3136
wt.add(shape, ids=ids)
3137
rev_id = b'r1-%s' % (relpath.encode('utf-8'),)
3138
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
3139
self.assertEqual(root_id, wt.path2id(''))
3142
def setup_two_branches(self, custom_root_ids=True):
3143
"""Setup 2 branches, one will be a library, the other a project."""
3147
root_id = inventory.ROOT_ID
3148
project_wt = self.setup_simple_branch(
3149
'project', ['README', 'dir/', 'dir/file.c'],
3151
lib_wt = self.setup_simple_branch(
3152
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3154
return project_wt, lib_wt
3156
def do_merge_into(self, location, merge_as):
3157
"""Helper for using MergeIntoMerger.
3159
:param location: location of directory to merge from, either the
3160
location of a branch or of a path inside a branch.
3161
:param merge_as: the path in a tree to add the new directory as.
3162
:returns: the conflicts from 'do_merge'.
3164
operation = cleanup.OperationWithCleanups(self._merge_into)
3165
return operation.run(location, merge_as)
3167
def _merge_into(self, op, location, merge_as):
3168
# Open and lock the various tree and branch objects
3169
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3170
op.add_cleanup(wt.lock_write().unlock)
3171
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3173
op.add_cleanup(branch_to_merge.lock_read().unlock)
3174
other_tree = branch_to_merge.basis_tree()
3175
op.add_cleanup(other_tree.lock_read().unlock)
3177
merger = _mod_merge.MergeIntoMerger(this_tree=wt, other_tree=other_tree,
3178
other_branch=branch_to_merge, target_subdir=subdir_relpath,
3179
source_subpath=subdir_to_merge)
3180
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3181
conflicts = merger.do_merge()
3182
merger.set_pending()
3185
def assertTreeEntriesEqual(self, expected_entries, tree):
3186
"""Assert that 'tree' contains the expected inventory entries.
3188
:param expected_entries: sequence of (path, file-id) pairs.
3190
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3191
self.assertEqual(expected_entries, files)
3194
class TestMergeInto(TestMergeIntoBase):
3196
def test_newdir_with_unique_roots(self):
3197
"""Merge a branch with a unique root into a new directory."""
3198
project_wt, lib_wt = self.setup_two_branches()
3199
self.do_merge_into('lib1', 'project/lib1')
3200
project_wt.lock_read()
3201
self.addCleanup(project_wt.unlock)
3202
# The r1-lib1 revision should be merged into this one
3203
self.assertEqual([b'r1-project', b'r1-lib1'], project_wt.get_parent_ids())
3204
self.assertTreeEntriesEqual(
3205
[('', b'project-root-id'),
3206
('README', b'project-README-id'),
3207
('dir', b'project-dir-id'),
3208
('lib1', b'lib1-root-id'),
3209
('dir/file.c', b'project-file.c-id'),
3210
('lib1/Makefile', b'lib1-Makefile-id'),
3211
('lib1/README', b'lib1-README-id'),
3212
('lib1/foo.c', b'lib1-foo.c-id'),
3215
def test_subdir(self):
3216
"""Merge a branch into a subdirectory of an existing directory."""
3217
project_wt, lib_wt = self.setup_two_branches()
3218
self.do_merge_into('lib1', 'project/dir/lib1')
3219
project_wt.lock_read()
3220
self.addCleanup(project_wt.unlock)
3221
# The r1-lib1 revision should be merged into this one
3222
self.assertEqual([b'r1-project', b'r1-lib1'], project_wt.get_parent_ids())
3223
self.assertTreeEntriesEqual(
3224
[('', b'project-root-id'),
3225
('README', b'project-README-id'),
3226
('dir', b'project-dir-id'),
3227
('dir/file.c', b'project-file.c-id'),
3228
('dir/lib1', b'lib1-root-id'),
3229
('dir/lib1/Makefile', b'lib1-Makefile-id'),
3230
('dir/lib1/README', b'lib1-README-id'),
3231
('dir/lib1/foo.c', b'lib1-foo.c-id'),
3234
def test_newdir_with_repeat_roots(self):
3235
"""If the file-id of the dir to be merged already exists a new ID will
3236
be allocated to let the merge happen.
3238
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3239
root_id = project_wt.path2id('')
3240
self.do_merge_into('lib1', 'project/lib1')
3241
project_wt.lock_read()
3242
self.addCleanup(project_wt.unlock)
3243
# The r1-lib1 revision should be merged into this one
3244
self.assertEqual([b'r1-project', b'r1-lib1'], project_wt.get_parent_ids())
3245
new_lib1_id = project_wt.path2id('lib1')
3246
self.assertNotEqual(None, new_lib1_id)
3247
self.assertTreeEntriesEqual(
3249
('README', b'project-README-id'),
3250
('dir', b'project-dir-id'),
3251
('lib1', new_lib1_id),
3252
('dir/file.c', b'project-file.c-id'),
3253
('lib1/Makefile', b'lib1-Makefile-id'),
3254
('lib1/README', b'lib1-README-id'),
3255
('lib1/foo.c', b'lib1-foo.c-id'),
3258
def test_name_conflict(self):
3259
"""When the target directory name already exists a conflict is
3260
generated and the original directory is renamed to foo.moved.
3262
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3263
src_wt = self.setup_simple_branch('src', ['README'])
3264
conflicts = self.do_merge_into('src', 'dest/dir')
3265
self.assertEqual(1, conflicts)
3267
self.addCleanup(dest_wt.unlock)
3268
# The r1-lib1 revision should be merged into this one
3269
self.assertEqual([b'r1-dest', b'r1-src'], dest_wt.get_parent_ids())
3270
self.assertTreeEntriesEqual(
3271
[('', b'dest-root-id'),
3272
('dir', b'src-root-id'),
3273
('dir.moved', b'dest-dir-id'),
3274
('dir/README', b'src-README-id'),
3275
('dir.moved/file.txt', b'dest-file.txt-id'),
3278
def test_file_id_conflict(self):
3279
"""A conflict is generated if the merge-into adds a file (or other
3280
inventory entry) with a file-id that already exists in the target tree.
3282
dest_wt = self.setup_simple_branch('dest', ['file.txt'])
3283
# Make a second tree with a file-id that will clash with file.txt in
3285
src_wt = self.make_branch_and_tree('src')
3286
self.build_tree(['src/README'])
3287
src_wt.add(['README'], ids=[b'dest-file.txt-id'])
3288
src_wt.commit("Rev 1 of src.", rev_id=b'r1-src')
3289
conflicts = self.do_merge_into('src', 'dest/dir')
3290
# This is an edge case that shouldn't happen to users very often. So
3291
# we don't care really about the exact presentation of the conflict,
3292
# just that there is one.
3293
self.assertEqual(1, conflicts)
3295
def test_only_subdir(self):
3296
"""When the location points to just part of a tree, merge just that
3299
dest_wt = self.setup_simple_branch('dest')
3300
src_wt = self.setup_simple_branch(
3301
'src', ['hello.txt', 'dir/', 'dir/foo.c'])
3302
conflicts = self.do_merge_into('src/dir', 'dest/dir')
3304
self.addCleanup(dest_wt.unlock)
3305
# The r1-lib1 revision should NOT be merged into this one (this is a
3307
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3308
self.assertTreeEntriesEqual(
3309
[('', b'dest-root-id'),
3310
('dir', b'src-dir-id'),
3311
('dir/foo.c', b'src-foo.c-id'),
3314
def test_only_file(self):
3315
"""An edge case: merge just one file, not a whole dir."""
3316
dest_wt = self.setup_simple_branch('dest')
3317
two_file_wt = self.setup_simple_branch(
3318
'two-file', ['file1.txt', 'file2.txt'])
3319
conflicts = self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3321
self.addCleanup(dest_wt.unlock)
3322
# The r1-lib1 revision should NOT be merged into this one
3323
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3324
self.assertTreeEntriesEqual(
3325
[('', b'dest-root-id'), ('file1.txt', b'two-file-file1.txt-id')],
3328
def test_no_such_source_path(self):
3329
"""PathNotInTree is raised if the specified path in the source tree
3332
dest_wt = self.setup_simple_branch('dest')
3333
two_file_wt = self.setup_simple_branch('src', ['dir/'])
3334
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3335
'src/no-such-dir', 'dest/foo')
3337
self.addCleanup(dest_wt.unlock)
3338
# The dest tree is unmodified.
3339
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3340
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3342
def test_no_such_target_path(self):
3343
"""PathNotInTree is also raised if the specified path in the target
3344
tree does not exist.
3346
dest_wt = self.setup_simple_branch('dest')
3347
two_file_wt = self.setup_simple_branch('src', ['file.txt'])
3348
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3349
'src', 'dest/no-such-dir/foo')
3351
self.addCleanup(dest_wt.unlock)
3352
# The dest tree is unmodified.
3353
self.assertEqual([b'r1-dest'], dest_wt.get_parent_ids())
3354
self.assertTreeEntriesEqual([('', b'dest-root-id')], dest_wt)
3357
class TestMergeHooks(TestCaseWithTransport):
3360
super(TestMergeHooks, self).setUp()
3361
self.tree_a = self.make_branch_and_tree('tree_a')
3362
self.build_tree_contents([('tree_a/file', b'content_1')])
3363
self.tree_a.add('file', b'file-id')
3364
self.tree_a.commit('added file')
3366
self.tree_b = self.tree_a.controldir.sprout('tree_b').open_workingtree()
3367
self.build_tree_contents([('tree_b/file', b'content_2')])
3368
self.tree_b.commit('modify file')
3370
def test_pre_merge_hook_inject_different_tree(self):
3371
tree_c = self.tree_b.controldir.sprout('tree_c').open_workingtree()
3372
self.build_tree_contents([('tree_c/file', b'content_3')])
3373
tree_c.commit("more content")
3375
def factory(merger):
3376
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3377
merger.other_tree = tree_c
3378
calls.append(merger)
3379
_mod_merge.Merger.hooks.install_named_hook('pre_merge',
3380
factory, 'test factory')
3381
self.tree_a.merge_from_branch(self.tree_b.branch)
3383
self.assertFileEqual(b"content_3", 'tree_a/file')
3384
self.assertLength(1, calls)
3386
def test_post_merge_hook_called(self):
3388
def factory(merger):
3389
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3390
calls.append(merger)
3391
_mod_merge.Merger.hooks.install_named_hook('post_merge',
3392
factory, 'test factory')
3394
self.tree_a.merge_from_branch(self.tree_b.branch)
3396
self.assertFileEqual(b"content_2", 'tree_a/file')
3397
self.assertLength(1, calls)