1
# Copyright (C) 2005-2010 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
18
from StringIO import StringIO
21
branch as _mod_branch,
30
revision as _mod_revision,
35
from bzrlib.conflicts import ConflictList, TextConflict
36
from bzrlib.errors import UnrelatedBranches, NoCommits
37
from bzrlib.merge import transform_tree, merge_inner, _PlanMerge
38
from bzrlib.osutils import basename, pathjoin, file_kind
39
from bzrlib.tests import (
40
TestCaseWithMemoryTransport,
41
TestCaseWithTransport,
44
from bzrlib.workingtree import WorkingTree
47
class TestMerge(TestCaseWithTransport):
48
"""Test appending more than one revision"""
50
def test_pending(self):
51
wt = self.make_branch_and_tree('.')
52
rev_a = wt.commit("lala!")
53
self.assertEqual([rev_a], wt.get_parent_ids())
54
self.assertRaises(errors.PointlessMerge, wt.merge_from_branch,
56
self.assertEqual([rev_a], wt.get_parent_ids())
60
wt = self.make_branch_and_tree('.')
64
wt.merge_from_branch(wt.branch, wt.branch.get_rev_id(2),
65
wt.branch.get_rev_id(1))
67
def test_nocommits(self):
68
wt = self.test_pending()
69
wt2 = self.make_branch_and_tree('branch2')
70
self.assertRaises(NoCommits, wt.merge_from_branch, wt2.branch)
73
def test_unrelated(self):
74
wt, wt2 = self.test_nocommits()
76
self.assertRaises(UnrelatedBranches, wt.merge_from_branch, wt2.branch)
79
def test_merge_one_file(self):
80
"""Do a partial merge of a tree which should not affect tree parents."""
81
wt1 = self.make_branch_and_tree('branch1')
82
tip = wt1.commit('empty commit')
83
wt2 = self.make_branch_and_tree('branch2')
85
file('branch1/foo', 'wb').write('foo')
86
file('branch1/bar', 'wb').write('bar')
89
wt1.commit('add foobar')
91
self.run_bzr('merge ../branch1/baz', retcode=3)
92
self.run_bzr('merge ../branch1/foo')
93
self.assertPathExists('foo')
94
self.assertPathDoesNotExist('bar')
95
wt2 = WorkingTree.open('.') # opens branch2
96
self.assertEqual([tip], wt2.get_parent_ids())
98
def test_pending_with_null(self):
99
"""When base is forced to revno 0, parent_ids are set"""
100
wt2 = self.test_unrelated()
101
wt1 = WorkingTree.open('.')
103
br1.fetch(wt2.branch)
104
# merge all of branch 2 into branch 1 even though they
106
wt1.merge_from_branch(wt2.branch, wt2.last_revision(), 'null:')
107
self.assertEqual([br1.last_revision(), wt2.branch.last_revision()],
108
wt1.get_parent_ids())
109
return (wt1, wt2.branch)
111
def test_two_roots(self):
112
"""Merge base is sane when two unrelated branches are merged"""
113
wt1, br2 = self.test_pending_with_null()
117
last = wt1.branch.last_revision()
118
last2 = br2.last_revision()
119
graph = wt1.branch.repository.get_graph()
120
self.assertEqual(last2, graph.find_unique_lca(last, last2))
124
def test_merge_into_null_tree(self):
125
wt = self.make_branch_and_tree('tree')
126
null_tree = wt.basis_tree()
127
self.build_tree(['tree/file'])
129
wt.commit('tree with root')
130
merger = _mod_merge.Merge3Merger(null_tree, null_tree, null_tree, wt,
131
this_branch=wt.branch,
133
with merger.make_preview_transform() as tt:
134
self.assertEqual([], tt.find_conflicts())
135
preview = tt.get_preview_tree()
136
self.assertEqual(wt.get_root_id(), preview.get_root_id())
138
def test_merge_unrelated_retains_root(self):
139
wt = self.make_branch_and_tree('tree')
140
root_id_before_merge = wt.get_root_id()
141
other_tree = self.make_branch_and_tree('other')
142
# Do a commit so there is something to merge
143
other_tree.commit('commit other')
144
self.assertNotEquals(root_id_before_merge, other_tree.get_root_id())
145
wt.merge_from_branch(other_tree.branch,
146
from_revision=_mod_revision.NULL_REVISION)
147
self.assertEqual(root_id_before_merge, wt.get_root_id())
149
def test_merge_preview_unrelated_retains_root(self):
150
wt = self.make_branch_and_tree('tree')
151
other_tree = self.make_branch_and_tree('other')
152
# Do a commit so there is something to merge
153
other_tree.commit('commit other')
154
merger = _mod_merge.Merge3Merger(wt, wt, wt.basis_tree(), other_tree,
155
this_branch=wt.branch,
157
with merger.make_preview_transform() as tt:
158
preview = tt.get_preview_tree()
159
self.assertEqual(wt.get_root_id(), preview.get_root_id())
161
def test_create_rename(self):
162
"""Rename an inventory entry while creating the file"""
163
tree =self.make_branch_and_tree('.')
164
file('name1', 'wb').write('Hello')
166
tree.commit(message="hello")
167
tree.rename_one('name1', 'name2')
169
transform_tree(tree, tree.branch.basis_tree())
171
def test_layered_rename(self):
172
"""Rename both child and parent at same time"""
173
tree =self.make_branch_and_tree('.')
176
filename = pathjoin('dirname1', 'name1')
177
file(filename, 'wb').write('Hello')
179
tree.commit(message="hello")
180
filename2 = pathjoin('dirname1', 'name2')
181
tree.rename_one(filename, filename2)
182
tree.rename_one('dirname1', 'dirname2')
183
transform_tree(tree, tree.branch.basis_tree())
185
def test_ignore_zero_merge_inner(self):
186
# Test that merge_inner's ignore zero parameter is effective
187
tree_a =self.make_branch_and_tree('a')
188
tree_a.commit(message="hello")
189
dir_b = tree_a.bzrdir.sprout('b')
190
tree_b = dir_b.open_workingtree()
192
self.addCleanup(tree_b.unlock)
193
tree_a.commit(message="hello again")
195
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
196
this_tree=tree_b, ignore_zero=True)
197
self.assertTrue('All changes applied successfully.\n' not in
200
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
201
this_tree=tree_b, ignore_zero=False)
202
self.assertTrue('All changes applied successfully.\n' in self.get_log())
204
def test_merge_inner_conflicts(self):
205
tree_a = self.make_branch_and_tree('a')
206
tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
207
merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
208
self.assertEqual(1, len(tree_a.conflicts()))
210
def test_rmdir_conflict(self):
211
tree_a = self.make_branch_and_tree('a')
212
self.build_tree(['a/b/'])
213
tree_a.add('b', 'b-id')
214
tree_a.commit('added b')
215
# basis_tree() is only guaranteed to be valid as long as it is actually
216
# the basis tree. This mutates the tree after grabbing basis, so go to
218
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
219
tree_z = tree_a.bzrdir.sprout('z').open_workingtree()
220
self.build_tree(['a/b/c'])
222
tree_a.commit('added c')
224
tree_z.commit('removed b')
225
merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
227
conflicts.MissingParent('Created directory', 'b', 'b-id'),
228
conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
230
merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
233
conflicts.DeletingParent('Not deleting', 'b', 'b-id'),
234
conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
237
def test_nested_merge(self):
238
tree = self.make_branch_and_tree('tree',
239
format='dirstate-with-subtree')
240
sub_tree = self.make_branch_and_tree('tree/sub-tree',
241
format='dirstate-with-subtree')
242
sub_tree.set_root_id('sub-tree-root')
243
self.build_tree_contents([('tree/sub-tree/file', 'text1')])
245
sub_tree.commit('foo')
246
tree.add_reference(sub_tree)
247
tree.commit('set text to 1')
248
tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
249
# modify the file in the subtree
250
self.build_tree_contents([('tree2/sub-tree/file', 'text2')])
251
# and merge the changes from the diverged subtree into the containing
253
tree2.commit('changed file text')
254
tree.merge_from_branch(tree2.branch)
255
self.assertFileEqual('text2', 'tree/sub-tree/file')
257
def test_merge_with_missing(self):
258
tree_a = self.make_branch_and_tree('tree_a')
259
self.build_tree_contents([('tree_a/file', 'content_1')])
261
tree_a.commit('commit base')
262
# basis_tree() is only guaranteed to be valid as long as it is actually
263
# the basis tree. This test commits to the tree after grabbing basis,
264
# so we go to the repository.
265
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
266
tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
267
self.build_tree_contents([('tree_a/file', 'content_2')])
268
tree_a.commit('commit other')
269
other_tree = tree_a.basis_tree()
270
# 'file' is now missing but isn't altered in any commit in b so no
271
# change should be applied.
272
os.unlink('tree_b/file')
273
merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
275
def test_merge_kind_change(self):
276
tree_a = self.make_branch_and_tree('tree_a')
277
self.build_tree_contents([('tree_a/file', 'content_1')])
278
tree_a.add('file', 'file-id')
279
tree_a.commit('added file')
280
tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
281
os.unlink('tree_a/file')
282
self.build_tree(['tree_a/file/'])
283
tree_a.commit('changed file to directory')
284
tree_b.merge_from_branch(tree_a.branch)
285
self.assertEqual('directory', file_kind('tree_b/file'))
287
self.assertEqual('file', file_kind('tree_b/file'))
288
self.build_tree_contents([('tree_b/file', 'content_2')])
289
tree_b.commit('content change')
290
tree_b.merge_from_branch(tree_a.branch)
291
self.assertEqual(tree_b.conflicts(),
292
[conflicts.ContentsConflict('file',
295
def test_merge_type_registry(self):
296
merge_type_option = option.Option.OPTIONS['merge-type']
297
self.assertFalse('merge4' in [x[0] for x in
298
merge_type_option.iter_switches()])
299
registry = _mod_merge.get_merge_type_registry()
300
registry.register_lazy('merge4', 'bzrlib.merge', 'Merge4Merger',
301
'time-travelling merge')
302
self.assertTrue('merge4' in [x[0] for x in
303
merge_type_option.iter_switches()])
304
registry.remove('merge4')
305
self.assertFalse('merge4' in [x[0] for x in
306
merge_type_option.iter_switches()])
308
def test_merge_other_moves_we_deleted(self):
309
tree_a = self.make_branch_and_tree('A')
311
self.addCleanup(tree_a.unlock)
312
self.build_tree(['A/a'])
314
tree_a.commit('1', rev_id='rev-1')
316
tree_a.rename_one('a', 'b')
318
bzrdir_b = tree_a.bzrdir.sprout('B', revision_id='rev-1')
319
tree_b = bzrdir_b.open_workingtree()
321
self.addCleanup(tree_b.unlock)
325
tree_b.merge_from_branch(tree_a.branch)
326
except AttributeError:
327
self.fail('tried to join a path when name was None')
329
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
330
tree_a = self.make_branch_and_tree('a')
331
self.build_tree(['a/file_1', 'a/file_2'])
332
tree_a.add(['file_1'])
333
tree_a.commit('commit 1')
334
tree_a.add(['file_2'])
335
tree_a.commit('commit 2')
336
tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
337
tree_b.rename_one('file_1', 'renamed')
338
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
339
merger.merge_type = _mod_merge.Merge3Merger
341
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
343
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
344
tree_a = self.make_branch_and_tree('a')
345
self.build_tree(['a/file_1', 'a/file_2'])
346
tree_a.add(['file_1'])
347
tree_a.commit('commit 1')
348
tree_a.add(['file_2'])
349
tree_a.commit('commit 2')
350
tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
351
tree_b.rename_one('file_1', 'renamed')
352
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
353
merger.merge_type = _mod_merge.WeaveMerger
355
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
357
def prepare_cherrypick(self):
358
"""Prepare a pair of trees for cherrypicking tests.
360
Both trees have a file, 'file'.
361
rev1 sets content to 'a'.
364
A full merge of rev2b and rev3b into this_tree would add both 'b' and
365
'c'. A successful cherrypick of rev2b-rev3b into this_tree will add
368
this_tree = self.make_branch_and_tree('this')
369
self.build_tree_contents([('this/file', "a\n")])
370
this_tree.add('file')
371
this_tree.commit('rev1')
372
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
373
self.build_tree_contents([('other/file', "a\nb\n")])
374
other_tree.commit('rev2b', rev_id='rev2b')
375
self.build_tree_contents([('other/file', "c\na\nb\n")])
376
other_tree.commit('rev3b', rev_id='rev3b')
377
this_tree.lock_write()
378
self.addCleanup(this_tree.unlock)
379
return this_tree, other_tree
381
def test_weave_cherrypick(self):
382
this_tree, other_tree = self.prepare_cherrypick()
383
merger = _mod_merge.Merger.from_revision_ids(None,
384
this_tree, 'rev3b', 'rev2b', other_tree.branch)
385
merger.merge_type = _mod_merge.WeaveMerger
387
self.assertFileEqual('c\na\n', 'this/file')
389
def test_weave_cannot_reverse_cherrypick(self):
390
this_tree, other_tree = self.prepare_cherrypick()
391
merger = _mod_merge.Merger.from_revision_ids(None,
392
this_tree, 'rev2b', 'rev3b', other_tree.branch)
393
merger.merge_type = _mod_merge.WeaveMerger
394
self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
396
def test_merge3_can_reverse_cherrypick(self):
397
this_tree, other_tree = self.prepare_cherrypick()
398
merger = _mod_merge.Merger.from_revision_ids(None,
399
this_tree, 'rev2b', 'rev3b', other_tree.branch)
400
merger.merge_type = _mod_merge.Merge3Merger
403
def test_merge3_will_detect_cherrypick(self):
404
this_tree = self.make_branch_and_tree('this')
405
self.build_tree_contents([('this/file', "a\n")])
406
this_tree.add('file')
407
this_tree.commit('rev1')
408
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
409
self.build_tree_contents([('other/file', "a\nb\n")])
410
other_tree.commit('rev2b', rev_id='rev2b')
411
self.build_tree_contents([('other/file', "a\nb\nc\n")])
412
other_tree.commit('rev3b', rev_id='rev3b')
413
this_tree.lock_write()
414
self.addCleanup(this_tree.unlock)
416
merger = _mod_merge.Merger.from_revision_ids(None,
417
this_tree, 'rev3b', 'rev2b', other_tree.branch)
418
merger.merge_type = _mod_merge.Merge3Merger
420
self.assertFileEqual('a\n'
424
'>>>>>>> MERGE-SOURCE\n',
427
def test_merge_reverse_revision_range(self):
428
tree = self.make_branch_and_tree(".")
430
self.addCleanup(tree.unlock)
431
self.build_tree(['a'])
433
tree.commit("added a")
434
first_rev = tree.branch.revision_history()[0]
435
merger = _mod_merge.Merger.from_revision_ids(None, tree,
436
_mod_revision.NULL_REVISION,
438
merger.merge_type = _mod_merge.Merge3Merger
439
merger.interesting_files = 'a'
440
conflict_count = merger.do_merge()
441
self.assertEqual(0, conflict_count)
443
self.assertPathDoesNotExist("a")
445
self.assertPathExists("a")
447
def test_make_merger(self):
448
this_tree = self.make_branch_and_tree('this')
449
this_tree.commit('rev1', rev_id='rev1')
450
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
451
this_tree.commit('rev2', rev_id='rev2a')
452
other_tree.commit('rev2', rev_id='rev2b')
453
this_tree.lock_write()
454
self.addCleanup(this_tree.unlock)
455
merger = _mod_merge.Merger.from_revision_ids(None,
456
this_tree, 'rev2b', other_branch=other_tree.branch)
457
merger.merge_type = _mod_merge.Merge3Merger
458
tree_merger = merger.make_merger()
459
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
460
self.assertEqual('rev2b', tree_merger.other_tree.get_revision_id())
461
self.assertEqual('rev1', tree_merger.base_tree.get_revision_id())
463
def test_make_preview_transform(self):
464
this_tree = self.make_branch_and_tree('this')
465
self.build_tree_contents([('this/file', '1\n')])
466
this_tree.add('file', 'file-id')
467
this_tree.commit('rev1', rev_id='rev1')
468
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
469
self.build_tree_contents([('this/file', '1\n2a\n')])
470
this_tree.commit('rev2', rev_id='rev2a')
471
self.build_tree_contents([('other/file', '2b\n1\n')])
472
other_tree.commit('rev2', rev_id='rev2b')
473
this_tree.lock_write()
474
self.addCleanup(this_tree.unlock)
475
merger = _mod_merge.Merger.from_revision_ids(None,
476
this_tree, 'rev2b', other_branch=other_tree.branch)
477
merger.merge_type = _mod_merge.Merge3Merger
478
tree_merger = merger.make_merger()
479
tt = tree_merger.make_preview_transform()
480
self.addCleanup(tt.finalize)
481
preview_tree = tt.get_preview_tree()
482
tree_file = this_tree.get_file('file-id')
484
self.assertEqual('1\n2a\n', tree_file.read())
487
preview_file = preview_tree.get_file('file-id')
489
self.assertEqual('2b\n1\n2a\n', preview_file.read())
493
def test_do_merge(self):
494
this_tree = self.make_branch_and_tree('this')
495
self.build_tree_contents([('this/file', '1\n')])
496
this_tree.add('file', 'file-id')
497
this_tree.commit('rev1', rev_id='rev1')
498
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
499
self.build_tree_contents([('this/file', '1\n2a\n')])
500
this_tree.commit('rev2', rev_id='rev2a')
501
self.build_tree_contents([('other/file', '2b\n1\n')])
502
other_tree.commit('rev2', rev_id='rev2b')
503
this_tree.lock_write()
504
self.addCleanup(this_tree.unlock)
505
merger = _mod_merge.Merger.from_revision_ids(None,
506
this_tree, 'rev2b', other_branch=other_tree.branch)
507
merger.merge_type = _mod_merge.Merge3Merger
508
tree_merger = merger.make_merger()
509
tt = tree_merger.do_merge()
510
tree_file = this_tree.get_file('file-id')
512
self.assertEqual('2b\n1\n2a\n', tree_file.read())
516
def test_merge_require_tree_root(self):
517
tree = self.make_branch_and_tree(".")
519
self.addCleanup(tree.unlock)
520
self.build_tree(['a'])
522
tree.commit("added a")
523
old_root_id = tree.get_root_id()
524
first_rev = tree.branch.revision_history()[0]
525
merger = _mod_merge.Merger.from_revision_ids(None, tree,
526
_mod_revision.NULL_REVISION,
528
merger.merge_type = _mod_merge.Merge3Merger
529
conflict_count = merger.do_merge()
530
self.assertEqual(0, conflict_count)
531
self.assertEquals(set([old_root_id]), tree.all_file_ids())
532
tree.set_parent_ids([])
534
def test_merge_add_into_deleted_root(self):
535
# Yes, people actually do this. And report bugs if it breaks.
536
source = self.make_branch_and_tree('source', format='rich-root-pack')
537
self.build_tree(['source/foo/'])
538
source.add('foo', 'foo-id')
539
source.commit('Add foo')
540
target = source.bzrdir.sprout('target').open_workingtree()
541
subtree = target.extract('foo-id')
542
subtree.commit('Delete root')
543
self.build_tree(['source/bar'])
544
source.add('bar', 'bar-id')
545
source.commit('Add bar')
546
subtree.merge_from_branch(source.branch)
548
def test_merge_joined_branch(self):
549
source = self.make_branch_and_tree('source', format='rich-root-pack')
550
self.build_tree(['source/foo'])
552
source.commit('Add foo')
553
target = self.make_branch_and_tree('target', format='rich-root-pack')
554
self.build_tree(['target/bla'])
556
target.commit('Add bla')
557
nested = source.bzrdir.sprout('target/subtree').open_workingtree()
558
target.subsume(nested)
559
target.commit('Join nested')
560
self.build_tree(['source/bar'])
562
source.commit('Add bar')
563
target.merge_from_branch(source.branch)
564
target.commit('Merge source')
567
class TestPlanMerge(TestCaseWithMemoryTransport):
570
TestCaseWithMemoryTransport.setUp(self)
571
mapper = versionedfile.PrefixMapper()
572
factory = knit.make_file_factory(True, mapper)
573
self.vf = factory(self.get_transport())
574
self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
575
self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
577
def add_version(self, key, parents, text):
578
self.vf.add_lines(key, parents, [c+'\n' for c in text])
580
def add_rev(self, prefix, revision_id, parents, text):
581
self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
584
def add_uncommitted_version(self, key, parents, text):
585
self.plan_merge_vf.add_lines(key, parents,
586
[c+'\n' for c in text])
588
def setup_plan_merge(self):
589
self.add_rev('root', 'A', [], 'abc')
590
self.add_rev('root', 'B', ['A'], 'acehg')
591
self.add_rev('root', 'C', ['A'], 'fabg')
592
return _PlanMerge('B', 'C', self.plan_merge_vf, ('root',))
594
def setup_plan_merge_uncommitted(self):
595
self.add_version(('root', 'A'), [], 'abc')
596
self.add_uncommitted_version(('root', 'B:'), [('root', 'A')], 'acehg')
597
self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')
598
return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))
600
def test_base_from_plan(self):
601
self.setup_plan_merge()
602
plan = self.plan_merge_vf.plan_merge('B', 'C')
603
pwm = versionedfile.PlanWeaveMerge(plan)
604
self.assertEqual(['a\n', 'b\n', 'c\n'], pwm.base_from_plan())
606
def test_unique_lines(self):
607
plan = self.setup_plan_merge()
608
self.assertEqual(plan._unique_lines(
609
plan._get_matching_blocks('B', 'C')),
612
def test_plan_merge(self):
613
self.setup_plan_merge()
614
plan = self.plan_merge_vf.plan_merge('B', 'C')
617
('unchanged', 'a\n'),
626
def test_plan_merge_cherrypick(self):
627
self.add_rev('root', 'A', [], 'abc')
628
self.add_rev('root', 'B', ['A'], 'abcde')
629
self.add_rev('root', 'C', ['A'], 'abcefg')
630
self.add_rev('root', 'D', ['A', 'B', 'C'], 'abcdegh')
631
my_plan = _PlanMerge('B', 'D', self.plan_merge_vf, ('root',))
632
# We shortcut when one text supersedes the other in the per-file graph.
633
# We don't actually need to compare the texts at this point.
642
list(my_plan.plan_merge()))
644
def test_plan_merge_no_common_ancestor(self):
645
self.add_rev('root', 'A', [], 'abc')
646
self.add_rev('root', 'B', [], 'xyz')
647
my_plan = _PlanMerge('A', 'B', self.plan_merge_vf, ('root',))
655
list(my_plan.plan_merge()))
657
def test_plan_merge_tail_ancestors(self):
658
# The graph looks like this:
659
# A # Common to all ancestors
661
# B C # Ancestors of E, only common to one side
663
# D E F # D, F are unique to G, H respectively
664
# |/ \| # E is the LCA for G & H, and the unique LCA for
669
# I J # criss-cross merge of G, H
671
# In this situation, a simple pruning of ancestors of E will leave D &
672
# F "dangling", which looks like they introduce lines different from
673
# the ones in E, but in actuality C&B introduced the lines, and they
674
# are already present in E
676
# Introduce the base text
677
self.add_rev('root', 'A', [], 'abc')
678
# Introduces a new line B
679
self.add_rev('root', 'B', ['A'], 'aBbc')
680
# Introduces a new line C
681
self.add_rev('root', 'C', ['A'], 'abCc')
682
# Introduce new line D
683
self.add_rev('root', 'D', ['B'], 'DaBbc')
684
# Merges B and C by just incorporating both
685
self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
686
# Introduce new line F
687
self.add_rev('root', 'F', ['C'], 'abCcF')
688
# Merge D & E by just combining the texts
689
self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
690
# Merge F & E by just combining the texts
691
self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
692
# Merge G & H by just combining texts
693
self.add_rev('root', 'I', ['G', 'H'], 'DaBbCcF')
694
# Merge G & H but supersede an old line in B
695
self.add_rev('root', 'J', ['H', 'G'], 'DaJbCcF')
696
plan = self.plan_merge_vf.plan_merge('I', 'J')
698
('unchanged', 'D\n'),
699
('unchanged', 'a\n'),
702
('unchanged', 'b\n'),
703
('unchanged', 'C\n'),
704
('unchanged', 'c\n'),
705
('unchanged', 'F\n')],
708
def test_plan_merge_tail_triple_ancestors(self):
709
# The graph looks like this:
710
# A # Common to all ancestors
712
# B C # Ancestors of E, only common to one side
714
# D E F # D, F are unique to G, H respectively
715
# |/|\| # E is the LCA for G & H, and the unique LCA for
717
# |\ /| # Q is just an extra node which is merged into both
720
# I J # criss-cross merge of G, H
722
# This is the same as the test_plan_merge_tail_ancestors, except we add
723
# a third LCA that doesn't add new lines, but will trigger our more
724
# involved ancestry logic
726
self.add_rev('root', 'A', [], 'abc')
727
self.add_rev('root', 'B', ['A'], 'aBbc')
728
self.add_rev('root', 'C', ['A'], 'abCc')
729
self.add_rev('root', 'D', ['B'], 'DaBbc')
730
self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
731
self.add_rev('root', 'F', ['C'], 'abCcF')
732
self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
733
self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
734
self.add_rev('root', 'Q', ['E'], 'aBbCc')
735
self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DaBbCcF')
736
# Merge G & H but supersede an old line in B
737
self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DaJbCcF')
738
plan = self.plan_merge_vf.plan_merge('I', 'J')
740
('unchanged', 'D\n'),
741
('unchanged', 'a\n'),
744
('unchanged', 'b\n'),
745
('unchanged', 'C\n'),
746
('unchanged', 'c\n'),
747
('unchanged', 'F\n')],
750
def test_plan_merge_2_tail_triple_ancestors(self):
751
# The graph looks like this:
752
# A B # 2 tails going back to NULL
754
# D E F # D, is unique to G, F to H
755
# |/|\| # E is the LCA for G & H, and the unique LCA for
757
# |\ /| # Q is just an extra node which is merged into both
760
# I J # criss-cross merge of G, H (and Q)
763
# This is meant to test after hitting a 3-way LCA, and multiple tail
764
# ancestors (only have NULL_REVISION in common)
766
self.add_rev('root', 'A', [], 'abc')
767
self.add_rev('root', 'B', [], 'def')
768
self.add_rev('root', 'D', ['A'], 'Dabc')
769
self.add_rev('root', 'E', ['A', 'B'], 'abcdef')
770
self.add_rev('root', 'F', ['B'], 'defF')
771
self.add_rev('root', 'G', ['D', 'E'], 'Dabcdef')
772
self.add_rev('root', 'H', ['F', 'E'], 'abcdefF')
773
self.add_rev('root', 'Q', ['E'], 'abcdef')
774
self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DabcdefF')
775
# Merge G & H but supersede an old line in B
776
self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DabcdJfF')
777
plan = self.plan_merge_vf.plan_merge('I', 'J')
779
('unchanged', 'D\n'),
780
('unchanged', 'a\n'),
781
('unchanged', 'b\n'),
782
('unchanged', 'c\n'),
783
('unchanged', 'd\n'),
786
('unchanged', 'f\n'),
787
('unchanged', 'F\n')],
790
def test_plan_merge_uncommitted_files(self):
791
self.setup_plan_merge_uncommitted()
792
plan = self.plan_merge_vf.plan_merge('B:', 'C:')
795
('unchanged', 'a\n'),
804
def test_plan_merge_insert_order(self):
805
"""Weave merges are sensitive to the order of insertion.
807
Specifically for overlapping regions, it effects which region gets put
808
'first'. And when a user resolves an overlapping merge, if they use the
809
same ordering, then the lines match the parents, if they don't only
810
*some* of the lines match.
812
self.add_rev('root', 'A', [], 'abcdef')
813
self.add_rev('root', 'B', ['A'], 'abwxcdef')
814
self.add_rev('root', 'C', ['A'], 'abyzcdef')
815
# Merge, and resolve the conflict by adding *both* sets of lines
816
# If we get the ordering wrong, these will look like new lines in D,
817
# rather than carried over from B, C
818
self.add_rev('root', 'D', ['B', 'C'],
820
# Supersede the lines in B and delete the lines in C, which will
821
# conflict if they are treated as being in D
822
self.add_rev('root', 'E', ['C', 'B'],
824
# Same thing for the lines in C
825
self.add_rev('root', 'F', ['C'], 'abpqcdef')
826
plan = self.plan_merge_vf.plan_merge('D', 'E')
828
('unchanged', 'a\n'),
829
('unchanged', 'b\n'),
836
('unchanged', 'c\n'),
837
('unchanged', 'd\n'),
838
('unchanged', 'e\n'),
839
('unchanged', 'f\n')],
841
plan = self.plan_merge_vf.plan_merge('E', 'D')
842
# Going in the opposite direction shows the effect of the opposite plan
844
('unchanged', 'a\n'),
845
('unchanged', 'b\n'),
850
('killed-both', 'w\n'),
851
('killed-both', 'x\n'),
854
('unchanged', 'c\n'),
855
('unchanged', 'd\n'),
856
('unchanged', 'e\n'),
857
('unchanged', 'f\n')],
860
def test_plan_merge_criss_cross(self):
861
# This is specificly trying to trigger problems when using limited
862
# ancestry and weaves. The ancestry graph looks like:
863
# XX unused ancestor, should not show up in the weave
867
# B \ Introduces a line 'foo'
869
# C D E C & D both have 'foo', E has different changes
873
# F G All of C, D, E are merged into F and G, so they are
874
# all common ancestors.
876
# The specific issue with weaves:
877
# B introduced a text ('foo') that is present in both C and D.
878
# If we do not include B (because it isn't an ancestor of E), then
879
# the A=>C and A=>D look like both sides independently introduce the
880
# text ('foo'). If F does not modify the text, it would still appear
881
# to have deleted on of the versions from C or D. If G then modifies
882
# 'foo', it should appear as superseding the value in F (since it
883
# came from B), rather than conflict because of the resolution during
885
self.add_rev('root', 'XX', [], 'qrs')
886
self.add_rev('root', 'A', ['XX'], 'abcdef')
887
self.add_rev('root', 'B', ['A'], 'axcdef')
888
self.add_rev('root', 'C', ['B'], 'axcdefg')
889
self.add_rev('root', 'D', ['B'], 'haxcdef')
890
self.add_rev('root', 'E', ['A'], 'abcdyf')
891
# Simple combining of all texts
892
self.add_rev('root', 'F', ['C', 'D', 'E'], 'haxcdyfg')
893
# combine and supersede 'x'
894
self.add_rev('root', 'G', ['C', 'D', 'E'], 'hazcdyfg')
895
plan = self.plan_merge_vf.plan_merge('F', 'G')
897
('unchanged', 'h\n'),
898
('unchanged', 'a\n'),
899
('killed-base', 'b\n'),
902
('unchanged', 'c\n'),
903
('unchanged', 'd\n'),
904
('killed-base', 'e\n'),
905
('unchanged', 'y\n'),
906
('unchanged', 'f\n'),
907
('unchanged', 'g\n')],
909
plan = self.plan_merge_vf.plan_lca_merge('F', 'G')
910
# This is one of the main differences between plan_merge and
911
# plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
912
# because 'x' was not present in one of the bases. However, in this
913
# case it is spurious because 'x' does not exist in the global base A.
915
('unchanged', 'h\n'),
916
('unchanged', 'a\n'),
917
('conflicted-a', 'x\n'),
919
('unchanged', 'c\n'),
920
('unchanged', 'd\n'),
921
('unchanged', 'y\n'),
922
('unchanged', 'f\n'),
923
('unchanged', 'g\n')],
926
def test_criss_cross_flip_flop(self):
927
# This is specificly trying to trigger problems when using limited
928
# ancestry and weaves. The ancestry graph looks like:
929
# XX unused ancestor, should not show up in the weave
933
# B C B & C both introduce a new line
937
# D E B & C are both merged, so both are common ancestors
938
# In the process of merging, both sides order the new
941
self.add_rev('root', 'XX', [], 'qrs')
942
self.add_rev('root', 'A', ['XX'], 'abcdef')
943
self.add_rev('root', 'B', ['A'], 'abcdgef')
944
self.add_rev('root', 'C', ['A'], 'abcdhef')
945
self.add_rev('root', 'D', ['B', 'C'], 'abcdghef')
946
self.add_rev('root', 'E', ['C', 'B'], 'abcdhgef')
947
plan = list(self.plan_merge_vf.plan_merge('D', 'E'))
949
('unchanged', 'a\n'),
950
('unchanged', 'b\n'),
951
('unchanged', 'c\n'),
952
('unchanged', 'd\n'),
954
('unchanged', 'g\n'),
956
('unchanged', 'e\n'),
957
('unchanged', 'f\n'),
959
pwm = versionedfile.PlanWeaveMerge(plan)
960
self.assertEqualDiff('\n'.join('abcdghef') + '\n',
961
''.join(pwm.base_from_plan()))
962
# Reversing the order reverses the merge plan, and final order of 'hg'
964
plan = list(self.plan_merge_vf.plan_merge('E', 'D'))
966
('unchanged', 'a\n'),
967
('unchanged', 'b\n'),
968
('unchanged', 'c\n'),
969
('unchanged', 'd\n'),
971
('unchanged', 'h\n'),
973
('unchanged', 'e\n'),
974
('unchanged', 'f\n'),
976
pwm = versionedfile.PlanWeaveMerge(plan)
977
self.assertEqualDiff('\n'.join('abcdhgef') + '\n',
978
''.join(pwm.base_from_plan()))
979
# This is where lca differs, in that it (fairly correctly) determines
980
# that there is a conflict because both sides resolved the merge
982
plan = list(self.plan_merge_vf.plan_lca_merge('D', 'E'))
984
('unchanged', 'a\n'),
985
('unchanged', 'b\n'),
986
('unchanged', 'c\n'),
987
('unchanged', 'd\n'),
988
('conflicted-b', 'h\n'),
989
('unchanged', 'g\n'),
990
('conflicted-a', 'h\n'),
991
('unchanged', 'e\n'),
992
('unchanged', 'f\n'),
994
pwm = versionedfile.PlanWeaveMerge(plan)
995
self.assertEqualDiff('\n'.join('abcdgef') + '\n',
996
''.join(pwm.base_from_plan()))
997
# Reversing it changes what line is doubled, but still gives a
999
plan = list(self.plan_merge_vf.plan_lca_merge('E', 'D'))
1001
('unchanged', 'a\n'),
1002
('unchanged', 'b\n'),
1003
('unchanged', 'c\n'),
1004
('unchanged', 'd\n'),
1005
('conflicted-b', 'g\n'),
1006
('unchanged', 'h\n'),
1007
('conflicted-a', 'g\n'),
1008
('unchanged', 'e\n'),
1009
('unchanged', 'f\n'),
1011
pwm = versionedfile.PlanWeaveMerge(plan)
1012
self.assertEqualDiff('\n'.join('abcdhef') + '\n',
1013
''.join(pwm.base_from_plan()))
1015
def assertRemoveExternalReferences(self, filtered_parent_map,
1016
child_map, tails, parent_map):
1017
"""Assert results for _PlanMerge._remove_external_references."""
1018
(act_filtered_parent_map, act_child_map,
1019
act_tails) = _PlanMerge._remove_external_references(parent_map)
1021
# The parent map *should* preserve ordering, but the ordering of
1022
# children is not strictly defined
1023
# child_map = dict((k, sorted(children))
1024
# for k, children in child_map.iteritems())
1025
# act_child_map = dict(k, sorted(children)
1026
# for k, children in act_child_map.iteritems())
1027
self.assertEqual(filtered_parent_map, act_filtered_parent_map)
1028
self.assertEqual(child_map, act_child_map)
1029
self.assertEqual(sorted(tails), sorted(act_tails))
1031
def test__remove_external_references(self):
1032
# First, nothing to remove
1033
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1034
{1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
1035
# The reverse direction
1036
self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
1037
{3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
1039
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
1040
{1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
1042
self.assertRemoveExternalReferences(
1043
{4: [2, 3], 3: [], 2: [1], 1: []},
1044
{1: [2], 2: [4], 3: [4], 4: []},
1046
{4: [2, 3], 3: [5], 2: [1], 1: [6]})
1048
self.assertRemoveExternalReferences(
1049
{1: [3], 2: [3, 4], 3: [], 4: []},
1050
{1: [], 2: [], 3: [1, 2], 4: [2]},
1052
{1: [3], 2: [3, 4], 3: [5], 4: []})
1054
def assertPruneTails(self, pruned_map, tails, parent_map):
1056
for key, parent_keys in parent_map.iteritems():
1057
child_map.setdefault(key, [])
1058
for pkey in parent_keys:
1059
child_map.setdefault(pkey, []).append(key)
1060
_PlanMerge._prune_tails(parent_map, child_map, tails)
1061
self.assertEqual(pruned_map, parent_map)
1063
def test__prune_tails(self):
1064
# Nothing requested to prune
1065
self.assertPruneTails({1: [], 2: [], 3: []}, [],
1066
{1: [], 2: [], 3: []})
1067
# Prune a single entry
1068
self.assertPruneTails({1: [], 3: []}, [2],
1069
{1: [], 2: [], 3: []})
1071
self.assertPruneTails({1: []}, [3],
1072
{1: [], 2: [3], 3: []})
1073
# Prune a chain with a diamond
1074
self.assertPruneTails({1: []}, [5],
1075
{1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
1076
# Prune a partial chain
1077
self.assertPruneTails({1: [6], 6:[]}, [5],
1078
{1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
1080
# Prune a chain with multiple tips, that pulls out intermediates
1081
self.assertPruneTails({1:[3], 3:[]}, [4, 5],
1082
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1083
self.assertPruneTails({1:[3], 3:[]}, [5, 4],
1084
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1086
def test_subtract_plans(self):
1088
('unchanged', 'a\n'),
1090
('killed-a', 'c\n'),
1093
('killed-b', 'f\n'),
1094
('killed-b', 'g\n'),
1097
('unchanged', 'a\n'),
1099
('killed-a', 'c\n'),
1102
('killed-b', 'f\n'),
1103
('killed-b', 'i\n'),
1106
('unchanged', 'a\n'),
1108
('killed-a', 'c\n'),
1110
('unchanged', 'f\n'),
1111
('killed-b', 'i\n'),
1113
self.assertEqual(subtracted_plan,
1114
list(_PlanMerge._subtract_plans(old_plan, new_plan)))
1116
def setup_merge_with_base(self):
1117
self.add_rev('root', 'COMMON', [], 'abc')
1118
self.add_rev('root', 'THIS', ['COMMON'], 'abcd')
1119
self.add_rev('root', 'BASE', ['COMMON'], 'eabc')
1120
self.add_rev('root', 'OTHER', ['BASE'], 'eafb')
1122
def test_plan_merge_with_base(self):
1123
self.setup_merge_with_base()
1124
plan = self.plan_merge_vf.plan_merge('THIS', 'OTHER', 'BASE')
1125
self.assertEqual([('unchanged', 'a\n'),
1127
('unchanged', 'b\n'),
1128
('killed-b', 'c\n'),
1132
def test_plan_lca_merge(self):
1133
self.setup_plan_merge()
1134
plan = self.plan_merge_vf.plan_lca_merge('B', 'C')
1137
('unchanged', 'a\n'),
1138
('killed-b', 'c\n'),
1141
('killed-a', 'b\n'),
1142
('unchanged', 'g\n')],
1145
def test_plan_lca_merge_uncommitted_files(self):
1146
self.setup_plan_merge_uncommitted()
1147
plan = self.plan_merge_vf.plan_lca_merge('B:', 'C:')
1150
('unchanged', 'a\n'),
1151
('killed-b', 'c\n'),
1154
('killed-a', 'b\n'),
1155
('unchanged', 'g\n')],
1158
def test_plan_lca_merge_with_base(self):
1159
self.setup_merge_with_base()
1160
plan = self.plan_merge_vf.plan_lca_merge('THIS', 'OTHER', 'BASE')
1161
self.assertEqual([('unchanged', 'a\n'),
1163
('unchanged', 'b\n'),
1164
('killed-b', 'c\n'),
1168
def test_plan_lca_merge_with_criss_cross(self):
1169
self.add_version(('root', 'ROOT'), [], 'abc')
1170
# each side makes a change
1171
self.add_version(('root', 'REV1'), [('root', 'ROOT')], 'abcd')
1172
self.add_version(('root', 'REV2'), [('root', 'ROOT')], 'abce')
1173
# both sides merge, discarding others' changes
1174
self.add_version(('root', 'LCA1'),
1175
[('root', 'REV1'), ('root', 'REV2')], 'abcd')
1176
self.add_version(('root', 'LCA2'),
1177
[('root', 'REV1'), ('root', 'REV2')], 'fabce')
1178
plan = self.plan_merge_vf.plan_lca_merge('LCA1', 'LCA2')
1179
self.assertEqual([('new-b', 'f\n'),
1180
('unchanged', 'a\n'),
1181
('unchanged', 'b\n'),
1182
('unchanged', 'c\n'),
1183
('conflicted-a', 'd\n'),
1184
('conflicted-b', 'e\n'),
1187
def test_plan_lca_merge_with_null(self):
1188
self.add_version(('root', 'A'), [], 'ab')
1189
self.add_version(('root', 'B'), [], 'bc')
1190
plan = self.plan_merge_vf.plan_lca_merge('A', 'B')
1191
self.assertEqual([('new-a', 'a\n'),
1192
('unchanged', 'b\n'),
1196
def test_plan_merge_with_delete_and_change(self):
1197
self.add_rev('root', 'C', [], 'a')
1198
self.add_rev('root', 'A', ['C'], 'b')
1199
self.add_rev('root', 'B', ['C'], '')
1200
plan = self.plan_merge_vf.plan_merge('A', 'B')
1201
self.assertEqual([('killed-both', 'a\n'),
1205
def test_plan_merge_with_move_and_change(self):
1206
self.add_rev('root', 'C', [], 'abcd')
1207
self.add_rev('root', 'A', ['C'], 'acbd')
1208
self.add_rev('root', 'B', ['C'], 'aBcd')
1209
plan = self.plan_merge_vf.plan_merge('A', 'B')
1210
self.assertEqual([('unchanged', 'a\n'),
1212
('killed-b', 'b\n'),
1214
('killed-a', 'c\n'),
1215
('unchanged', 'd\n'),
1219
class LoggingMerger(object):
1220
# These seem to be the required attributes
1221
requires_base = False
1222
supports_reprocess = False
1223
supports_show_base = False
1224
supports_cherrypick = False
1225
# We intentionally do not define supports_lca_trees
1227
def __init__(self, *args, **kwargs):
1229
self.kwargs = kwargs
1232
class TestMergerBase(TestCaseWithMemoryTransport):
1233
"""Common functionality for Merger tests that don't write to disk."""
1235
def get_builder(self):
1236
builder = self.make_branch_builder('path')
1237
builder.start_series()
1238
self.addCleanup(builder.finish_series)
1241
def setup_simple_graph(self):
1242
"""Create a simple 3-node graph.
1244
:return: A BranchBuilder
1251
builder = self.get_builder()
1252
builder.build_snapshot('A-id', None,
1253
[('add', ('', None, 'directory', None))])
1254
builder.build_snapshot('C-id', ['A-id'], [])
1255
builder.build_snapshot('B-id', ['A-id'], [])
1258
def setup_criss_cross_graph(self):
1259
"""Create a 5-node graph with a criss-cross.
1261
:return: A BranchBuilder
1268
builder = self.setup_simple_graph()
1269
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1270
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1273
def make_Merger(self, builder, other_revision_id,
1274
interesting_files=None, interesting_ids=None):
1275
"""Make a Merger object from a branch builder"""
1276
mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1277
mem_tree.lock_write()
1278
self.addCleanup(mem_tree.unlock)
1279
merger = _mod_merge.Merger.from_revision_ids(None,
1280
mem_tree, other_revision_id)
1281
merger.set_interesting_files(interesting_files)
1282
# It seems there is no matching function for set_interesting_ids
1283
merger.interesting_ids = interesting_ids
1284
merger.merge_type = _mod_merge.Merge3Merger
1288
class TestMergerInMemory(TestMergerBase):
1290
def test_cache_trees_with_revision_ids_None(self):
1291
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1292
original_cache = dict(merger._cached_trees)
1293
merger.cache_trees_with_revision_ids([None])
1294
self.assertEqual(original_cache, merger._cached_trees)
1296
def test_cache_trees_with_revision_ids_no_revision_id(self):
1297
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1298
original_cache = dict(merger._cached_trees)
1299
tree = self.make_branch_and_memory_tree('tree')
1300
merger.cache_trees_with_revision_ids([tree])
1301
self.assertEqual(original_cache, merger._cached_trees)
1303
def test_cache_trees_with_revision_ids_having_revision_id(self):
1304
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1305
original_cache = dict(merger._cached_trees)
1306
tree = merger.this_branch.repository.revision_tree('B-id')
1307
original_cache['B-id'] = tree
1308
merger.cache_trees_with_revision_ids([tree])
1309
self.assertEqual(original_cache, merger._cached_trees)
1311
def test_find_base(self):
1312
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1313
self.assertEqual('A-id', merger.base_rev_id)
1314
self.assertFalse(merger._is_criss_cross)
1315
self.assertIs(None, merger._lca_trees)
1317
def test_find_base_criss_cross(self):
1318
builder = self.setup_criss_cross_graph()
1319
merger = self.make_Merger(builder, 'E-id')
1320
self.assertEqual('A-id', merger.base_rev_id)
1321
self.assertTrue(merger._is_criss_cross)
1322
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1323
for t in merger._lca_trees])
1324
# If we swap the order, we should get a different lca order
1325
builder.build_snapshot('F-id', ['E-id'], [])
1326
merger = self.make_Merger(builder, 'D-id')
1327
self.assertEqual(['C-id', 'B-id'], [t.get_revision_id()
1328
for t in merger._lca_trees])
1330
def test_find_base_triple_criss_cross(self):
1333
# B C F # F is merged into both branches
1340
builder = self.setup_criss_cross_graph()
1341
builder.build_snapshot('F-id', ['A-id'], [])
1342
builder.build_snapshot('H-id', ['E-id', 'F-id'], [])
1343
builder.build_snapshot('G-id', ['D-id', 'F-id'], [])
1344
merger = self.make_Merger(builder, 'H-id')
1345
self.assertEqual(['B-id', 'C-id', 'F-id'],
1346
[t.get_revision_id() for t in merger._lca_trees])
1348
def test_find_base_new_root_criss_cross(self):
1355
builder = self.get_builder()
1356
builder.build_snapshot('A-id', None,
1357
[('add', ('', None, 'directory', None))])
1358
builder.build_snapshot('B-id', [],
1359
[('add', ('', None, 'directory', None))])
1360
builder.build_snapshot('D-id', ['A-id', 'B-id'], [])
1361
builder.build_snapshot('C-id', ['A-id', 'B-id'], [])
1362
merger = self.make_Merger(builder, 'D-id')
1363
self.assertEqual('A-id', merger.base_rev_id)
1364
self.assertTrue(merger._is_criss_cross)
1365
self.assertEqual(['A-id', 'B-id'], [t.get_revision_id()
1366
for t in merger._lca_trees])
1368
def test_no_criss_cross_passed_to_merge_type(self):
1369
class LCATreesMerger(LoggingMerger):
1370
supports_lca_trees = True
1372
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1373
merger.merge_type = LCATreesMerger
1374
merge_obj = merger.make_merger()
1375
self.assertIsInstance(merge_obj, LCATreesMerger)
1376
self.assertFalse('lca_trees' in merge_obj.kwargs)
1378
def test_criss_cross_passed_to_merge_type(self):
1379
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1380
merger.merge_type = _mod_merge.Merge3Merger
1381
merge_obj = merger.make_merger()
1382
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1383
for t in merger._lca_trees])
1385
def test_criss_cross_not_supported_merge_type(self):
1386
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1387
# We explicitly do not define supports_lca_trees
1388
merger.merge_type = LoggingMerger
1389
merge_obj = merger.make_merger()
1390
self.assertIsInstance(merge_obj, LoggingMerger)
1391
self.assertFalse('lca_trees' in merge_obj.kwargs)
1393
def test_criss_cross_unsupported_merge_type(self):
1394
class UnsupportedLCATreesMerger(LoggingMerger):
1395
supports_lca_trees = False
1397
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1398
merger.merge_type = UnsupportedLCATreesMerger
1399
merge_obj = merger.make_merger()
1400
self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
1401
self.assertFalse('lca_trees' in merge_obj.kwargs)
1404
class TestMergerEntriesLCA(TestMergerBase):
1406
def make_merge_obj(self, builder, other_revision_id,
1407
interesting_files=None, interesting_ids=None):
1408
merger = self.make_Merger(builder, other_revision_id,
1409
interesting_files=interesting_files,
1410
interesting_ids=interesting_ids)
1411
return merger.make_merger()
1413
def test_simple(self):
1414
builder = self.get_builder()
1415
builder.build_snapshot('A-id', None,
1416
[('add', (u'', 'a-root-id', 'directory', None)),
1417
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1418
builder.build_snapshot('C-id', ['A-id'],
1419
[('modify', ('a-id', 'a\nb\nC\nc\n'))])
1420
builder.build_snapshot('B-id', ['A-id'],
1421
[('modify', ('a-id', 'a\nB\nb\nc\n'))])
1422
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1423
[('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
1424
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1425
[('modify', ('a-id', 'a\nB\nb\nC\nc\n'))])
1426
merge_obj = self.make_merge_obj(builder, 'E-id')
1428
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1429
for t in merge_obj._lca_trees])
1430
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1431
entries = list(merge_obj._entries_lca())
1433
# (file_id, changed, parents, names, executable)
1434
# BASE, lca1, lca2, OTHER, THIS
1435
root_id = 'a-root-id'
1436
self.assertEqual([('a-id', True,
1437
((root_id, [root_id, root_id]), root_id, root_id),
1438
((u'a', [u'a', u'a']), u'a', u'a'),
1439
((False, [False, False]), False, False)),
1442
def test_not_in_base(self):
1443
# LCAs all have the same last-modified revision for the file, as do
1444
# the tips, but the base has something different
1445
# A base, doesn't have the file
1447
# B C B introduces 'foo', C introduces 'bar'
1449
# D E D and E now both have 'foo' and 'bar'
1451
# F G the files are now in F, G, D and E, but not in A
1454
builder = self.get_builder()
1455
builder.build_snapshot('A-id', None,
1456
[('add', (u'', 'a-root-id', 'directory', None))])
1457
builder.build_snapshot('B-id', ['A-id'],
1458
[('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
1459
builder.build_snapshot('C-id', ['A-id'],
1460
[('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
1461
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1462
[('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
1463
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1464
[('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
1465
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1466
[('modify', (u'bar-id', 'd\ne\nf\nG\n'))])
1467
builder.build_snapshot('F-id', ['D-id', 'E-id'], [])
1468
merge_obj = self.make_merge_obj(builder, 'G-id')
1470
self.assertEqual(['D-id', 'E-id'], [t.get_revision_id()
1471
for t in merge_obj._lca_trees])
1472
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1473
entries = list(merge_obj._entries_lca())
1474
root_id = 'a-root-id'
1475
self.assertEqual([('bar-id', True,
1476
((None, [root_id, root_id]), root_id, root_id),
1477
((None, [u'bar', u'bar']), u'bar', u'bar'),
1478
((None, [False, False]), False, False)),
1481
def test_not_in_this(self):
1482
builder = self.get_builder()
1483
builder.build_snapshot('A-id', None,
1484
[('add', (u'', 'a-root-id', 'directory', None)),
1485
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1486
builder.build_snapshot('B-id', ['A-id'],
1487
[('modify', ('a-id', 'a\nB\nb\nc\n'))])
1488
builder.build_snapshot('C-id', ['A-id'],
1489
[('modify', ('a-id', 'a\nb\nC\nc\n'))])
1490
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1491
[('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
1492
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1493
[('unversion', 'a-id')])
1494
merge_obj = self.make_merge_obj(builder, 'E-id')
1496
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1497
for t in merge_obj._lca_trees])
1498
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1500
entries = list(merge_obj._entries_lca())
1501
root_id = 'a-root-id'
1502
self.assertEqual([('a-id', True,
1503
((root_id, [root_id, root_id]), root_id, None),
1504
((u'a', [u'a', u'a']), u'a', None),
1505
((False, [False, False]), False, None)),
1508
def test_file_not_in_one_lca(self):
1511
# B C # B no file, C introduces a file
1513
# D E # D and E both have the file, unchanged from C
1514
builder = self.get_builder()
1515
builder.build_snapshot('A-id', None,
1516
[('add', (u'', 'a-root-id', 'directory', None))])
1517
builder.build_snapshot('B-id', ['A-id'], [])
1518
builder.build_snapshot('C-id', ['A-id'],
1519
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1520
builder.build_snapshot('E-id', ['C-id', 'B-id'], []) # Inherited from C
1521
builder.build_snapshot('D-id', ['B-id', 'C-id'], # Merged from C
1522
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1523
merge_obj = self.make_merge_obj(builder, 'E-id')
1525
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1526
for t in merge_obj._lca_trees])
1527
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1529
entries = list(merge_obj._entries_lca())
1530
self.assertEqual([], entries)
1532
def test_not_in_other(self):
1533
builder = self.get_builder()
1534
builder.build_snapshot('A-id', None,
1535
[('add', (u'', 'a-root-id', 'directory', None)),
1536
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1537
builder.build_snapshot('B-id', ['A-id'], [])
1538
builder.build_snapshot('C-id', ['A-id'], [])
1539
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1540
[('unversion', 'a-id')])
1541
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1542
merge_obj = self.make_merge_obj(builder, 'E-id')
1544
entries = list(merge_obj._entries_lca())
1545
root_id = 'a-root-id'
1546
self.assertEqual([('a-id', True,
1547
((root_id, [root_id, root_id]), None, root_id),
1548
((u'a', [u'a', u'a']), None, u'a'),
1549
((False, [False, False]), None, False)),
1552
def test_not_in_other_or_lca(self):
1553
# A base, introduces 'foo'
1555
# B C B nothing, C deletes foo
1557
# D E D restores foo (same as B), E leaves it deleted
1559
# A => B, no changes
1560
# A => C, delete foo (C should supersede B)
1561
# C => D, restore foo
1562
# C => E, no changes
1563
# D would then win 'cleanly' and no record would be given
1564
builder = self.get_builder()
1565
builder.build_snapshot('A-id', None,
1566
[('add', (u'', 'a-root-id', 'directory', None)),
1567
('add', (u'foo', 'foo-id', 'file', 'content\n'))])
1568
builder.build_snapshot('B-id', ['A-id'], [])
1569
builder.build_snapshot('C-id', ['A-id'],
1570
[('unversion', 'foo-id')])
1571
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1572
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1573
merge_obj = self.make_merge_obj(builder, 'E-id')
1575
entries = list(merge_obj._entries_lca())
1576
self.assertEqual([], entries)
1578
def test_not_in_other_mod_in_lca1_not_in_lca2(self):
1579
# A base, introduces 'foo'
1581
# B C B changes 'foo', C deletes foo
1583
# D E D restores foo (same as B), E leaves it deleted (as C)
1585
# A => B, modified foo
1586
# A => C, delete foo, C does not supersede B
1587
# B => D, no changes
1588
# C => D, resolve in favor of B
1589
# B => E, resolve in favor of E
1590
# C => E, no changes
1591
# In this case, we have a conflict of how the changes were resolved. E
1592
# picked C and D picked B, so we should issue a conflict
1593
builder = self.get_builder()
1594
builder.build_snapshot('A-id', None,
1595
[('add', (u'', 'a-root-id', 'directory', None)),
1596
('add', (u'foo', 'foo-id', 'file', 'content\n'))])
1597
builder.build_snapshot('B-id', ['A-id'], [
1598
('modify', ('foo-id', 'new-content\n'))])
1599
builder.build_snapshot('C-id', ['A-id'],
1600
[('unversion', 'foo-id')])
1601
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1602
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1603
merge_obj = self.make_merge_obj(builder, 'E-id')
1605
entries = list(merge_obj._entries_lca())
1606
root_id = 'a-root-id'
1607
self.assertEqual([('foo-id', True,
1608
((root_id, [root_id, None]), None, root_id),
1609
((u'foo', [u'foo', None]), None, 'foo'),
1610
((False, [False, None]), None, False)),
1613
def test_only_in_one_lca(self):
1616
# B C B nothing, C add file
1618
# D E D still has nothing, E removes file
1621
# C => D, removed the file
1623
# C => E, removed the file
1624
# Thus D & E have identical changes, and this is a no-op
1627
# A => C, add file, thus C supersedes B
1628
# w/ C=BASE, D=THIS, E=OTHER we have 'happy convergence'
1629
builder = self.get_builder()
1630
builder.build_snapshot('A-id', None,
1631
[('add', (u'', 'a-root-id', 'directory', None))])
1632
builder.build_snapshot('B-id', ['A-id'], [])
1633
builder.build_snapshot('C-id', ['A-id'],
1634
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1635
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1636
[('unversion', 'a-id')])
1637
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1638
merge_obj = self.make_merge_obj(builder, 'E-id')
1640
entries = list(merge_obj._entries_lca())
1641
self.assertEqual([], entries)
1643
def test_only_in_other(self):
1644
builder = self.get_builder()
1645
builder.build_snapshot('A-id', None,
1646
[('add', (u'', 'a-root-id', 'directory', None))])
1647
builder.build_snapshot('B-id', ['A-id'], [])
1648
builder.build_snapshot('C-id', ['A-id'], [])
1649
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1650
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1651
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1652
merge_obj = self.make_merge_obj(builder, 'E-id')
1654
entries = list(merge_obj._entries_lca())
1655
root_id = 'a-root-id'
1656
self.assertEqual([('a-id', True,
1657
((None, [None, None]), root_id, None),
1658
((None, [None, None]), u'a', None),
1659
((None, [None, None]), False, None)),
1662
def test_one_lca_supersedes(self):
1663
# One LCA supersedes the other LCAs last modified value, but the
1664
# value is not the same as BASE.
1665
# A base, introduces 'foo', last mod A
1667
# B C B modifies 'foo' (mod B), C does nothing (mod A)
1669
# D E D does nothing (mod B), E updates 'foo' (mod E)
1671
# F G F updates 'foo' (mod F). G does nothing (mod E)
1673
# At this point, G should not be considered to modify 'foo', even
1674
# though its LCAs disagree. This is because the modification in E
1675
# completely supersedes the value in D.
1676
builder = self.get_builder()
1677
builder.build_snapshot('A-id', None,
1678
[('add', (u'', 'a-root-id', 'directory', None)),
1679
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1680
builder.build_snapshot('C-id', ['A-id'], [])
1681
builder.build_snapshot('B-id', ['A-id'],
1682
[('modify', ('foo-id', 'B content\n'))])
1683
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1684
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1685
[('modify', ('foo-id', 'E content\n'))])
1686
builder.build_snapshot('G-id', ['E-id', 'D-id'], [])
1687
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1688
[('modify', ('foo-id', 'F content\n'))])
1689
merge_obj = self.make_merge_obj(builder, 'G-id')
1691
self.assertEqual([], list(merge_obj._entries_lca()))
1693
def test_one_lca_supersedes_path(self):
1694
# Double-criss-cross merge, the ultimate base value is different from
1698
# B C B value 'bar', C = 'foo'
1700
# D E D = 'bar', E supersedes to 'bing'
1702
# F G F = 'bing', G supersedes to 'barry'
1704
# In this case, we technically should not care about the value 'bar' for
1705
# D, because it was clearly superseded by E's 'bing'. The
1706
# per-file/attribute graph would actually look like:
1715
# Because the other side of the merge never modifies the value, it just
1716
# takes the value from the merge.
1718
# ATM this fails because we will prune 'foo' from the LCAs, but we
1719
# won't prune 'bar'. This is getting far off into edge-case land, so we
1720
# aren't supporting it yet.
1722
builder = self.get_builder()
1723
builder.build_snapshot('A-id', None,
1724
[('add', (u'', 'a-root-id', 'directory', None)),
1725
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1726
builder.build_snapshot('C-id', ['A-id'], [])
1727
builder.build_snapshot('B-id', ['A-id'],
1728
[('rename', ('foo', 'bar'))])
1729
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1730
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1731
[('rename', ('foo', 'bing'))]) # override to bing
1732
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1733
[('rename', ('bing', 'barry'))]) # override to barry
1734
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1735
[('rename', ('bar', 'bing'))]) # Merge in E's change
1736
merge_obj = self.make_merge_obj(builder, 'G-id')
1738
self.expectFailure("We don't do an actual heads() check on lca values,"
1739
" or use the per-attribute graph",
1740
self.assertEqual, [], list(merge_obj._entries_lca()))
1742
def test_one_lca_accidentally_pruned(self):
1743
# Another incorrect resolution from the same basic flaw:
1746
# B C B value 'bar', C = 'foo'
1748
# D E D = 'bar', E reverts to 'foo'
1750
# F G F = 'bing', G switches to 'bar'
1752
# 'bar' will not be seen as an interesting change, because 'foo' will
1753
# be pruned from the LCAs, even though it was newly introduced by E
1755
builder = self.get_builder()
1756
builder.build_snapshot('A-id', None,
1757
[('add', (u'', 'a-root-id', 'directory', None)),
1758
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1759
builder.build_snapshot('C-id', ['A-id'], [])
1760
builder.build_snapshot('B-id', ['A-id'],
1761
[('rename', ('foo', 'bar'))])
1762
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1763
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1764
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1765
[('rename', ('foo', 'bar'))])
1766
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1767
[('rename', ('bar', 'bing'))]) # should end up conflicting
1768
merge_obj = self.make_merge_obj(builder, 'G-id')
1770
entries = list(merge_obj._entries_lca())
1771
root_id = 'a-root-id'
1772
self.expectFailure("We prune values from BASE even when relevant.",
1775
((root_id, [root_id, root_id]), root_id, root_id),
1776
((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
1777
((False, [False, False]), False, False)),
1780
def test_both_sides_revert(self):
1781
# Both sides of a criss-cross revert the text to the lca
1782
# A base, introduces 'foo'
1784
# B C B modifies 'foo', C modifies 'foo'
1786
# D E D reverts to B, E reverts to C
1787
# This should conflict
1788
builder = self.get_builder()
1789
builder.build_snapshot('A-id', None,
1790
[('add', (u'', 'a-root-id', 'directory', None)),
1791
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1792
builder.build_snapshot('B-id', ['A-id'],
1793
[('modify', ('foo-id', 'B content\n'))])
1794
builder.build_snapshot('C-id', ['A-id'],
1795
[('modify', ('foo-id', 'C content\n'))])
1796
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1797
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1798
merge_obj = self.make_merge_obj(builder, 'E-id')
1800
entries = list(merge_obj._entries_lca())
1801
root_id = 'a-root-id'
1802
self.assertEqual([('foo-id', True,
1803
((root_id, [root_id, root_id]), root_id, root_id),
1804
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1805
((False, [False, False]), False, False)),
1808
def test_different_lca_resolve_one_side_updates_content(self):
1809
# Both sides converge, but then one side updates the text.
1810
# A base, introduces 'foo'
1812
# B C B modifies 'foo', C modifies 'foo'
1814
# D E D reverts to B, E reverts to C
1816
# F F updates to a new value
1817
# We need to emit an entry for 'foo', because D & E differed on the
1819
builder = self.get_builder()
1820
builder.build_snapshot('A-id', None,
1821
[('add', (u'', 'a-root-id', 'directory', None)),
1822
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1823
builder.build_snapshot('B-id', ['A-id'],
1824
[('modify', ('foo-id', 'B content\n'))])
1825
builder.build_snapshot('C-id', ['A-id'],
1826
[('modify', ('foo-id', 'C content\n'))])
1827
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1828
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1829
builder.build_snapshot('F-id', ['D-id'],
1830
[('modify', ('foo-id', 'F content\n'))])
1831
merge_obj = self.make_merge_obj(builder, 'E-id')
1833
entries = list(merge_obj._entries_lca())
1834
root_id = 'a-root-id'
1835
self.assertEqual([('foo-id', True,
1836
((root_id, [root_id, root_id]), root_id, root_id),
1837
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1838
((False, [False, False]), False, False)),
1841
def test_same_lca_resolution_one_side_updates_content(self):
1842
# Both sides converge, but then one side updates the text.
1843
# A base, introduces 'foo'
1845
# B C B modifies 'foo', C modifies 'foo'
1847
# D E D and E use C's value
1849
# F F updates to a new value
1850
# I think it is a bug that this conflicts, but we don't have a way to
1851
# detect otherwise. And because of:
1852
# test_different_lca_resolve_one_side_updates_content
1853
# We need to conflict.
1855
builder = self.get_builder()
1856
builder.build_snapshot('A-id', None,
1857
[('add', (u'', 'a-root-id', 'directory', None)),
1858
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1859
builder.build_snapshot('B-id', ['A-id'],
1860
[('modify', ('foo-id', 'B content\n'))])
1861
builder.build_snapshot('C-id', ['A-id'],
1862
[('modify', ('foo-id', 'C content\n'))])
1863
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1864
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1865
[('modify', ('foo-id', 'C content\n'))]) # Same as E
1866
builder.build_snapshot('F-id', ['D-id'],
1867
[('modify', ('foo-id', 'F content\n'))])
1868
merge_obj = self.make_merge_obj(builder, 'E-id')
1870
entries = list(merge_obj._entries_lca())
1871
self.expectFailure("We don't detect that LCA resolution was the"
1872
" same on both sides",
1873
self.assertEqual, [], entries)
1875
def test_only_path_changed(self):
1876
builder = self.get_builder()
1877
builder.build_snapshot('A-id', None,
1878
[('add', (u'', 'a-root-id', 'directory', None)),
1879
('add', (u'a', 'a-id', 'file', 'content\n'))])
1880
builder.build_snapshot('B-id', ['A-id'], [])
1881
builder.build_snapshot('C-id', ['A-id'], [])
1882
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1883
[('rename', (u'a', u'b'))])
1884
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1885
merge_obj = self.make_merge_obj(builder, 'E-id')
1886
entries = list(merge_obj._entries_lca())
1887
root_id = 'a-root-id'
1888
# The content was not changed, only the path
1889
self.assertEqual([('a-id', False,
1890
((root_id, [root_id, root_id]), root_id, root_id),
1891
((u'a', [u'a', u'a']), u'b', u'a'),
1892
((False, [False, False]), False, False)),
1895
def test_kind_changed(self):
1896
# Identical content, except 'D' changes a-id into a directory
1897
builder = self.get_builder()
1898
builder.build_snapshot('A-id', None,
1899
[('add', (u'', 'a-root-id', 'directory', None)),
1900
('add', (u'a', 'a-id', 'file', 'content\n'))])
1901
builder.build_snapshot('B-id', ['A-id'], [])
1902
builder.build_snapshot('C-id', ['A-id'], [])
1903
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1904
[('unversion', 'a-id'),
1906
('add', (u'a', 'a-id', 'directory', None))])
1907
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1908
merge_obj = self.make_merge_obj(builder, 'E-id')
1909
entries = list(merge_obj._entries_lca())
1910
root_id = 'a-root-id'
1911
# Only the kind was changed (content)
1912
self.assertEqual([('a-id', True,
1913
((root_id, [root_id, root_id]), root_id, root_id),
1914
((u'a', [u'a', u'a']), u'a', u'a'),
1915
((False, [False, False]), False, False)),
1918
def test_this_changed_kind(self):
1919
# Identical content, but THIS changes a file to a directory
1920
builder = self.get_builder()
1921
builder.build_snapshot('A-id', None,
1922
[('add', (u'', 'a-root-id', 'directory', None)),
1923
('add', (u'a', 'a-id', 'file', 'content\n'))])
1924
builder.build_snapshot('B-id', ['A-id'], [])
1925
builder.build_snapshot('C-id', ['A-id'], [])
1926
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1927
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1928
[('unversion', 'a-id'),
1930
('add', (u'a', 'a-id', 'directory', None))])
1931
merge_obj = self.make_merge_obj(builder, 'E-id')
1932
entries = list(merge_obj._entries_lca())
1933
# Only the kind was changed (content)
1934
self.assertEqual([], entries)
1936
def test_interesting_files(self):
1937
# Two files modified, but we should filter one of them
1938
builder = self.get_builder()
1939
builder.build_snapshot('A-id', None,
1940
[('add', (u'', 'a-root-id', 'directory', None)),
1941
('add', (u'a', 'a-id', 'file', 'content\n')),
1942
('add', (u'b', 'b-id', 'file', 'content\n'))])
1943
builder.build_snapshot('B-id', ['A-id'], [])
1944
builder.build_snapshot('C-id', ['A-id'], [])
1945
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1946
[('modify', ('a-id', 'new-content\n')),
1947
('modify', ('b-id', 'new-content\n'))])
1948
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1949
merge_obj = self.make_merge_obj(builder, 'E-id',
1950
interesting_files=['b'])
1951
entries = list(merge_obj._entries_lca())
1952
root_id = 'a-root-id'
1953
self.assertEqual([('b-id', True,
1954
((root_id, [root_id, root_id]), root_id, root_id),
1955
((u'b', [u'b', u'b']), u'b', u'b'),
1956
((False, [False, False]), False, False)),
1959
def test_interesting_file_in_this(self):
1960
# This renamed the file, but it should still match the entry in other
1961
builder = self.get_builder()
1962
builder.build_snapshot('A-id', None,
1963
[('add', (u'', 'a-root-id', 'directory', None)),
1964
('add', (u'a', 'a-id', 'file', 'content\n')),
1965
('add', (u'b', 'b-id', 'file', 'content\n'))])
1966
builder.build_snapshot('B-id', ['A-id'], [])
1967
builder.build_snapshot('C-id', ['A-id'], [])
1968
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1969
[('modify', ('a-id', 'new-content\n')),
1970
('modify', ('b-id', 'new-content\n'))])
1971
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1972
[('rename', ('b', 'c'))])
1973
merge_obj = self.make_merge_obj(builder, 'E-id',
1974
interesting_files=['c'])
1975
entries = list(merge_obj._entries_lca())
1976
root_id = 'a-root-id'
1977
self.assertEqual([('b-id', True,
1978
((root_id, [root_id, root_id]), root_id, root_id),
1979
((u'b', [u'b', u'b']), u'b', u'c'),
1980
((False, [False, False]), False, False)),
1983
def test_interesting_file_in_base(self):
1984
# This renamed the file, but it should still match the entry in BASE
1985
builder = self.get_builder()
1986
builder.build_snapshot('A-id', None,
1987
[('add', (u'', 'a-root-id', 'directory', None)),
1988
('add', (u'a', 'a-id', 'file', 'content\n')),
1989
('add', (u'c', 'c-id', 'file', 'content\n'))])
1990
builder.build_snapshot('B-id', ['A-id'],
1991
[('rename', ('c', 'b'))])
1992
builder.build_snapshot('C-id', ['A-id'],
1993
[('rename', ('c', 'b'))])
1994
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1995
[('modify', ('a-id', 'new-content\n')),
1996
('modify', ('c-id', 'new-content\n'))])
1997
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1998
merge_obj = self.make_merge_obj(builder, 'E-id',
1999
interesting_files=['c'])
2000
entries = list(merge_obj._entries_lca())
2001
root_id = 'a-root-id'
2002
self.assertEqual([('c-id', True,
2003
((root_id, [root_id, root_id]), root_id, root_id),
2004
((u'c', [u'b', u'b']), u'b', u'b'),
2005
((False, [False, False]), False, False)),
2008
def test_interesting_file_in_lca(self):
2009
# This renamed the file, but it should still match the entry in LCA
2010
builder = self.get_builder()
2011
builder.build_snapshot('A-id', None,
2012
[('add', (u'', 'a-root-id', 'directory', None)),
2013
('add', (u'a', 'a-id', 'file', 'content\n')),
2014
('add', (u'b', 'b-id', 'file', 'content\n'))])
2015
builder.build_snapshot('B-id', ['A-id'],
2016
[('rename', ('b', 'c'))])
2017
builder.build_snapshot('C-id', ['A-id'], [])
2018
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2019
[('modify', ('a-id', 'new-content\n')),
2020
('modify', ('b-id', 'new-content\n'))])
2021
builder.build_snapshot('D-id', ['B-id', 'C-id'],
2022
[('rename', ('c', 'b'))])
2023
merge_obj = self.make_merge_obj(builder, 'E-id',
2024
interesting_files=['c'])
2025
entries = list(merge_obj._entries_lca())
2026
root_id = 'a-root-id'
2027
self.assertEqual([('b-id', True,
2028
((root_id, [root_id, root_id]), root_id, root_id),
2029
((u'b', [u'c', u'b']), u'b', u'b'),
2030
((False, [False, False]), False, False)),
2033
def test_interesting_ids(self):
2034
# Two files modified, but we should filter one of them
2035
builder = self.get_builder()
2036
builder.build_snapshot('A-id', None,
2037
[('add', (u'', 'a-root-id', 'directory', None)),
2038
('add', (u'a', 'a-id', 'file', 'content\n')),
2039
('add', (u'b', 'b-id', 'file', 'content\n'))])
2040
builder.build_snapshot('B-id', ['A-id'], [])
2041
builder.build_snapshot('C-id', ['A-id'], [])
2042
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2043
[('modify', ('a-id', 'new-content\n')),
2044
('modify', ('b-id', 'new-content\n'))])
2045
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2046
merge_obj = self.make_merge_obj(builder, 'E-id',
2047
interesting_ids=['b-id'])
2048
entries = list(merge_obj._entries_lca())
2049
root_id = 'a-root-id'
2050
self.assertEqual([('b-id', True,
2051
((root_id, [root_id, root_id]), root_id, root_id),
2052
((u'b', [u'b', u'b']), u'b', u'b'),
2053
((False, [False, False]), False, False)),
2058
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
2060
def get_builder(self):
2061
builder = self.make_branch_builder('path')
2062
builder.start_series()
2063
self.addCleanup(builder.finish_series)
2066
def get_wt_from_builder(self, builder):
2067
"""Get a real WorkingTree from the builder."""
2068
the_branch = builder.get_branch()
2069
wt = the_branch.bzrdir.create_workingtree()
2070
# Note: This is a little bit ugly, but we are holding the branch
2071
# write-locked as part of the build process, and we would like to
2072
# maintain that. So we just force the WT to re-use the same
2074
wt._branch = the_branch
2076
self.addCleanup(wt.unlock)
2079
def do_merge(self, builder, other_revision_id):
2080
wt = self.get_wt_from_builder(builder)
2081
merger = _mod_merge.Merger.from_revision_ids(None,
2082
wt, other_revision_id)
2083
merger.merge_type = _mod_merge.Merge3Merger
2084
return wt, merger.do_merge()
2086
def test_simple_lca(self):
2087
builder = self.get_builder()
2088
builder.build_snapshot('A-id', None,
2089
[('add', (u'', 'a-root-id', 'directory', None)),
2090
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
2091
builder.build_snapshot('C-id', ['A-id'], [])
2092
builder.build_snapshot('B-id', ['A-id'], [])
2093
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2094
builder.build_snapshot('D-id', ['B-id', 'C-id'],
2095
[('modify', ('a-id', 'a\nb\nc\nd\ne\nf\n'))])
2096
wt, conflicts = self.do_merge(builder, 'E-id')
2097
self.assertEqual(0, conflicts)
2098
# The merge should have simply update the contents of 'a'
2099
self.assertEqual('a\nb\nc\nd\ne\nf\n', wt.get_file_text('a-id'))
2101
def test_conflict_without_lca(self):
2102
# This test would cause a merge conflict, unless we use the lca trees
2103
# to determine the real ancestry
2106
# B C Path renamed to 'bar' in B
2110
# D E Path at 'bar' in D and E
2112
# F Path at 'baz' in F, which supersedes 'bar' and 'foo'
2113
builder = self.get_builder()
2114
builder.build_snapshot('A-id', None,
2115
[('add', (u'', 'a-root-id', 'directory', None)),
2116
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2117
builder.build_snapshot('C-id', ['A-id'], [])
2118
builder.build_snapshot('B-id', ['A-id'],
2119
[('rename', ('foo', 'bar'))])
2120
builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
2121
[('rename', ('foo', 'bar'))])
2122
builder.build_snapshot('F-id', ['E-id'],
2123
[('rename', ('bar', 'baz'))])
2124
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2125
wt, conflicts = self.do_merge(builder, 'F-id')
2126
self.assertEqual(0, conflicts)
2127
# The merge should simply recognize that the final rename takes
2129
self.assertEqual('baz', wt.id2path('foo-id'))
2131
def test_other_deletes_lca_renames(self):
2132
# This test would cause a merge conflict, unless we use the lca trees
2133
# to determine the real ancestry
2136
# B C Path renamed to 'bar' in B
2140
# D E Path at 'bar' in D and E
2143
builder = self.get_builder()
2144
builder.build_snapshot('A-id', None,
2145
[('add', (u'', 'a-root-id', 'directory', None)),
2146
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2147
builder.build_snapshot('C-id', ['A-id'], [])
2148
builder.build_snapshot('B-id', ['A-id'],
2149
[('rename', ('foo', 'bar'))])
2150
builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
2151
[('rename', ('foo', 'bar'))])
2152
builder.build_snapshot('F-id', ['E-id'],
2153
[('unversion', 'foo-id')])
2154
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2155
wt, conflicts = self.do_merge(builder, 'F-id')
2156
self.assertEqual(0, conflicts)
2157
self.assertRaises(errors.NoSuchId, wt.id2path, 'foo-id')
2159
def test_executable_changes(self):
2168
# F Executable bit changed
2169
builder = self.get_builder()
2170
builder.build_snapshot('A-id', None,
2171
[('add', (u'', 'a-root-id', 'directory', None)),
2172
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2173
builder.build_snapshot('C-id', ['A-id'], [])
2174
builder.build_snapshot('B-id', ['A-id'], [])
2175
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2176
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2177
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2178
wt = self.get_wt_from_builder(builder)
2179
tt = transform.TreeTransform(wt)
2181
tt.set_executability(True, tt.trans_id_tree_file_id('foo-id'))
2186
self.assertTrue(wt.is_executable('foo-id'))
2187
wt.commit('F-id', rev_id='F-id')
2188
# Reset to D, so that we can merge F
2189
wt.set_parent_ids(['D-id'])
2190
wt.branch.set_last_revision_info(3, 'D-id')
2192
self.assertFalse(wt.is_executable('foo-id'))
2193
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2194
self.assertEqual(0, conflicts)
2195
self.assertTrue(wt.is_executable('foo-id'))
2197
def test_create_symlink(self):
2198
self.requireFeature(tests.SymlinkFeature)
2207
# F Add a symlink 'foo' => 'bar'
2208
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2209
# have symlink support
2210
builder = self.get_builder()
2211
builder.build_snapshot('A-id', None,
2212
[('add', (u'', 'a-root-id', 'directory', None))])
2213
builder.build_snapshot('C-id', ['A-id'], [])
2214
builder.build_snapshot('B-id', ['A-id'], [])
2215
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2216
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2217
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2218
wt = self.get_wt_from_builder(builder)
2219
os.symlink('bar', 'path/foo')
2220
wt.add(['foo'], ['foo-id'])
2221
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2222
wt.commit('add symlink', rev_id='F-id')
2223
# Reset to D, so that we can merge F
2224
wt.set_parent_ids(['D-id'])
2225
wt.branch.set_last_revision_info(3, 'D-id')
2227
self.assertIs(None, wt.path2id('foo'))
2228
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2229
self.assertEqual(0, conflicts)
2230
self.assertEqual('foo-id', wt.path2id('foo'))
2231
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2233
def test_both_sides_revert(self):
2234
# Both sides of a criss-cross revert the text to the lca
2235
# A base, introduces 'foo'
2237
# B C B modifies 'foo', C modifies 'foo'
2239
# D E D reverts to B, E reverts to C
2240
# This should conflict
2241
# This must be done with a real WorkingTree, because normally their
2242
# inventory contains "None" rather than a real sha1
2243
builder = self.get_builder()
2244
builder.build_snapshot('A-id', None,
2245
[('add', (u'', 'a-root-id', 'directory', None)),
2246
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
2247
builder.build_snapshot('B-id', ['A-id'],
2248
[('modify', ('foo-id', 'B content\n'))])
2249
builder.build_snapshot('C-id', ['A-id'],
2250
[('modify', ('foo-id', 'C content\n'))])
2251
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2252
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2253
wt, conflicts = self.do_merge(builder, 'E-id')
2254
self.assertEqual(1, conflicts)
2255
self.assertEqualDiff('<<<<<<< TREE\n'
2259
'>>>>>>> MERGE-SOURCE\n',
2260
wt.get_file_text('foo-id'))
2262
def test_modified_symlink(self):
2263
self.requireFeature(tests.SymlinkFeature)
2264
# A Create symlink foo => bar
2266
# B C B relinks foo => baz
2270
# D E D & E have foo => baz
2272
# F F changes it to bing
2274
# Merging D & F should result in F cleanly overriding D, because D's
2275
# value actually comes from B
2277
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2278
# have symlink support
2279
wt = self.make_branch_and_tree('path')
2281
self.addCleanup(wt.unlock)
2282
os.symlink('bar', 'path/foo')
2283
wt.add(['foo'], ['foo-id'])
2284
wt.commit('add symlink', rev_id='A-id')
2285
os.remove('path/foo')
2286
os.symlink('baz', 'path/foo')
2287
wt.commit('foo => baz', rev_id='B-id')
2288
wt.set_last_revision('A-id')
2289
wt.branch.set_last_revision_info(1, 'A-id')
2291
wt.commit('C', rev_id='C-id')
2292
wt.merge_from_branch(wt.branch, 'B-id')
2293
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2294
wt.commit('E merges C & B', rev_id='E-id')
2295
os.remove('path/foo')
2296
os.symlink('bing', 'path/foo')
2297
wt.commit('F foo => bing', rev_id='F-id')
2298
wt.set_last_revision('B-id')
2299
wt.branch.set_last_revision_info(2, 'B-id')
2301
wt.merge_from_branch(wt.branch, 'C-id')
2302
wt.commit('D merges B & C', rev_id='D-id')
2303
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2304
self.assertEqual(0, conflicts)
2305
self.assertEqual('bing', wt.get_symlink_target('foo-id'))
2307
def test_renamed_symlink(self):
2308
self.requireFeature(tests.SymlinkFeature)
2309
# A Create symlink foo => bar
2311
# B C B renames foo => barry
2315
# D E D & E have barry
2317
# F F renames barry to blah
2319
# Merging D & F should result in F cleanly overriding D, because D's
2320
# value actually comes from B
2322
wt = self.make_branch_and_tree('path')
2324
self.addCleanup(wt.unlock)
2325
os.symlink('bar', 'path/foo')
2326
wt.add(['foo'], ['foo-id'])
2327
wt.commit('A add symlink', rev_id='A-id')
2328
wt.rename_one('foo', 'barry')
2329
wt.commit('B foo => barry', rev_id='B-id')
2330
wt.set_last_revision('A-id')
2331
wt.branch.set_last_revision_info(1, 'A-id')
2333
wt.commit('C', rev_id='C-id')
2334
wt.merge_from_branch(wt.branch, 'B-id')
2335
self.assertEqual('barry', wt.id2path('foo-id'))
2336
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2337
wt.commit('E merges C & B', rev_id='E-id')
2338
wt.rename_one('barry', 'blah')
2339
wt.commit('F barry => blah', rev_id='F-id')
2340
wt.set_last_revision('B-id')
2341
wt.branch.set_last_revision_info(2, 'B-id')
2343
wt.merge_from_branch(wt.branch, 'C-id')
2344
wt.commit('D merges B & C', rev_id='D-id')
2345
self.assertEqual('barry', wt.id2path('foo-id'))
2346
# Check the output of the Merger object directly
2347
merger = _mod_merge.Merger.from_revision_ids(None,
2349
merger.merge_type = _mod_merge.Merge3Merger
2350
merge_obj = merger.make_merger()
2351
root_id = wt.path2id('')
2352
entries = list(merge_obj._entries_lca())
2353
# No content change, just a path change
2354
self.assertEqual([('foo-id', False,
2355
((root_id, [root_id, root_id]), root_id, root_id),
2356
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2357
((False, [False, False]), False, False)),
2359
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2360
self.assertEqual(0, conflicts)
2361
self.assertEqual('blah', wt.id2path('foo-id'))
2363
def test_symlink_no_content_change(self):
2364
self.requireFeature(tests.SymlinkFeature)
2365
# A Create symlink foo => bar
2367
# B C B relinks foo => baz
2371
# D E D & E have foo => baz
2373
# F F has foo => bing
2375
# Merging E into F should not cause a conflict, because E doesn't have
2376
# a content change relative to the LCAs (it does relative to A)
2377
wt = self.make_branch_and_tree('path')
2379
self.addCleanup(wt.unlock)
2380
os.symlink('bar', 'path/foo')
2381
wt.add(['foo'], ['foo-id'])
2382
wt.commit('add symlink', rev_id='A-id')
2383
os.remove('path/foo')
2384
os.symlink('baz', 'path/foo')
2385
wt.commit('foo => baz', rev_id='B-id')
2386
wt.set_last_revision('A-id')
2387
wt.branch.set_last_revision_info(1, 'A-id')
2389
wt.commit('C', rev_id='C-id')
2390
wt.merge_from_branch(wt.branch, 'B-id')
2391
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2392
wt.commit('E merges C & B', rev_id='E-id')
2393
wt.set_last_revision('B-id')
2394
wt.branch.set_last_revision_info(2, 'B-id')
2396
wt.merge_from_branch(wt.branch, 'C-id')
2397
wt.commit('D merges B & C', rev_id='D-id')
2398
os.remove('path/foo')
2399
os.symlink('bing', 'path/foo')
2400
wt.commit('F foo => bing', rev_id='F-id')
2402
# Check the output of the Merger object directly
2403
merger = _mod_merge.Merger.from_revision_ids(None,
2405
merger.merge_type = _mod_merge.Merge3Merger
2406
merge_obj = merger.make_merger()
2407
# Nothing interesting happened in OTHER relative to BASE
2408
self.assertEqual([], list(merge_obj._entries_lca()))
2409
# Now do a real merge, just to test the rest of the stack
2410
conflicts = wt.merge_from_branch(wt.branch, to_revision='E-id')
2411
self.assertEqual(0, conflicts)
2412
self.assertEqual('bing', wt.get_symlink_target('foo-id'))
2414
def test_symlink_this_changed_kind(self):
2415
self.requireFeature(tests.SymlinkFeature)
2418
# B C B creates symlink foo => bar
2422
# D E D changes foo into a file, E has foo => bing
2424
# Mostly, this is trying to test that we don't try to os.readlink() on
2425
# a file, or when there is nothing there
2426
wt = self.make_branch_and_tree('path')
2428
self.addCleanup(wt.unlock)
2429
wt.commit('base', rev_id='A-id')
2430
os.symlink('bar', 'path/foo')
2431
wt.add(['foo'], ['foo-id'])
2432
wt.commit('add symlink foo => bar', rev_id='B-id')
2433
wt.set_last_revision('A-id')
2434
wt.branch.set_last_revision_info(1, 'A-id')
2436
wt.commit('C', rev_id='C-id')
2437
wt.merge_from_branch(wt.branch, 'B-id')
2438
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2439
os.remove('path/foo')
2440
# We have to change the link in E, or it won't try to do a comparison
2441
os.symlink('bing', 'path/foo')
2442
wt.commit('E merges C & B, overrides to bing', rev_id='E-id')
2443
wt.set_last_revision('B-id')
2444
wt.branch.set_last_revision_info(2, 'B-id')
2446
wt.merge_from_branch(wt.branch, 'C-id')
2447
os.remove('path/foo')
2448
self.build_tree_contents([('path/foo', 'file content\n')])
2449
# XXX: workaround, WT doesn't detect kind changes unless you do
2451
list(wt.iter_changes(wt.basis_tree()))
2452
wt.commit('D merges B & C, makes it a file', rev_id='D-id')
2454
merger = _mod_merge.Merger.from_revision_ids(None,
2456
merger.merge_type = _mod_merge.Merge3Merger
2457
merge_obj = merger.make_merger()
2458
entries = list(merge_obj._entries_lca())
2459
root_id = wt.path2id('')
2460
self.assertEqual([('foo-id', True,
2461
((None, [root_id, None]), root_id, root_id),
2462
((None, [u'foo', None]), u'foo', u'foo'),
2463
((None, [False, None]), False, False)),
2466
def test_symlink_all_wt(self):
2467
"""Check behavior if all trees are Working Trees."""
2468
self.requireFeature(tests.SymlinkFeature)
2469
# The big issue is that entry.symlink_target is None for WorkingTrees.
2470
# So we need to make sure we handle that case correctly.
2473
# B C B relinks foo => baz
2475
# D E D & E have foo => baz
2477
# F F changes it to bing
2478
# Merging D & F should result in F cleanly overriding D, because D's
2479
# value actually comes from B
2481
wt = self.make_branch_and_tree('path')
2483
self.addCleanup(wt.unlock)
2484
os.symlink('bar', 'path/foo')
2485
wt.add(['foo'], ['foo-id'])
2486
wt.commit('add symlink', rev_id='A-id')
2487
os.remove('path/foo')
2488
os.symlink('baz', 'path/foo')
2489
wt.commit('foo => baz', rev_id='B-id')
2490
wt.set_last_revision('A-id')
2491
wt.branch.set_last_revision_info(1, 'A-id')
2493
wt.commit('C', rev_id='C-id')
2494
wt.merge_from_branch(wt.branch, 'B-id')
2495
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2496
wt.commit('E merges C & B', rev_id='E-id')
2497
os.remove('path/foo')
2498
os.symlink('bing', 'path/foo')
2499
wt.commit('F foo => bing', rev_id='F-id')
2500
wt.set_last_revision('B-id')
2501
wt.branch.set_last_revision_info(2, 'B-id')
2503
wt.merge_from_branch(wt.branch, 'C-id')
2504
wt.commit('D merges B & C', rev_id='D-id')
2505
wt_base = wt.bzrdir.sprout('base', 'A-id').open_workingtree()
2507
self.addCleanup(wt_base.unlock)
2508
wt_lca1 = wt.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
2510
self.addCleanup(wt_lca1.unlock)
2511
wt_lca2 = wt.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
2513
self.addCleanup(wt_lca2.unlock)
2514
wt_other = wt.bzrdir.sprout('other', 'F-id').open_workingtree()
2515
wt_other.lock_read()
2516
self.addCleanup(wt_other.unlock)
2517
merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
2518
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2519
entries = list(merge_obj._entries_lca())
2520
root_id = wt.path2id('')
2521
self.assertEqual([('foo-id', True,
2522
((root_id, [root_id, root_id]), root_id, root_id),
2523
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2524
((False, [False, False]), False, False)),
2527
def test_other_reverted_path_to_base(self):
2530
# B C Path at 'bar' in B
2537
builder = self.get_builder()
2538
builder.build_snapshot('A-id', None,
2539
[('add', (u'', 'a-root-id', 'directory', None)),
2540
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2541
builder.build_snapshot('C-id', ['A-id'], [])
2542
builder.build_snapshot('B-id', ['A-id'],
2543
[('rename', ('foo', 'bar'))])
2544
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2545
[('rename', ('foo', 'bar'))]) # merge the rename
2546
builder.build_snapshot('F-id', ['E-id'],
2547
[('rename', ('bar', 'foo'))]) # Rename back to BASE
2548
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2549
wt, conflicts = self.do_merge(builder, 'F-id')
2550
self.assertEqual(0, conflicts)
2551
self.assertEqual('foo', wt.id2path('foo-id'))
2553
def test_other_reverted_content_to_base(self):
2554
builder = self.get_builder()
2555
builder.build_snapshot('A-id', None,
2556
[('add', (u'', 'a-root-id', 'directory', None)),
2557
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2558
builder.build_snapshot('C-id', ['A-id'], [])
2559
builder.build_snapshot('B-id', ['A-id'],
2560
[('modify', ('foo-id', 'B content\n'))])
2561
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2562
[('modify', ('foo-id', 'B content\n'))]) # merge the content
2563
builder.build_snapshot('F-id', ['E-id'],
2564
[('modify', ('foo-id', 'base content\n'))]) # Revert back to BASE
2565
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2566
wt, conflicts = self.do_merge(builder, 'F-id')
2567
self.assertEqual(0, conflicts)
2568
# TODO: We need to use the per-file graph to properly select a BASE
2569
# before this will work. Or at least use the LCA trees to find
2570
# the appropriate content base. (which is B, not A).
2571
self.assertEqual('base content\n', wt.get_file_text('foo-id'))
2573
def test_other_modified_content(self):
2574
builder = self.get_builder()
2575
builder.build_snapshot('A-id', None,
2576
[('add', (u'', 'a-root-id', 'directory', None)),
2577
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2578
builder.build_snapshot('C-id', ['A-id'], [])
2579
builder.build_snapshot('B-id', ['A-id'],
2580
[('modify', ('foo-id', 'B content\n'))])
2581
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2582
[('modify', ('foo-id', 'B content\n'))]) # merge the content
2583
builder.build_snapshot('F-id', ['E-id'],
2584
[('modify', ('foo-id', 'F content\n'))]) # Override B content
2585
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2586
wt, conflicts = self.do_merge(builder, 'F-id')
2587
self.assertEqual(0, conflicts)
2588
self.assertEqual('F content\n', wt.get_file_text('foo-id'))
2590
def test_all_wt(self):
2591
"""Check behavior if all trees are Working Trees."""
2592
# The big issue is that entry.revision is None for WorkingTrees. (as is
2593
# entry.text_sha1, etc. So we need to make sure we handle that case
2595
# A Content of 'foo', path of 'a'
2597
# B C B modifies content, C renames 'a' => 'b'
2599
# D E E updates content, renames 'b' => 'c'
2600
builder = self.get_builder()
2601
builder.build_snapshot('A-id', None,
2602
[('add', (u'', 'a-root-id', 'directory', None)),
2603
('add', (u'a', 'a-id', 'file', 'base content\n')),
2604
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2605
builder.build_snapshot('B-id', ['A-id'],
2606
[('modify', ('foo-id', 'B content\n'))])
2607
builder.build_snapshot('C-id', ['A-id'],
2608
[('rename', ('a', 'b'))])
2609
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2610
[('rename', ('b', 'c')),
2611
('modify', ('foo-id', 'E content\n'))])
2612
builder.build_snapshot('D-id', ['B-id', 'C-id'],
2613
[('rename', ('a', 'b'))]) # merged change
2614
wt_this = self.get_wt_from_builder(builder)
2615
wt_base = wt_this.bzrdir.sprout('base', 'A-id').open_workingtree()
2617
self.addCleanup(wt_base.unlock)
2618
wt_lca1 = wt_this.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
2620
self.addCleanup(wt_lca1.unlock)
2621
wt_lca2 = wt_this.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
2623
self.addCleanup(wt_lca2.unlock)
2624
wt_other = wt_this.bzrdir.sprout('other', 'E-id').open_workingtree()
2625
wt_other.lock_read()
2626
self.addCleanup(wt_other.unlock)
2627
merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
2628
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2629
entries = list(merge_obj._entries_lca())
2630
root_id = 'a-root-id'
2631
self.assertEqual([('a-id', False,
2632
((root_id, [root_id, root_id]), root_id, root_id),
2633
((u'a', [u'a', u'b']), u'c', u'b'),
2634
((False, [False, False]), False, False)),
2636
((root_id, [root_id, root_id]), root_id, root_id),
2637
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2638
((False, [False, False]), False, False)),
2641
def test_nested_tree_unmodified(self):
2642
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2644
wt = self.make_branch_and_tree('tree',
2645
format='dirstate-with-subtree')
2647
self.addCleanup(wt.unlock)
2648
sub_tree = self.make_branch_and_tree('tree/sub-tree',
2649
format='dirstate-with-subtree')
2650
wt.set_root_id('a-root-id')
2651
sub_tree.set_root_id('sub-tree-root')
2652
self.build_tree_contents([('tree/sub-tree/file', 'text1')])
2653
sub_tree.add('file')
2654
sub_tree.commit('foo', rev_id='sub-A-id')
2655
wt.add_reference(sub_tree)
2656
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2657
# Now create a criss-cross merge in the parent, without modifying the
2659
wt.commit('B', rev_id='B-id', recursive=None)
2660
wt.set_last_revision('A-id')
2661
wt.branch.set_last_revision_info(1, 'A-id')
2662
wt.commit('C', rev_id='C-id', recursive=None)
2663
wt.merge_from_branch(wt.branch, to_revision='B-id')
2664
wt.commit('E', rev_id='E-id', recursive=None)
2665
wt.set_parent_ids(['B-id', 'C-id'])
2666
wt.branch.set_last_revision_info(2, 'B-id')
2667
wt.commit('D', rev_id='D-id', recursive=None)
2669
merger = _mod_merge.Merger.from_revision_ids(None,
2671
merger.merge_type = _mod_merge.Merge3Merger
2672
merge_obj = merger.make_merger()
2673
entries = list(merge_obj._entries_lca())
2674
self.assertEqual([], entries)
2676
def test_nested_tree_subtree_modified(self):
2677
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2679
wt = self.make_branch_and_tree('tree',
2680
format='dirstate-with-subtree')
2682
self.addCleanup(wt.unlock)
2683
sub_tree = self.make_branch_and_tree('tree/sub',
2684
format='dirstate-with-subtree')
2685
wt.set_root_id('a-root-id')
2686
sub_tree.set_root_id('sub-tree-root')
2687
self.build_tree_contents([('tree/sub/file', 'text1')])
2688
sub_tree.add('file')
2689
sub_tree.commit('foo', rev_id='sub-A-id')
2690
wt.add_reference(sub_tree)
2691
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2692
# Now create a criss-cross merge in the parent, without modifying the
2694
wt.commit('B', rev_id='B-id', recursive=None)
2695
wt.set_last_revision('A-id')
2696
wt.branch.set_last_revision_info(1, 'A-id')
2697
wt.commit('C', rev_id='C-id', recursive=None)
2698
wt.merge_from_branch(wt.branch, to_revision='B-id')
2699
self.build_tree_contents([('tree/sub/file', 'text2')])
2700
sub_tree.commit('modify contents', rev_id='sub-B-id')
2701
wt.commit('E', rev_id='E-id', recursive=None)
2702
wt.set_parent_ids(['B-id', 'C-id'])
2703
wt.branch.set_last_revision_info(2, 'B-id')
2704
wt.commit('D', rev_id='D-id', recursive=None)
2706
merger = _mod_merge.Merger.from_revision_ids(None,
2708
merger.merge_type = _mod_merge.Merge3Merger
2709
merge_obj = merger.make_merger()
2710
entries = list(merge_obj._entries_lca())
2711
# Nothing interesting about this sub-tree, because content changes are
2712
# computed at a higher level
2713
self.assertEqual([], entries)
2715
def test_nested_tree_subtree_renamed(self):
2716
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2718
wt = self.make_branch_and_tree('tree',
2719
format='dirstate-with-subtree')
2721
self.addCleanup(wt.unlock)
2722
sub_tree = self.make_branch_and_tree('tree/sub',
2723
format='dirstate-with-subtree')
2724
wt.set_root_id('a-root-id')
2725
sub_tree.set_root_id('sub-tree-root')
2726
self.build_tree_contents([('tree/sub/file', 'text1')])
2727
sub_tree.add('file')
2728
sub_tree.commit('foo', rev_id='sub-A-id')
2729
wt.add_reference(sub_tree)
2730
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2731
# Now create a criss-cross merge in the parent, without modifying the
2733
wt.commit('B', rev_id='B-id', recursive=None)
2734
wt.set_last_revision('A-id')
2735
wt.branch.set_last_revision_info(1, 'A-id')
2736
wt.commit('C', rev_id='C-id', recursive=None)
2737
wt.merge_from_branch(wt.branch, to_revision='B-id')
2738
wt.rename_one('sub', 'alt_sub')
2739
wt.commit('E', rev_id='E-id', recursive=None)
2740
wt.set_last_revision('B-id')
2742
wt.set_parent_ids(['B-id', 'C-id'])
2743
wt.branch.set_last_revision_info(2, 'B-id')
2744
wt.commit('D', rev_id='D-id', recursive=None)
2746
merger = _mod_merge.Merger.from_revision_ids(None,
2748
merger.merge_type = _mod_merge.Merge3Merger
2749
merge_obj = merger.make_merger()
2750
entries = list(merge_obj._entries_lca())
2751
root_id = 'a-root-id'
2752
self.assertEqual([('sub-tree-root', False,
2753
((root_id, [root_id, root_id]), root_id, root_id),
2754
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2755
((False, [False, False]), False, False)),
2758
def test_nested_tree_subtree_renamed_and_modified(self):
2759
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2761
wt = self.make_branch_and_tree('tree',
2762
format='dirstate-with-subtree')
2764
self.addCleanup(wt.unlock)
2765
sub_tree = self.make_branch_and_tree('tree/sub',
2766
format='dirstate-with-subtree')
2767
wt.set_root_id('a-root-id')
2768
sub_tree.set_root_id('sub-tree-root')
2769
self.build_tree_contents([('tree/sub/file', 'text1')])
2770
sub_tree.add('file')
2771
sub_tree.commit('foo', rev_id='sub-A-id')
2772
wt.add_reference(sub_tree)
2773
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2774
# Now create a criss-cross merge in the parent, without modifying the
2776
wt.commit('B', rev_id='B-id', recursive=None)
2777
wt.set_last_revision('A-id')
2778
wt.branch.set_last_revision_info(1, 'A-id')
2779
wt.commit('C', rev_id='C-id', recursive=None)
2780
wt.merge_from_branch(wt.branch, to_revision='B-id')
2781
self.build_tree_contents([('tree/sub/file', 'text2')])
2782
sub_tree.commit('modify contents', rev_id='sub-B-id')
2783
wt.rename_one('sub', 'alt_sub')
2784
wt.commit('E', rev_id='E-id', recursive=None)
2785
wt.set_last_revision('B-id')
2787
wt.set_parent_ids(['B-id', 'C-id'])
2788
wt.branch.set_last_revision_info(2, 'B-id')
2789
wt.commit('D', rev_id='D-id', recursive=None)
2791
merger = _mod_merge.Merger.from_revision_ids(None,
2793
merger.merge_type = _mod_merge.Merge3Merger
2794
merge_obj = merger.make_merger()
2795
entries = list(merge_obj._entries_lca())
2796
root_id = 'a-root-id'
2797
self.assertEqual([('sub-tree-root', False,
2798
((root_id, [root_id, root_id]), root_id, root_id),
2799
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2800
((False, [False, False]), False, False)),
2804
class TestLCAMultiWay(tests.TestCase):
2806
def assertLCAMultiWay(self, expected, base, lcas, other, this,
2807
allow_overriding_lca=True):
2808
self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
2809
(base, lcas), other, this,
2810
allow_overriding_lca=allow_overriding_lca))
2812
def test_other_equal_equal_lcas(self):
2813
"""Test when OTHER=LCA and all LCAs are identical."""
2814
self.assertLCAMultiWay('this',
2815
'bval', ['bval', 'bval'], 'bval', 'bval')
2816
self.assertLCAMultiWay('this',
2817
'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
2818
self.assertLCAMultiWay('this',
2819
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
2820
self.assertLCAMultiWay('this',
2821
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
2822
self.assertLCAMultiWay('this',
2823
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
2825
def test_other_equal_this(self):
2826
"""Test when other and this are identical."""
2827
self.assertLCAMultiWay('this',
2828
'bval', ['bval', 'bval'], 'oval', 'oval')
2829
self.assertLCAMultiWay('this',
2830
'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
2831
self.assertLCAMultiWay('this',
2832
'bval', ['cval', 'dval'], 'oval', 'oval')
2833
self.assertLCAMultiWay('this',
2834
'bval', [None, 'lcaval'], 'oval', 'oval')
2835
self.assertLCAMultiWay('this',
2836
None, [None, 'lcaval'], 'oval', 'oval')
2837
self.assertLCAMultiWay('this',
2838
None, ['lcaval', 'lcaval'], 'oval', 'oval')
2839
self.assertLCAMultiWay('this',
2840
None, ['cval', 'dval'], 'oval', 'oval')
2841
self.assertLCAMultiWay('this',
2842
None, ['cval', 'dval'], None, None)
2843
self.assertLCAMultiWay('this',
2844
None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
2846
def test_no_lcas(self):
2847
self.assertLCAMultiWay('this',
2848
'bval', [], 'bval', 'tval')
2849
self.assertLCAMultiWay('other',
2850
'bval', [], 'oval', 'bval')
2851
self.assertLCAMultiWay('conflict',
2852
'bval', [], 'oval', 'tval')
2853
self.assertLCAMultiWay('this',
2854
'bval', [], 'oval', 'oval')
2856
def test_lca_supersedes_other_lca(self):
2857
"""If one lca == base, the other lca takes precedence"""
2858
self.assertLCAMultiWay('this',
2859
'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
2860
self.assertLCAMultiWay('this',
2861
'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
2862
# This is actually considered a 'revert' because the 'lcaval' in LCAS
2863
# supersedes the BASE val (in the other LCA) but then OTHER reverts it
2865
self.assertLCAMultiWay('other',
2866
'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
2867
self.assertLCAMultiWay('conflict',
2868
'bval', ['bval', 'lcaval'], 'bval', 'tval')
2870
def test_other_and_this_pick_different_lca(self):
2871
# OTHER and THIS resolve the lca conflict in different ways
2872
self.assertLCAMultiWay('conflict',
2873
'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
2874
self.assertLCAMultiWay('conflict',
2875
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
2876
self.assertLCAMultiWay('conflict',
2877
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
2879
def test_other_in_lca(self):
2880
# OTHER takes a value of one of the LCAs, THIS takes a new value, which
2881
# theoretically supersedes both LCA values and 'wins'
2882
self.assertLCAMultiWay('this',
2883
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
2884
self.assertLCAMultiWay('this',
2885
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval')
2886
self.assertLCAMultiWay('conflict',
2887
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval',
2888
allow_overriding_lca=False)
2889
self.assertLCAMultiWay('conflict',
2890
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval',
2891
allow_overriding_lca=False)
2892
# THIS reverted back to BASE, but that is an explicit supersede of all
2894
self.assertLCAMultiWay('this',
2895
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval')
2896
self.assertLCAMultiWay('this',
2897
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
2898
self.assertLCAMultiWay('conflict',
2899
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval',
2900
allow_overriding_lca=False)
2901
self.assertLCAMultiWay('conflict',
2902
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval',
2903
allow_overriding_lca=False)
2905
def test_this_in_lca(self):
2906
# THIS takes a value of one of the LCAs, OTHER takes a new value, which
2907
# theoretically supersedes both LCA values and 'wins'
2908
self.assertLCAMultiWay('other',
2909
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
2910
self.assertLCAMultiWay('other',
2911
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
2912
self.assertLCAMultiWay('conflict',
2913
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val',
2914
allow_overriding_lca=False)
2915
self.assertLCAMultiWay('conflict',
2916
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val',
2917
allow_overriding_lca=False)
2918
# OTHER reverted back to BASE, but that is an explicit supersede of all
2920
self.assertLCAMultiWay('other',
2921
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val')
2922
self.assertLCAMultiWay('conflict',
2923
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val',
2924
allow_overriding_lca=False)
2926
def test_all_differ(self):
2927
self.assertLCAMultiWay('conflict',
2928
'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
2929
self.assertLCAMultiWay('conflict',
2930
'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval', 'tval')
2931
self.assertLCAMultiWay('conflict',
2932
'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval', 'tval')
2935
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
2938
super(TestConfigurableFileMerger, self).setUp()
2941
def get_merger_factory(self):
2942
# Allows the inner methods to access the test attributes
2945
class FooMerger(_mod_merge.ConfigurableFileMerger):
2947
default_files = ['bar']
2949
def merge_text(self, params):
2950
calls.append('merge_text')
2951
return ('not_applicable', None)
2953
def factory(merger):
2954
result = FooMerger(merger)
2955
# Make sure we start with a clean slate
2956
self.assertEqual(None, result.affected_files)
2957
# Track the original merger
2958
self.merger = result
2963
def _install_hook(self, factory):
2964
_mod_merge.Merger.hooks.install_named_hook('merge_file_content',
2965
factory, 'test factory')
2967
def make_builder(self):
2968
builder = test_merge_core.MergeBuilder(self.test_base_dir)
2969
self.addCleanup(builder.cleanup)
2972
def make_text_conflict(self, file_name='bar'):
2973
factory = self.get_merger_factory()
2974
self._install_hook(factory)
2975
builder = self.make_builder()
2976
builder.add_file('bar-id', builder.tree_root, file_name, 'text1', True)
2977
builder.change_contents('bar-id', other='text4', this='text3')
2980
def make_kind_change(self):
2981
factory = self.get_merger_factory()
2982
self._install_hook(factory)
2983
builder = self.make_builder()
2984
builder.add_file('bar-id', builder.tree_root, 'bar', 'text1', True,
2986
builder.add_dir('bar-dir', builder.tree_root, 'bar-id',
2987
base=False, other=False)
2990
def test_uses_this_branch(self):
2991
builder = self.make_text_conflict()
2992
tt = builder.make_preview_transform()
2993
self.addCleanup(tt.finalize)
2995
def test_affected_files_cached(self):
2996
"""Ensures that the config variable is cached"""
2997
builder = self.make_text_conflict()
2998
conflicts = builder.merge()
2999
# The hook should set the variable
3000
self.assertEqual(['bar'], self.merger.affected_files)
3001
self.assertEqual(1, len(conflicts))
3003
def test_hook_called_for_text_conflicts(self):
3004
builder = self.make_text_conflict()
3005
conflicts = builder.merge()
3006
# The hook should call the merge_text() method
3007
self.assertEqual(['merge_text'], self.calls)
3009
def test_hook_not_called_for_kind_change(self):
3010
builder = self.make_kind_change()
3011
conflicts = builder.merge()
3012
# The hook should not call the merge_text() method
3013
self.assertEqual([], self.calls)
3015
def test_hook_not_called_for_other_files(self):
3016
builder = self.make_text_conflict('foobar')
3017
conflicts = builder.merge()
3018
# The hook should not call the merge_text() method
3019
self.assertEqual([], self.calls)
3022
class TestMergeIntoBase(tests.TestCaseWithTransport):
3024
def setup_simple_branch(self, relpath, shape=None, root_id=None):
3025
"""One commit, containing tree specified by optional shape.
3027
Default is empty tree (just root entry).
3030
root_id = '%s-root-id' % (relpath,)
3031
wt = self.make_branch_and_tree(relpath)
3032
wt.set_root_id(root_id)
3033
if shape is not None:
3034
adjusted_shape = [relpath + '/' + elem for elem in shape]
3035
self.build_tree(adjusted_shape)
3036
ids = ['%s-%s-id' % (relpath, basename(elem.rstrip('/')))
3038
wt.add(shape, ids=ids)
3039
rev_id = 'r1-%s' % (relpath,)
3040
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
3041
self.assertEqual(root_id, wt.path2id(''))
3044
def setup_two_branches(self, custom_root_ids=True):
3045
"""Setup 2 branches, one will be a library, the other a project."""
3049
root_id = inventory.ROOT_ID
3050
project_wt = self.setup_simple_branch(
3051
'project', ['README', 'dir/', 'dir/file.c'],
3053
lib_wt = self.setup_simple_branch(
3054
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3056
return project_wt, lib_wt
3058
def do_merge_into(self, location, merge_as):
3059
"""Helper for using MergeIntoMerger.
3061
:param location: location of directory to merge from, either the
3062
location of a branch or of a path inside a branch.
3063
:param merge_as: the path in a tree to add the new directory as.
3064
:returns: the conflicts from 'do_merge'.
3066
operation = cleanup.OperationWithCleanups(self._merge_into)
3067
return operation.run(location, merge_as)
3069
def _merge_into(self, op, location, merge_as):
3070
# Open and lock the various tree and branch objects
3071
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3072
op.add_cleanup(wt.lock_write().unlock)
3073
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3075
op.add_cleanup(branch_to_merge.lock_read().unlock)
3076
other_tree = branch_to_merge.basis_tree()
3077
op.add_cleanup(other_tree.lock_read().unlock)
3079
merger = _mod_merge.MergeIntoMerger(this_tree=wt, other_tree=other_tree,
3080
other_branch=branch_to_merge, target_subdir=subdir_relpath,
3081
source_subpath=subdir_to_merge)
3082
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3083
conflicts = merger.do_merge()
3084
merger.set_pending()
3087
def assertTreeEntriesEqual(self, expected_entries, tree):
3088
"""Assert that 'tree' contains the expected inventory entries.
3090
:param expected_entries: sequence of (path, file-id) pairs.
3092
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3093
self.assertEqual(expected_entries, files)
3096
class TestMergeInto(TestMergeIntoBase):
3098
def test_newdir_with_unique_roots(self):
3099
"""Merge a branch with a unique root into a new directory."""
3100
project_wt, lib_wt = self.setup_two_branches()
3101
self.do_merge_into('lib1', 'project/lib1')
3102
project_wt.lock_read()
3103
self.addCleanup(project_wt.unlock)
3104
# The r1-lib1 revision should be merged into this one
3105
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3106
self.assertTreeEntriesEqual(
3107
[('', 'project-root-id'),
3108
('README', 'project-README-id'),
3109
('dir', 'project-dir-id'),
3110
('lib1', 'lib1-root-id'),
3111
('dir/file.c', 'project-file.c-id'),
3112
('lib1/Makefile', 'lib1-Makefile-id'),
3113
('lib1/README', 'lib1-README-id'),
3114
('lib1/foo.c', 'lib1-foo.c-id'),
3117
def test_subdir(self):
3118
"""Merge a branch into a subdirectory of an existing directory."""
3119
project_wt, lib_wt = self.setup_two_branches()
3120
self.do_merge_into('lib1', 'project/dir/lib1')
3121
project_wt.lock_read()
3122
self.addCleanup(project_wt.unlock)
3123
# The r1-lib1 revision should be merged into this one
3124
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3125
self.assertTreeEntriesEqual(
3126
[('', 'project-root-id'),
3127
('README', 'project-README-id'),
3128
('dir', 'project-dir-id'),
3129
('dir/file.c', 'project-file.c-id'),
3130
('dir/lib1', 'lib1-root-id'),
3131
('dir/lib1/Makefile', 'lib1-Makefile-id'),
3132
('dir/lib1/README', 'lib1-README-id'),
3133
('dir/lib1/foo.c', 'lib1-foo.c-id'),
3136
def test_newdir_with_repeat_roots(self):
3137
"""If the file-id of the dir to be merged already exists a new ID will
3138
be allocated to let the merge happen.
3140
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3141
root_id = project_wt.path2id('')
3142
self.do_merge_into('lib1', 'project/lib1')
3143
project_wt.lock_read()
3144
self.addCleanup(project_wt.unlock)
3145
# The r1-lib1 revision should be merged into this one
3146
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3147
new_lib1_id = project_wt.path2id('lib1')
3148
self.assertNotEqual(None, new_lib1_id)
3149
self.assertTreeEntriesEqual(
3151
('README', 'project-README-id'),
3152
('dir', 'project-dir-id'),
3153
('lib1', new_lib1_id),
3154
('dir/file.c', 'project-file.c-id'),
3155
('lib1/Makefile', 'lib1-Makefile-id'),
3156
('lib1/README', 'lib1-README-id'),
3157
('lib1/foo.c', 'lib1-foo.c-id'),
3160
def test_name_conflict(self):
3161
"""When the target directory name already exists a conflict is
3162
generated and the original directory is renamed to foo.moved.
3164
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3165
src_wt = self.setup_simple_branch('src', ['README'])
3166
conflicts = self.do_merge_into('src', 'dest/dir')
3167
self.assertEqual(1, conflicts)
3169
self.addCleanup(dest_wt.unlock)
3170
# The r1-lib1 revision should be merged into this one
3171
self.assertEqual(['r1-dest', 'r1-src'], dest_wt.get_parent_ids())
3172
self.assertTreeEntriesEqual(
3173
[('', 'dest-root-id'),
3174
('dir', 'src-root-id'),
3175
('dir.moved', 'dest-dir-id'),
3176
('dir/README', 'src-README-id'),
3177
('dir.moved/file.txt', 'dest-file.txt-id'),
3180
def test_file_id_conflict(self):
3181
"""A conflict is generated if the merge-into adds a file (or other
3182
inventory entry) with a file-id that already exists in the target tree.
3184
dest_wt = self.setup_simple_branch('dest', ['file.txt'])
3185
# Make a second tree with a file-id that will clash with file.txt in
3187
src_wt = self.make_branch_and_tree('src')
3188
self.build_tree(['src/README'])
3189
src_wt.add(['README'], ids=['dest-file.txt-id'])
3190
src_wt.commit("Rev 1 of src.", rev_id='r1-src')
3191
conflicts = self.do_merge_into('src', 'dest/dir')
3192
# This is an edge case that shouldn't happen to users very often. So
3193
# we don't care really about the exact presentation of the conflict,
3194
# just that there is one.
3195
self.assertEqual(1, conflicts)
3197
def test_only_subdir(self):
3198
"""When the location points to just part of a tree, merge just that
3201
dest_wt = self.setup_simple_branch('dest')
3202
src_wt = self.setup_simple_branch(
3203
'src', ['hello.txt', 'dir/', 'dir/foo.c'])
3204
conflicts = self.do_merge_into('src/dir', 'dest/dir')
3206
self.addCleanup(dest_wt.unlock)
3207
# The r1-lib1 revision should NOT be merged into this one (this is a
3209
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3210
self.assertTreeEntriesEqual(
3211
[('', 'dest-root-id'),
3212
('dir', 'src-dir-id'),
3213
('dir/foo.c', 'src-foo.c-id'),
3216
def test_only_file(self):
3217
"""An edge case: merge just one file, not a whole dir."""
3218
dest_wt = self.setup_simple_branch('dest')
3219
two_file_wt = self.setup_simple_branch(
3220
'two-file', ['file1.txt', 'file2.txt'])
3221
conflicts = self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3223
self.addCleanup(dest_wt.unlock)
3224
# The r1-lib1 revision should NOT be merged into this one
3225
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3226
self.assertTreeEntriesEqual(
3227
[('', 'dest-root-id'), ('file1.txt', 'two-file-file1.txt-id')],
3230
def test_no_such_source_path(self):
3231
"""PathNotInTree is raised if the specified path in the source tree
3234
dest_wt = self.setup_simple_branch('dest')
3235
two_file_wt = self.setup_simple_branch('src', ['dir/'])
3236
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3237
'src/no-such-dir', 'dest/foo')
3239
self.addCleanup(dest_wt.unlock)
3240
# The dest tree is unmodified.
3241
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3242
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)
3244
def test_no_such_target_path(self):
3245
"""PathNotInTree is also raised if the specified path in the target
3246
tree does not exist.
3248
dest_wt = self.setup_simple_branch('dest')
3249
two_file_wt = self.setup_simple_branch('src', ['file.txt'])
3250
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3251
'src', 'dest/no-such-dir/foo')
3253
self.addCleanup(dest_wt.unlock)
3254
# The dest tree is unmodified.
3255
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3256
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)