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
32
from bzrlib.conflicts import ConflictList, TextConflict
33
from bzrlib.errors import UnrelatedBranches, NoCommits
34
from bzrlib.merge import transform_tree, merge_inner, _PlanMerge
35
from bzrlib.osutils import pathjoin, file_kind
36
from bzrlib.tests import (
37
TestCaseWithMemoryTransport,
38
TestCaseWithTransport,
41
from bzrlib.workingtree import WorkingTree
44
class TestMerge(TestCaseWithTransport):
45
"""Test appending more than one revision"""
47
def test_pending(self):
48
wt = self.make_branch_and_tree('.')
49
rev_a = wt.commit("lala!")
50
self.assertEqual([rev_a], wt.get_parent_ids())
51
self.assertRaises(errors.PointlessMerge, wt.merge_from_branch,
53
self.assertEqual([rev_a], wt.get_parent_ids())
57
wt = self.make_branch_and_tree('.')
61
wt.merge_from_branch(wt.branch, wt.branch.get_rev_id(2),
62
wt.branch.get_rev_id(1))
64
def test_nocommits(self):
65
wt = self.test_pending()
66
wt2 = self.make_branch_and_tree('branch2')
67
self.assertRaises(NoCommits, wt.merge_from_branch, wt2.branch)
70
def test_unrelated(self):
71
wt, wt2 = self.test_nocommits()
73
self.assertRaises(UnrelatedBranches, wt.merge_from_branch, wt2.branch)
76
def test_merge_one_file(self):
77
"""Do a partial merge of a tree which should not affect tree parents."""
78
wt1 = self.make_branch_and_tree('branch1')
79
tip = wt1.commit('empty commit')
80
wt2 = self.make_branch_and_tree('branch2')
82
file('branch1/foo', 'wb').write('foo')
83
file('branch1/bar', 'wb').write('bar')
86
wt1.commit('add foobar')
88
self.run_bzr('merge ../branch1/baz', retcode=3)
89
self.run_bzr('merge ../branch1/foo')
90
self.failUnlessExists('foo')
91
self.failIfExists('bar')
92
wt2 = WorkingTree.open('.') # opens branch2
93
self.assertEqual([tip], wt2.get_parent_ids())
95
def test_pending_with_null(self):
96
"""When base is forced to revno 0, parent_ids are set"""
97
wt2 = self.test_unrelated()
98
wt1 = WorkingTree.open('.')
100
br1.fetch(wt2.branch)
101
# merge all of branch 2 into branch 1 even though they
103
wt1.merge_from_branch(wt2.branch, wt2.last_revision(), 'null:')
104
self.assertEqual([br1.last_revision(), wt2.branch.last_revision()],
105
wt1.get_parent_ids())
106
return (wt1, wt2.branch)
108
def test_two_roots(self):
109
"""Merge base is sane when two unrelated branches are merged"""
110
wt1, br2 = self.test_pending_with_null()
114
last = wt1.branch.last_revision()
115
last2 = br2.last_revision()
116
graph = wt1.branch.repository.get_graph()
117
self.assertEqual(last2, graph.find_unique_lca(last, last2))
121
def test_create_rename(self):
122
"""Rename an inventory entry while creating the file"""
123
tree =self.make_branch_and_tree('.')
124
file('name1', 'wb').write('Hello')
126
tree.commit(message="hello")
127
tree.rename_one('name1', 'name2')
129
transform_tree(tree, tree.branch.basis_tree())
131
def test_layered_rename(self):
132
"""Rename both child and parent at same time"""
133
tree =self.make_branch_and_tree('.')
136
filename = pathjoin('dirname1', 'name1')
137
file(filename, 'wb').write('Hello')
139
tree.commit(message="hello")
140
filename2 = pathjoin('dirname1', 'name2')
141
tree.rename_one(filename, filename2)
142
tree.rename_one('dirname1', 'dirname2')
143
transform_tree(tree, tree.branch.basis_tree())
145
def test_ignore_zero_merge_inner(self):
146
# Test that merge_inner's ignore zero parameter is effective
147
tree_a =self.make_branch_and_tree('a')
148
tree_a.commit(message="hello")
149
dir_b = tree_a.bzrdir.sprout('b')
150
tree_b = dir_b.open_workingtree()
152
self.addCleanup(tree_b.unlock)
153
tree_a.commit(message="hello again")
155
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
156
this_tree=tree_b, ignore_zero=True)
157
self.failUnless('All changes applied successfully.\n' not in
160
merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
161
this_tree=tree_b, ignore_zero=False)
162
self.failUnless('All changes applied successfully.\n' in self.get_log())
164
def test_merge_inner_conflicts(self):
165
tree_a = self.make_branch_and_tree('a')
166
tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
167
merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
168
self.assertEqual(1, len(tree_a.conflicts()))
170
def test_rmdir_conflict(self):
171
tree_a = self.make_branch_and_tree('a')
172
self.build_tree(['a/b/'])
173
tree_a.add('b', 'b-id')
174
tree_a.commit('added b')
175
# basis_tree() is only guaranteed to be valid as long as it is actually
176
# the basis tree. This mutates the tree after grabbing basis, so go to
178
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
179
tree_z = tree_a.bzrdir.sprout('z').open_workingtree()
180
self.build_tree(['a/b/c'])
182
tree_a.commit('added c')
184
tree_z.commit('removed b')
185
merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
187
conflicts.MissingParent('Created directory', 'b', 'b-id'),
188
conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
190
merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
193
conflicts.DeletingParent('Not deleting', 'b', 'b-id'),
194
conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
197
def test_nested_merge(self):
198
tree = self.make_branch_and_tree('tree',
199
format='dirstate-with-subtree')
200
sub_tree = self.make_branch_and_tree('tree/sub-tree',
201
format='dirstate-with-subtree')
202
sub_tree.set_root_id('sub-tree-root')
203
self.build_tree_contents([('tree/sub-tree/file', 'text1')])
205
sub_tree.commit('foo')
206
tree.add_reference(sub_tree)
207
tree.commit('set text to 1')
208
tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
209
# modify the file in the subtree
210
self.build_tree_contents([('tree2/sub-tree/file', 'text2')])
211
# and merge the changes from the diverged subtree into the containing
213
tree2.commit('changed file text')
214
tree.merge_from_branch(tree2.branch)
215
self.assertFileEqual('text2', 'tree/sub-tree/file')
217
def test_merge_with_missing(self):
218
tree_a = self.make_branch_and_tree('tree_a')
219
self.build_tree_contents([('tree_a/file', 'content_1')])
221
tree_a.commit('commit base')
222
# basis_tree() is only guaranteed to be valid as long as it is actually
223
# the basis tree. This test commits to the tree after grabbing basis,
224
# so we go to the repository.
225
base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
226
tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
227
self.build_tree_contents([('tree_a/file', 'content_2')])
228
tree_a.commit('commit other')
229
other_tree = tree_a.basis_tree()
230
# 'file' is now missing but isn't altered in any commit in b so no
231
# change should be applied.
232
os.unlink('tree_b/file')
233
merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
235
def test_merge_kind_change(self):
236
tree_a = self.make_branch_and_tree('tree_a')
237
self.build_tree_contents([('tree_a/file', 'content_1')])
238
tree_a.add('file', 'file-id')
239
tree_a.commit('added file')
240
tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
241
os.unlink('tree_a/file')
242
self.build_tree(['tree_a/file/'])
243
tree_a.commit('changed file to directory')
244
tree_b.merge_from_branch(tree_a.branch)
245
self.assertEqual('directory', file_kind('tree_b/file'))
247
self.assertEqual('file', file_kind('tree_b/file'))
248
self.build_tree_contents([('tree_b/file', 'content_2')])
249
tree_b.commit('content change')
250
tree_b.merge_from_branch(tree_a.branch)
251
self.assertEqual(tree_b.conflicts(),
252
[conflicts.ContentsConflict('file',
255
def test_merge_type_registry(self):
256
merge_type_option = option.Option.OPTIONS['merge-type']
257
self.assertFalse('merge4' in [x[0] for x in
258
merge_type_option.iter_switches()])
259
registry = _mod_merge.get_merge_type_registry()
260
registry.register_lazy('merge4', 'bzrlib.merge', 'Merge4Merger',
261
'time-travelling merge')
262
self.assertTrue('merge4' in [x[0] for x in
263
merge_type_option.iter_switches()])
264
registry.remove('merge4')
265
self.assertFalse('merge4' in [x[0] for x in
266
merge_type_option.iter_switches()])
268
def test_merge_other_moves_we_deleted(self):
269
tree_a = self.make_branch_and_tree('A')
271
self.addCleanup(tree_a.unlock)
272
self.build_tree(['A/a'])
274
tree_a.commit('1', rev_id='rev-1')
276
tree_a.rename_one('a', 'b')
278
bzrdir_b = tree_a.bzrdir.sprout('B', revision_id='rev-1')
279
tree_b = bzrdir_b.open_workingtree()
281
self.addCleanup(tree_b.unlock)
285
tree_b.merge_from_branch(tree_a.branch)
286
except AttributeError:
287
self.fail('tried to join a path when name was None')
289
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
290
tree_a = self.make_branch_and_tree('a')
291
self.build_tree(['a/file_1', 'a/file_2'])
292
tree_a.add(['file_1'])
293
tree_a.commit('commit 1')
294
tree_a.add(['file_2'])
295
tree_a.commit('commit 2')
296
tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
297
tree_b.rename_one('file_1', 'renamed')
298
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
299
progress.DummyProgress())
300
merger.merge_type = _mod_merge.Merge3Merger
302
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
304
def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
305
tree_a = self.make_branch_and_tree('a')
306
self.build_tree(['a/file_1', 'a/file_2'])
307
tree_a.add(['file_1'])
308
tree_a.commit('commit 1')
309
tree_a.add(['file_2'])
310
tree_a.commit('commit 2')
311
tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
312
tree_b.rename_one('file_1', 'renamed')
313
merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
314
progress.DummyProgress())
315
merger.merge_type = _mod_merge.WeaveMerger
317
self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
319
def test_Merger_defaults_to_DummyProgress(self):
320
branch = self.make_branch('branch')
321
merger = _mod_merge.Merger(branch, pb=None)
322
self.assertIsInstance(merger._pb, progress.DummyProgress)
324
def prepare_cherrypick(self):
325
"""Prepare a pair of trees for cherrypicking tests.
327
Both trees have a file, 'file'.
328
rev1 sets content to 'a'.
331
A full merge of rev2b and rev3b into this_tree would add both 'b' and
332
'c'. A successful cherrypick of rev2b-rev3b into this_tree will add
335
this_tree = self.make_branch_and_tree('this')
336
self.build_tree_contents([('this/file', "a\n")])
337
this_tree.add('file')
338
this_tree.commit('rev1')
339
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
340
self.build_tree_contents([('other/file', "a\nb\n")])
341
other_tree.commit('rev2b', rev_id='rev2b')
342
self.build_tree_contents([('other/file', "c\na\nb\n")])
343
other_tree.commit('rev3b', rev_id='rev3b')
344
this_tree.lock_write()
345
self.addCleanup(this_tree.unlock)
346
return this_tree, other_tree
348
def test_weave_cherrypick(self):
349
this_tree, other_tree = self.prepare_cherrypick()
350
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
351
this_tree, 'rev3b', 'rev2b', other_tree.branch)
352
merger.merge_type = _mod_merge.WeaveMerger
354
self.assertFileEqual('c\na\n', 'this/file')
356
def test_weave_cannot_reverse_cherrypick(self):
357
this_tree, other_tree = self.prepare_cherrypick()
358
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
359
this_tree, 'rev2b', 'rev3b', other_tree.branch)
360
merger.merge_type = _mod_merge.WeaveMerger
361
self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
363
def test_merge3_can_reverse_cherrypick(self):
364
this_tree, other_tree = self.prepare_cherrypick()
365
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
366
this_tree, 'rev2b', 'rev3b', other_tree.branch)
367
merger.merge_type = _mod_merge.Merge3Merger
370
def test_merge3_will_detect_cherrypick(self):
371
this_tree = self.make_branch_and_tree('this')
372
self.build_tree_contents([('this/file', "a\n")])
373
this_tree.add('file')
374
this_tree.commit('rev1')
375
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
376
self.build_tree_contents([('other/file', "a\nb\n")])
377
other_tree.commit('rev2b', rev_id='rev2b')
378
self.build_tree_contents([('other/file', "a\nb\nc\n")])
379
other_tree.commit('rev3b', rev_id='rev3b')
380
this_tree.lock_write()
381
self.addCleanup(this_tree.unlock)
383
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
384
this_tree, 'rev3b', 'rev2b', other_tree.branch)
385
merger.merge_type = _mod_merge.Merge3Merger
387
self.assertFileEqual('a\n'
391
'>>>>>>> MERGE-SOURCE\n',
394
def test_make_merger(self):
395
this_tree = self.make_branch_and_tree('this')
396
this_tree.commit('rev1', rev_id='rev1')
397
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
398
this_tree.commit('rev2', rev_id='rev2a')
399
other_tree.commit('rev2', rev_id='rev2b')
400
this_tree.lock_write()
401
self.addCleanup(this_tree.unlock)
402
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress,
403
this_tree, 'rev2b', other_branch=other_tree.branch)
404
merger.merge_type = _mod_merge.Merge3Merger
405
tree_merger = merger.make_merger()
406
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
407
self.assertEqual('rev2b', tree_merger.other_tree.get_revision_id())
408
self.assertEqual('rev1', tree_merger.base_tree.get_revision_id())
410
def test_make_preview_transform(self):
411
this_tree = self.make_branch_and_tree('this')
412
self.build_tree_contents([('this/file', '1\n')])
413
this_tree.add('file', 'file-id')
414
this_tree.commit('rev1', rev_id='rev1')
415
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
416
self.build_tree_contents([('this/file', '1\n2a\n')])
417
this_tree.commit('rev2', rev_id='rev2a')
418
self.build_tree_contents([('other/file', '2b\n1\n')])
419
other_tree.commit('rev2', rev_id='rev2b')
420
this_tree.lock_write()
421
self.addCleanup(this_tree.unlock)
422
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
423
this_tree, 'rev2b', other_branch=other_tree.branch)
424
merger.merge_type = _mod_merge.Merge3Merger
425
tree_merger = merger.make_merger()
426
tt = tree_merger.make_preview_transform()
427
self.addCleanup(tt.finalize)
428
preview_tree = tt.get_preview_tree()
429
tree_file = this_tree.get_file('file-id')
431
self.assertEqual('1\n2a\n', tree_file.read())
434
preview_file = preview_tree.get_file('file-id')
436
self.assertEqual('2b\n1\n2a\n', preview_file.read())
440
def test_do_merge(self):
441
this_tree = self.make_branch_and_tree('this')
442
self.build_tree_contents([('this/file', '1\n')])
443
this_tree.add('file', 'file-id')
444
this_tree.commit('rev1', rev_id='rev1')
445
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
446
self.build_tree_contents([('this/file', '1\n2a\n')])
447
this_tree.commit('rev2', rev_id='rev2a')
448
self.build_tree_contents([('other/file', '2b\n1\n')])
449
other_tree.commit('rev2', rev_id='rev2b')
450
this_tree.lock_write()
451
self.addCleanup(this_tree.unlock)
452
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
453
this_tree, 'rev2b', other_branch=other_tree.branch)
454
merger.merge_type = _mod_merge.Merge3Merger
455
tree_merger = merger.make_merger()
456
tt = tree_merger.do_merge()
457
tree_file = this_tree.get_file('file-id')
459
self.assertEqual('2b\n1\n2a\n', tree_file.read())
463
def test_merge_add_into_deleted_root(self):
464
# Yes, people actually do this. And report bugs if it breaks.
465
source = self.make_branch_and_tree('source', format='rich-root-pack')
466
self.build_tree(['source/foo/'])
467
source.add('foo', 'foo-id')
468
source.commit('Add foo')
469
target = source.bzrdir.sprout('target').open_workingtree()
470
subtree = target.extract('foo-id')
471
subtree.commit('Delete root')
472
self.build_tree(['source/bar'])
473
source.add('bar', 'bar-id')
474
source.commit('Add bar')
475
subtree.merge_from_branch(source.branch)
477
def test_merge_joined_branch(self):
478
source = self.make_branch_and_tree('source', format='rich-root-pack')
479
self.build_tree(['source/foo'])
481
source.commit('Add foo')
482
target = self.make_branch_and_tree('target', format='rich-root-pack')
483
self.build_tree(['target/bla'])
485
target.commit('Add bla')
486
nested = source.bzrdir.sprout('target/subtree').open_workingtree()
487
target.subsume(nested)
488
target.commit('Join nested')
489
self.build_tree(['source/bar'])
491
source.commit('Add bar')
492
target.merge_from_branch(source.branch)
493
target.commit('Merge source')
496
class TestPlanMerge(TestCaseWithMemoryTransport):
499
TestCaseWithMemoryTransport.setUp(self)
500
mapper = versionedfile.PrefixMapper()
501
factory = knit.make_file_factory(True, mapper)
502
self.vf = factory(self.get_transport())
503
self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
504
self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
506
def add_version(self, key, parents, text):
507
self.vf.add_lines(key, parents, [c+'\n' for c in text])
509
def add_rev(self, prefix, revision_id, parents, text):
510
self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
513
def add_uncommitted_version(self, key, parents, text):
514
self.plan_merge_vf.add_lines(key, parents,
515
[c+'\n' for c in text])
517
def setup_plan_merge(self):
518
self.add_rev('root', 'A', [], 'abc')
519
self.add_rev('root', 'B', ['A'], 'acehg')
520
self.add_rev('root', 'C', ['A'], 'fabg')
521
return _PlanMerge('B', 'C', self.plan_merge_vf, ('root',))
523
def setup_plan_merge_uncommitted(self):
524
self.add_version(('root', 'A'), [], 'abc')
525
self.add_uncommitted_version(('root', 'B:'), [('root', 'A')], 'acehg')
526
self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')
527
return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))
529
def test_base_from_plan(self):
530
self.setup_plan_merge()
531
plan = self.plan_merge_vf.plan_merge('B', 'C')
532
pwm = versionedfile.PlanWeaveMerge(plan)
533
self.assertEqual(['a\n', 'b\n', 'c\n'], pwm.base_from_plan())
535
def test_unique_lines(self):
536
plan = self.setup_plan_merge()
537
self.assertEqual(plan._unique_lines(
538
plan._get_matching_blocks('B', 'C')),
541
def test_plan_merge(self):
542
self.setup_plan_merge()
543
plan = self.plan_merge_vf.plan_merge('B', 'C')
546
('unchanged', 'a\n'),
555
def test_plan_merge_cherrypick(self):
556
self.add_rev('root', 'A', [], 'abc')
557
self.add_rev('root', 'B', ['A'], 'abcde')
558
self.add_rev('root', 'C', ['A'], 'abcefg')
559
self.add_rev('root', 'D', ['A', 'B', 'C'], 'abcdegh')
560
my_plan = _PlanMerge('B', 'D', self.plan_merge_vf, ('root',))
561
# We shortcut when one text supersedes the other in the per-file graph.
562
# We don't actually need to compare the texts at this point.
571
list(my_plan.plan_merge()))
573
def test_plan_merge_no_common_ancestor(self):
574
self.add_rev('root', 'A', [], 'abc')
575
self.add_rev('root', 'B', [], 'xyz')
576
my_plan = _PlanMerge('A', 'B', self.plan_merge_vf, ('root',))
584
list(my_plan.plan_merge()))
586
def test_plan_merge_tail_ancestors(self):
587
# The graph looks like this:
588
# A # Common to all ancestors
590
# B C # Ancestors of E, only common to one side
592
# D E F # D, F are unique to G, H respectively
593
# |/ \| # E is the LCA for G & H, and the unique LCA for
598
# I J # criss-cross merge of G, H
600
# In this situation, a simple pruning of ancestors of E will leave D &
601
# F "dangling", which looks like they introduce lines different from
602
# the ones in E, but in actuality C&B introduced the lines, and they
603
# are already present in E
605
# Introduce the base text
606
self.add_rev('root', 'A', [], 'abc')
607
# Introduces a new line B
608
self.add_rev('root', 'B', ['A'], 'aBbc')
609
# Introduces a new line C
610
self.add_rev('root', 'C', ['A'], 'abCc')
611
# Introduce new line D
612
self.add_rev('root', 'D', ['B'], 'DaBbc')
613
# Merges B and C by just incorporating both
614
self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
615
# Introduce new line F
616
self.add_rev('root', 'F', ['C'], 'abCcF')
617
# Merge D & E by just combining the texts
618
self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
619
# Merge F & E by just combining the texts
620
self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
621
# Merge G & H by just combining texts
622
self.add_rev('root', 'I', ['G', 'H'], 'DaBbCcF')
623
# Merge G & H but supersede an old line in B
624
self.add_rev('root', 'J', ['H', 'G'], 'DaJbCcF')
625
plan = self.plan_merge_vf.plan_merge('I', 'J')
627
('unchanged', 'D\n'),
628
('unchanged', 'a\n'),
631
('unchanged', 'b\n'),
632
('unchanged', 'C\n'),
633
('unchanged', 'c\n'),
634
('unchanged', 'F\n')],
637
def test_plan_merge_tail_triple_ancestors(self):
638
# The graph looks like this:
639
# A # Common to all ancestors
641
# B C # Ancestors of E, only common to one side
643
# D E F # D, F are unique to G, H respectively
644
# |/|\| # E is the LCA for G & H, and the unique LCA for
646
# |\ /| # Q is just an extra node which is merged into both
649
# I J # criss-cross merge of G, H
651
# This is the same as the test_plan_merge_tail_ancestors, except we add
652
# a third LCA that doesn't add new lines, but will trigger our more
653
# involved ancestry logic
655
self.add_rev('root', 'A', [], 'abc')
656
self.add_rev('root', 'B', ['A'], 'aBbc')
657
self.add_rev('root', 'C', ['A'], 'abCc')
658
self.add_rev('root', 'D', ['B'], 'DaBbc')
659
self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
660
self.add_rev('root', 'F', ['C'], 'abCcF')
661
self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
662
self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
663
self.add_rev('root', 'Q', ['E'], 'aBbCc')
664
self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DaBbCcF')
665
# Merge G & H but supersede an old line in B
666
self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DaJbCcF')
667
plan = self.plan_merge_vf.plan_merge('I', 'J')
669
('unchanged', 'D\n'),
670
('unchanged', 'a\n'),
673
('unchanged', 'b\n'),
674
('unchanged', 'C\n'),
675
('unchanged', 'c\n'),
676
('unchanged', 'F\n')],
679
def test_plan_merge_2_tail_triple_ancestors(self):
680
# The graph looks like this:
681
# A B # 2 tails going back to NULL
683
# D E F # D, is unique to G, F to H
684
# |/|\| # E is the LCA for G & H, and the unique LCA for
686
# |\ /| # Q is just an extra node which is merged into both
689
# I J # criss-cross merge of G, H (and Q)
692
# This is meant to test after hitting a 3-way LCA, and multiple tail
693
# ancestors (only have NULL_REVISION in common)
695
self.add_rev('root', 'A', [], 'abc')
696
self.add_rev('root', 'B', [], 'def')
697
self.add_rev('root', 'D', ['A'], 'Dabc')
698
self.add_rev('root', 'E', ['A', 'B'], 'abcdef')
699
self.add_rev('root', 'F', ['B'], 'defF')
700
self.add_rev('root', 'G', ['D', 'E'], 'Dabcdef')
701
self.add_rev('root', 'H', ['F', 'E'], 'abcdefF')
702
self.add_rev('root', 'Q', ['E'], 'abcdef')
703
self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DabcdefF')
704
# Merge G & H but supersede an old line in B
705
self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DabcdJfF')
706
plan = self.plan_merge_vf.plan_merge('I', 'J')
708
('unchanged', 'D\n'),
709
('unchanged', 'a\n'),
710
('unchanged', 'b\n'),
711
('unchanged', 'c\n'),
712
('unchanged', 'd\n'),
715
('unchanged', 'f\n'),
716
('unchanged', 'F\n')],
719
def test_plan_merge_uncommitted_files(self):
720
self.setup_plan_merge_uncommitted()
721
plan = self.plan_merge_vf.plan_merge('B:', 'C:')
724
('unchanged', 'a\n'),
733
def test_plan_merge_insert_order(self):
734
"""Weave merges are sensitive to the order of insertion.
736
Specifically for overlapping regions, it effects which region gets put
737
'first'. And when a user resolves an overlapping merge, if they use the
738
same ordering, then the lines match the parents, if they don't only
739
*some* of the lines match.
741
self.add_rev('root', 'A', [], 'abcdef')
742
self.add_rev('root', 'B', ['A'], 'abwxcdef')
743
self.add_rev('root', 'C', ['A'], 'abyzcdef')
744
# Merge, and resolve the conflict by adding *both* sets of lines
745
# If we get the ordering wrong, these will look like new lines in D,
746
# rather than carried over from B, C
747
self.add_rev('root', 'D', ['B', 'C'],
749
# Supersede the lines in B and delete the lines in C, which will
750
# conflict if they are treated as being in D
751
self.add_rev('root', 'E', ['C', 'B'],
753
# Same thing for the lines in C
754
self.add_rev('root', 'F', ['C'], 'abpqcdef')
755
plan = self.plan_merge_vf.plan_merge('D', 'E')
757
('unchanged', 'a\n'),
758
('unchanged', 'b\n'),
765
('unchanged', 'c\n'),
766
('unchanged', 'd\n'),
767
('unchanged', 'e\n'),
768
('unchanged', 'f\n')],
770
plan = self.plan_merge_vf.plan_merge('E', 'D')
771
# Going in the opposite direction shows the effect of the opposite plan
773
('unchanged', 'a\n'),
774
('unchanged', 'b\n'),
779
('killed-both', 'w\n'),
780
('killed-both', 'x\n'),
783
('unchanged', 'c\n'),
784
('unchanged', 'd\n'),
785
('unchanged', 'e\n'),
786
('unchanged', 'f\n')],
789
def test_plan_merge_criss_cross(self):
790
# This is specificly trying to trigger problems when using limited
791
# ancestry and weaves. The ancestry graph looks like:
792
# XX unused ancestor, should not show up in the weave
796
# B \ Introduces a line 'foo'
798
# C D E C & D both have 'foo', E has different changes
802
# F G All of C, D, E are merged into F and G, so they are
803
# all common ancestors.
805
# The specific issue with weaves:
806
# B introduced a text ('foo') that is present in both C and D.
807
# If we do not include B (because it isn't an ancestor of E), then
808
# the A=>C and A=>D look like both sides independently introduce the
809
# text ('foo'). If F does not modify the text, it would still appear
810
# to have deleted on of the versions from C or D. If G then modifies
811
# 'foo', it should appear as superseding the value in F (since it
812
# came from B), rather than conflict because of the resolution during
814
self.add_rev('root', 'XX', [], 'qrs')
815
self.add_rev('root', 'A', ['XX'], 'abcdef')
816
self.add_rev('root', 'B', ['A'], 'axcdef')
817
self.add_rev('root', 'C', ['B'], 'axcdefg')
818
self.add_rev('root', 'D', ['B'], 'haxcdef')
819
self.add_rev('root', 'E', ['A'], 'abcdyf')
820
# Simple combining of all texts
821
self.add_rev('root', 'F', ['C', 'D', 'E'], 'haxcdyfg')
822
# combine and supersede 'x'
823
self.add_rev('root', 'G', ['C', 'D', 'E'], 'hazcdyfg')
824
plan = self.plan_merge_vf.plan_merge('F', 'G')
826
('unchanged', 'h\n'),
827
('unchanged', 'a\n'),
828
('killed-base', 'b\n'),
831
('unchanged', 'c\n'),
832
('unchanged', 'd\n'),
833
('killed-base', 'e\n'),
834
('unchanged', 'y\n'),
835
('unchanged', 'f\n'),
836
('unchanged', 'g\n')],
838
plan = self.plan_merge_vf.plan_lca_merge('F', 'G')
839
# This is one of the main differences between plan_merge and
840
# plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
841
# because 'x' was not present in one of the bases. However, in this
842
# case it is spurious because 'x' does not exist in the global base A.
844
('unchanged', 'h\n'),
845
('unchanged', 'a\n'),
846
('conflicted-a', 'x\n'),
848
('unchanged', 'c\n'),
849
('unchanged', 'd\n'),
850
('unchanged', 'y\n'),
851
('unchanged', 'f\n'),
852
('unchanged', 'g\n')],
855
def test_criss_cross_flip_flop(self):
856
# This is specificly trying to trigger problems when using limited
857
# ancestry and weaves. The ancestry graph looks like:
858
# XX unused ancestor, should not show up in the weave
862
# B C B & C both introduce a new line
866
# D E B & C are both merged, so both are common ancestors
867
# In the process of merging, both sides order the new
870
self.add_rev('root', 'XX', [], 'qrs')
871
self.add_rev('root', 'A', ['XX'], 'abcdef')
872
self.add_rev('root', 'B', ['A'], 'abcdgef')
873
self.add_rev('root', 'C', ['A'], 'abcdhef')
874
self.add_rev('root', 'D', ['B', 'C'], 'abcdghef')
875
self.add_rev('root', 'E', ['C', 'B'], 'abcdhgef')
876
plan = list(self.plan_merge_vf.plan_merge('D', 'E'))
878
('unchanged', 'a\n'),
879
('unchanged', 'b\n'),
880
('unchanged', 'c\n'),
881
('unchanged', 'd\n'),
883
('unchanged', 'g\n'),
885
('unchanged', 'e\n'),
886
('unchanged', 'f\n'),
888
pwm = versionedfile.PlanWeaveMerge(plan)
889
self.assertEqualDiff('\n'.join('abcdghef') + '\n',
890
''.join(pwm.base_from_plan()))
891
# Reversing the order reverses the merge plan, and final order of 'hg'
893
plan = list(self.plan_merge_vf.plan_merge('E', 'D'))
895
('unchanged', 'a\n'),
896
('unchanged', 'b\n'),
897
('unchanged', 'c\n'),
898
('unchanged', 'd\n'),
900
('unchanged', 'h\n'),
902
('unchanged', 'e\n'),
903
('unchanged', 'f\n'),
905
pwm = versionedfile.PlanWeaveMerge(plan)
906
self.assertEqualDiff('\n'.join('abcdhgef') + '\n',
907
''.join(pwm.base_from_plan()))
908
# This is where lca differs, in that it (fairly correctly) determines
909
# that there is a conflict because both sides resolved the merge
911
plan = list(self.plan_merge_vf.plan_lca_merge('D', 'E'))
913
('unchanged', 'a\n'),
914
('unchanged', 'b\n'),
915
('unchanged', 'c\n'),
916
('unchanged', 'd\n'),
917
('conflicted-b', 'h\n'),
918
('unchanged', 'g\n'),
919
('conflicted-a', 'h\n'),
920
('unchanged', 'e\n'),
921
('unchanged', 'f\n'),
923
pwm = versionedfile.PlanWeaveMerge(plan)
924
self.assertEqualDiff('\n'.join('abcdgef') + '\n',
925
''.join(pwm.base_from_plan()))
926
# Reversing it changes what line is doubled, but still gives a
928
plan = list(self.plan_merge_vf.plan_lca_merge('E', 'D'))
930
('unchanged', 'a\n'),
931
('unchanged', 'b\n'),
932
('unchanged', 'c\n'),
933
('unchanged', 'd\n'),
934
('conflicted-b', 'g\n'),
935
('unchanged', 'h\n'),
936
('conflicted-a', 'g\n'),
937
('unchanged', 'e\n'),
938
('unchanged', 'f\n'),
940
pwm = versionedfile.PlanWeaveMerge(plan)
941
self.assertEqualDiff('\n'.join('abcdhef') + '\n',
942
''.join(pwm.base_from_plan()))
944
def assertRemoveExternalReferences(self, filtered_parent_map,
945
child_map, tails, parent_map):
946
"""Assert results for _PlanMerge._remove_external_references."""
947
(act_filtered_parent_map, act_child_map,
948
act_tails) = _PlanMerge._remove_external_references(parent_map)
950
# The parent map *should* preserve ordering, but the ordering of
951
# children is not strictly defined
952
# child_map = dict((k, sorted(children))
953
# for k, children in child_map.iteritems())
954
# act_child_map = dict(k, sorted(children)
955
# for k, children in act_child_map.iteritems())
956
self.assertEqual(filtered_parent_map, act_filtered_parent_map)
957
self.assertEqual(child_map, act_child_map)
958
self.assertEqual(sorted(tails), sorted(act_tails))
960
def test__remove_external_references(self):
961
# First, nothing to remove
962
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
963
{1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
964
# The reverse direction
965
self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
966
{3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
968
self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
969
{1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
971
self.assertRemoveExternalReferences(
972
{4: [2, 3], 3: [], 2: [1], 1: []},
973
{1: [2], 2: [4], 3: [4], 4: []},
975
{4: [2, 3], 3: [5], 2: [1], 1: [6]})
977
self.assertRemoveExternalReferences(
978
{1: [3], 2: [3, 4], 3: [], 4: []},
979
{1: [], 2: [], 3: [1, 2], 4: [2]},
981
{1: [3], 2: [3, 4], 3: [5], 4: []})
983
def assertPruneTails(self, pruned_map, tails, parent_map):
985
for key, parent_keys in parent_map.iteritems():
986
child_map.setdefault(key, [])
987
for pkey in parent_keys:
988
child_map.setdefault(pkey, []).append(key)
989
_PlanMerge._prune_tails(parent_map, child_map, tails)
990
self.assertEqual(pruned_map, parent_map)
992
def test__prune_tails(self):
993
# Nothing requested to prune
994
self.assertPruneTails({1: [], 2: [], 3: []}, [],
995
{1: [], 2: [], 3: []})
996
# Prune a single entry
997
self.assertPruneTails({1: [], 3: []}, [2],
998
{1: [], 2: [], 3: []})
1000
self.assertPruneTails({1: []}, [3],
1001
{1: [], 2: [3], 3: []})
1002
# Prune a chain with a diamond
1003
self.assertPruneTails({1: []}, [5],
1004
{1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
1005
# Prune a partial chain
1006
self.assertPruneTails({1: [6], 6:[]}, [5],
1007
{1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
1009
# Prune a chain with multiple tips, that pulls out intermediates
1010
self.assertPruneTails({1:[3], 3:[]}, [4, 5],
1011
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1012
self.assertPruneTails({1:[3], 3:[]}, [5, 4],
1013
{1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
1015
def test_subtract_plans(self):
1017
('unchanged', 'a\n'),
1019
('killed-a', 'c\n'),
1022
('killed-b', 'f\n'),
1023
('killed-b', 'g\n'),
1026
('unchanged', 'a\n'),
1028
('killed-a', 'c\n'),
1031
('killed-b', 'f\n'),
1032
('killed-b', 'i\n'),
1035
('unchanged', 'a\n'),
1037
('killed-a', 'c\n'),
1039
('unchanged', 'f\n'),
1040
('killed-b', 'i\n'),
1042
self.assertEqual(subtracted_plan,
1043
list(_PlanMerge._subtract_plans(old_plan, new_plan)))
1045
def setup_merge_with_base(self):
1046
self.add_rev('root', 'COMMON', [], 'abc')
1047
self.add_rev('root', 'THIS', ['COMMON'], 'abcd')
1048
self.add_rev('root', 'BASE', ['COMMON'], 'eabc')
1049
self.add_rev('root', 'OTHER', ['BASE'], 'eafb')
1051
def test_plan_merge_with_base(self):
1052
self.setup_merge_with_base()
1053
plan = self.plan_merge_vf.plan_merge('THIS', 'OTHER', 'BASE')
1054
self.assertEqual([('unchanged', 'a\n'),
1056
('unchanged', 'b\n'),
1057
('killed-b', 'c\n'),
1061
def test_plan_lca_merge(self):
1062
self.setup_plan_merge()
1063
plan = self.plan_merge_vf.plan_lca_merge('B', 'C')
1066
('unchanged', 'a\n'),
1067
('killed-b', 'c\n'),
1070
('killed-a', 'b\n'),
1071
('unchanged', 'g\n')],
1074
def test_plan_lca_merge_uncommitted_files(self):
1075
self.setup_plan_merge_uncommitted()
1076
plan = self.plan_merge_vf.plan_lca_merge('B:', 'C:')
1079
('unchanged', 'a\n'),
1080
('killed-b', 'c\n'),
1083
('killed-a', 'b\n'),
1084
('unchanged', 'g\n')],
1087
def test_plan_lca_merge_with_base(self):
1088
self.setup_merge_with_base()
1089
plan = self.plan_merge_vf.plan_lca_merge('THIS', 'OTHER', 'BASE')
1090
self.assertEqual([('unchanged', 'a\n'),
1092
('unchanged', 'b\n'),
1093
('killed-b', 'c\n'),
1097
def test_plan_lca_merge_with_criss_cross(self):
1098
self.add_version(('root', 'ROOT'), [], 'abc')
1099
# each side makes a change
1100
self.add_version(('root', 'REV1'), [('root', 'ROOT')], 'abcd')
1101
self.add_version(('root', 'REV2'), [('root', 'ROOT')], 'abce')
1102
# both sides merge, discarding others' changes
1103
self.add_version(('root', 'LCA1'),
1104
[('root', 'REV1'), ('root', 'REV2')], 'abcd')
1105
self.add_version(('root', 'LCA2'),
1106
[('root', 'REV1'), ('root', 'REV2')], 'fabce')
1107
plan = self.plan_merge_vf.plan_lca_merge('LCA1', 'LCA2')
1108
self.assertEqual([('new-b', 'f\n'),
1109
('unchanged', 'a\n'),
1110
('unchanged', 'b\n'),
1111
('unchanged', 'c\n'),
1112
('conflicted-a', 'd\n'),
1113
('conflicted-b', 'e\n'),
1116
def test_plan_lca_merge_with_null(self):
1117
self.add_version(('root', 'A'), [], 'ab')
1118
self.add_version(('root', 'B'), [], 'bc')
1119
plan = self.plan_merge_vf.plan_lca_merge('A', 'B')
1120
self.assertEqual([('new-a', 'a\n'),
1121
('unchanged', 'b\n'),
1125
def test_plan_merge_with_delete_and_change(self):
1126
self.add_rev('root', 'C', [], 'a')
1127
self.add_rev('root', 'A', ['C'], 'b')
1128
self.add_rev('root', 'B', ['C'], '')
1129
plan = self.plan_merge_vf.plan_merge('A', 'B')
1130
self.assertEqual([('killed-both', 'a\n'),
1134
def test_plan_merge_with_move_and_change(self):
1135
self.add_rev('root', 'C', [], 'abcd')
1136
self.add_rev('root', 'A', ['C'], 'acbd')
1137
self.add_rev('root', 'B', ['C'], 'aBcd')
1138
plan = self.plan_merge_vf.plan_merge('A', 'B')
1139
self.assertEqual([('unchanged', 'a\n'),
1141
('killed-b', 'b\n'),
1143
('killed-a', 'c\n'),
1144
('unchanged', 'd\n'),
1148
class LoggingMerger(object):
1149
# These seem to be the required attributes
1150
requires_base = False
1151
supports_reprocess = False
1152
supports_show_base = False
1153
supports_cherrypick = False
1154
# We intentionally do not define supports_lca_trees
1156
def __init__(self, *args, **kwargs):
1158
self.kwargs = kwargs
1161
class TestMergerBase(TestCaseWithMemoryTransport):
1162
"""Common functionality for Merger tests that don't write to disk."""
1164
def get_builder(self):
1165
builder = self.make_branch_builder('path')
1166
builder.start_series()
1167
self.addCleanup(builder.finish_series)
1170
def setup_simple_graph(self):
1171
"""Create a simple 3-node graph.
1173
:return: A BranchBuilder
1180
builder = self.get_builder()
1181
builder.build_snapshot('A-id', None,
1182
[('add', ('', None, 'directory', None))])
1183
builder.build_snapshot('C-id', ['A-id'], [])
1184
builder.build_snapshot('B-id', ['A-id'], [])
1187
def setup_criss_cross_graph(self):
1188
"""Create a 5-node graph with a criss-cross.
1190
:return: A BranchBuilder
1197
builder = self.setup_simple_graph()
1198
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1199
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1202
def make_Merger(self, builder, other_revision_id,
1203
interesting_files=None, interesting_ids=None):
1204
"""Make a Merger object from a branch builder"""
1205
mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1206
mem_tree.lock_write()
1207
self.addCleanup(mem_tree.unlock)
1208
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
1209
mem_tree, other_revision_id)
1210
merger.set_interesting_files(interesting_files)
1211
# It seems there is no matching function for set_interesting_ids
1212
merger.interesting_ids = interesting_ids
1213
merger.merge_type = _mod_merge.Merge3Merger
1217
class TestMergerInMemory(TestMergerBase):
1219
def test_cache_trees_with_revision_ids_None(self):
1220
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1221
original_cache = dict(merger._cached_trees)
1222
merger.cache_trees_with_revision_ids([None])
1223
self.assertEqual(original_cache, merger._cached_trees)
1225
def test_cache_trees_with_revision_ids_no_revision_id(self):
1226
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1227
original_cache = dict(merger._cached_trees)
1228
tree = self.make_branch_and_memory_tree('tree')
1229
merger.cache_trees_with_revision_ids([tree])
1230
self.assertEqual(original_cache, merger._cached_trees)
1232
def test_cache_trees_with_revision_ids_having_revision_id(self):
1233
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1234
original_cache = dict(merger._cached_trees)
1235
tree = merger.this_branch.repository.revision_tree('B-id')
1236
original_cache['B-id'] = tree
1237
merger.cache_trees_with_revision_ids([tree])
1238
self.assertEqual(original_cache, merger._cached_trees)
1240
def test_find_base(self):
1241
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1242
self.assertEqual('A-id', merger.base_rev_id)
1243
self.assertFalse(merger._is_criss_cross)
1244
self.assertIs(None, merger._lca_trees)
1246
def test_find_base_criss_cross(self):
1247
builder = self.setup_criss_cross_graph()
1248
merger = self.make_Merger(builder, 'E-id')
1249
self.assertEqual('A-id', merger.base_rev_id)
1250
self.assertTrue(merger._is_criss_cross)
1251
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1252
for t in merger._lca_trees])
1253
# If we swap the order, we should get a different lca order
1254
builder.build_snapshot('F-id', ['E-id'], [])
1255
merger = self.make_Merger(builder, 'D-id')
1256
self.assertEqual(['C-id', 'B-id'], [t.get_revision_id()
1257
for t in merger._lca_trees])
1259
def test_find_base_triple_criss_cross(self):
1262
# B C F # F is merged into both branches
1269
builder = self.setup_criss_cross_graph()
1270
builder.build_snapshot('F-id', ['A-id'], [])
1271
builder.build_snapshot('H-id', ['E-id', 'F-id'], [])
1272
builder.build_snapshot('G-id', ['D-id', 'F-id'], [])
1273
merger = self.make_Merger(builder, 'H-id')
1274
self.assertEqual(['B-id', 'C-id', 'F-id'],
1275
[t.get_revision_id() for t in merger._lca_trees])
1277
def test_no_criss_cross_passed_to_merge_type(self):
1278
class LCATreesMerger(LoggingMerger):
1279
supports_lca_trees = True
1281
merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1282
merger.merge_type = LCATreesMerger
1283
merge_obj = merger.make_merger()
1284
self.assertIsInstance(merge_obj, LCATreesMerger)
1285
self.assertFalse('lca_trees' in merge_obj.kwargs)
1287
def test_criss_cross_passed_to_merge_type(self):
1288
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1289
merger.merge_type = _mod_merge.Merge3Merger
1290
merge_obj = merger.make_merger()
1291
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1292
for t in merger._lca_trees])
1294
def test_criss_cross_not_supported_merge_type(self):
1295
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1296
# We explicitly do not define supports_lca_trees
1297
merger.merge_type = LoggingMerger
1298
merge_obj = merger.make_merger()
1299
self.assertIsInstance(merge_obj, LoggingMerger)
1300
self.assertFalse('lca_trees' in merge_obj.kwargs)
1302
def test_criss_cross_unsupported_merge_type(self):
1303
class UnsupportedLCATreesMerger(LoggingMerger):
1304
supports_lca_trees = False
1306
merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
1307
merger.merge_type = UnsupportedLCATreesMerger
1308
merge_obj = merger.make_merger()
1309
self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
1310
self.assertFalse('lca_trees' in merge_obj.kwargs)
1313
class TestMergerEntriesLCA(TestMergerBase):
1315
def make_merge_obj(self, builder, other_revision_id,
1316
interesting_files=None, interesting_ids=None):
1317
merger = self.make_Merger(builder, other_revision_id,
1318
interesting_files=interesting_files,
1319
interesting_ids=interesting_ids)
1320
return merger.make_merger()
1322
def test_simple(self):
1323
builder = self.get_builder()
1324
builder.build_snapshot('A-id', None,
1325
[('add', (u'', 'a-root-id', 'directory', None)),
1326
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1327
builder.build_snapshot('C-id', ['A-id'],
1328
[('modify', ('a-id', 'a\nb\nC\nc\n'))])
1329
builder.build_snapshot('B-id', ['A-id'],
1330
[('modify', ('a-id', 'a\nB\nb\nc\n'))])
1331
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1332
[('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
1333
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1334
[('modify', ('a-id', 'a\nB\nb\nC\nc\n'))])
1335
merge_obj = self.make_merge_obj(builder, 'E-id')
1337
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1338
for t in merge_obj._lca_trees])
1339
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1340
entries = list(merge_obj._entries_lca())
1342
# (file_id, changed, parents, names, executable)
1343
# BASE, lca1, lca2, OTHER, THIS
1344
root_id = 'a-root-id'
1345
self.assertEqual([('a-id', True,
1346
((root_id, [root_id, root_id]), root_id, root_id),
1347
((u'a', [u'a', u'a']), u'a', u'a'),
1348
((False, [False, False]), False, False)),
1351
def test_not_in_base(self):
1352
# LCAs all have the same last-modified revision for the file, as do
1353
# the tips, but the base has something different
1354
# A base, doesn't have the file
1356
# B C B introduces 'foo', C introduces 'bar'
1358
# D E D and E now both have 'foo' and 'bar'
1360
# F G the files are now in F, G, D and E, but not in A
1363
builder = self.get_builder()
1364
builder.build_snapshot('A-id', None,
1365
[('add', (u'', 'a-root-id', 'directory', None))])
1366
builder.build_snapshot('B-id', ['A-id'],
1367
[('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
1368
builder.build_snapshot('C-id', ['A-id'],
1369
[('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
1370
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1371
[('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
1372
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1373
[('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
1374
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1375
[('modify', (u'bar-id', 'd\ne\nf\nG\n'))])
1376
builder.build_snapshot('F-id', ['D-id', 'E-id'], [])
1377
merge_obj = self.make_merge_obj(builder, 'G-id')
1379
self.assertEqual(['D-id', 'E-id'], [t.get_revision_id()
1380
for t in merge_obj._lca_trees])
1381
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1382
entries = list(merge_obj._entries_lca())
1383
root_id = 'a-root-id'
1384
self.assertEqual([('bar-id', True,
1385
((None, [root_id, root_id]), root_id, root_id),
1386
((None, [u'bar', u'bar']), u'bar', u'bar'),
1387
((None, [False, False]), False, False)),
1390
def test_not_in_this(self):
1391
builder = self.get_builder()
1392
builder.build_snapshot('A-id', None,
1393
[('add', (u'', 'a-root-id', 'directory', None)),
1394
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1395
builder.build_snapshot('B-id', ['A-id'],
1396
[('modify', ('a-id', 'a\nB\nb\nc\n'))])
1397
builder.build_snapshot('C-id', ['A-id'],
1398
[('modify', ('a-id', 'a\nb\nC\nc\n'))])
1399
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1400
[('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
1401
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1402
[('unversion', 'a-id')])
1403
merge_obj = self.make_merge_obj(builder, 'E-id')
1405
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1406
for t in merge_obj._lca_trees])
1407
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1409
entries = list(merge_obj._entries_lca())
1410
root_id = 'a-root-id'
1411
self.assertEqual([('a-id', True,
1412
((root_id, [root_id, root_id]), root_id, None),
1413
((u'a', [u'a', u'a']), u'a', None),
1414
((False, [False, False]), False, None)),
1417
def test_file_not_in_one_lca(self):
1420
# B C # B no file, C introduces a file
1422
# D E # D and E both have the file, unchanged from C
1423
builder = self.get_builder()
1424
builder.build_snapshot('A-id', None,
1425
[('add', (u'', 'a-root-id', 'directory', None))])
1426
builder.build_snapshot('B-id', ['A-id'], [])
1427
builder.build_snapshot('C-id', ['A-id'],
1428
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1429
builder.build_snapshot('E-id', ['C-id', 'B-id'], []) # Inherited from C
1430
builder.build_snapshot('D-id', ['B-id', 'C-id'], # Merged from C
1431
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1432
merge_obj = self.make_merge_obj(builder, 'E-id')
1434
self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
1435
for t in merge_obj._lca_trees])
1436
self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
1438
entries = list(merge_obj._entries_lca())
1439
self.assertEqual([], entries)
1441
def test_not_in_other(self):
1442
builder = self.get_builder()
1443
builder.build_snapshot('A-id', None,
1444
[('add', (u'', 'a-root-id', 'directory', None)),
1445
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1446
builder.build_snapshot('B-id', ['A-id'], [])
1447
builder.build_snapshot('C-id', ['A-id'], [])
1448
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1449
[('unversion', 'a-id')])
1450
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1451
merge_obj = self.make_merge_obj(builder, 'E-id')
1453
entries = list(merge_obj._entries_lca())
1454
root_id = 'a-root-id'
1455
self.assertEqual([('a-id', True,
1456
((root_id, [root_id, root_id]), None, root_id),
1457
((u'a', [u'a', u'a']), None, u'a'),
1458
((False, [False, False]), None, False)),
1461
def test_not_in_other_or_lca(self):
1462
# A base, introduces 'foo'
1464
# B C B nothing, C deletes foo
1466
# D E D restores foo (same as B), E leaves it deleted
1468
# A => B, no changes
1469
# A => C, delete foo (C should supersede B)
1470
# C => D, restore foo
1471
# C => E, no changes
1472
# D would then win 'cleanly' and no record would be given
1473
builder = self.get_builder()
1474
builder.build_snapshot('A-id', None,
1475
[('add', (u'', 'a-root-id', 'directory', None)),
1476
('add', (u'foo', 'foo-id', 'file', 'content\n'))])
1477
builder.build_snapshot('B-id', ['A-id'], [])
1478
builder.build_snapshot('C-id', ['A-id'],
1479
[('unversion', 'foo-id')])
1480
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1481
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1482
merge_obj = self.make_merge_obj(builder, 'E-id')
1484
entries = list(merge_obj._entries_lca())
1485
self.assertEqual([], entries)
1487
def test_not_in_other_mod_in_lca1_not_in_lca2(self):
1488
# A base, introduces 'foo'
1490
# B C B changes 'foo', C deletes foo
1492
# D E D restores foo (same as B), E leaves it deleted (as C)
1494
# A => B, modified foo
1495
# A => C, delete foo, C does not supersede B
1496
# B => D, no changes
1497
# C => D, resolve in favor of B
1498
# B => E, resolve in favor of E
1499
# C => E, no changes
1500
# In this case, we have a conflict of how the changes were resolved. E
1501
# picked C and D picked B, so we should issue a conflict
1502
builder = self.get_builder()
1503
builder.build_snapshot('A-id', None,
1504
[('add', (u'', 'a-root-id', 'directory', None)),
1505
('add', (u'foo', 'foo-id', 'file', 'content\n'))])
1506
builder.build_snapshot('B-id', ['A-id'], [
1507
('modify', ('foo-id', 'new-content\n'))])
1508
builder.build_snapshot('C-id', ['A-id'],
1509
[('unversion', 'foo-id')])
1510
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1511
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1512
merge_obj = self.make_merge_obj(builder, 'E-id')
1514
entries = list(merge_obj._entries_lca())
1515
root_id = 'a-root-id'
1516
self.assertEqual([('foo-id', True,
1517
((root_id, [root_id, None]), None, root_id),
1518
((u'foo', [u'foo', None]), None, 'foo'),
1519
((False, [False, None]), None, False)),
1522
def test_only_in_one_lca(self):
1525
# B C B nothing, C add file
1527
# D E D still has nothing, E removes file
1530
# C => D, removed the file
1532
# C => E, removed the file
1533
# Thus D & E have identical changes, and this is a no-op
1536
# A => C, add file, thus C supersedes B
1537
# w/ C=BASE, D=THIS, E=OTHER we have 'happy convergence'
1538
builder = self.get_builder()
1539
builder.build_snapshot('A-id', None,
1540
[('add', (u'', 'a-root-id', 'directory', None))])
1541
builder.build_snapshot('B-id', ['A-id'], [])
1542
builder.build_snapshot('C-id', ['A-id'],
1543
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1544
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1545
[('unversion', 'a-id')])
1546
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1547
merge_obj = self.make_merge_obj(builder, 'E-id')
1549
entries = list(merge_obj._entries_lca())
1550
self.assertEqual([], entries)
1552
def test_only_in_other(self):
1553
builder = self.get_builder()
1554
builder.build_snapshot('A-id', None,
1555
[('add', (u'', 'a-root-id', 'directory', None))])
1556
builder.build_snapshot('B-id', ['A-id'], [])
1557
builder.build_snapshot('C-id', ['A-id'], [])
1558
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1559
[('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1560
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1561
merge_obj = self.make_merge_obj(builder, 'E-id')
1563
entries = list(merge_obj._entries_lca())
1564
root_id = 'a-root-id'
1565
self.assertEqual([('a-id', True,
1566
((None, [None, None]), root_id, None),
1567
((None, [None, None]), u'a', None),
1568
((None, [None, None]), False, None)),
1571
def test_one_lca_supersedes(self):
1572
# One LCA supersedes the other LCAs last modified value, but the
1573
# value is not the same as BASE.
1574
# A base, introduces 'foo', last mod A
1576
# B C B modifies 'foo' (mod B), C does nothing (mod A)
1578
# D E D does nothing (mod B), E updates 'foo' (mod E)
1580
# F G F updates 'foo' (mod F). G does nothing (mod E)
1582
# At this point, G should not be considered to modify 'foo', even
1583
# though its LCAs disagree. This is because the modification in E
1584
# completely supersedes the value in D.
1585
builder = self.get_builder()
1586
builder.build_snapshot('A-id', None,
1587
[('add', (u'', 'a-root-id', 'directory', None)),
1588
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1589
builder.build_snapshot('C-id', ['A-id'], [])
1590
builder.build_snapshot('B-id', ['A-id'],
1591
[('modify', ('foo-id', 'B content\n'))])
1592
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1593
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1594
[('modify', ('foo-id', 'E content\n'))])
1595
builder.build_snapshot('G-id', ['E-id', 'D-id'], [])
1596
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1597
[('modify', ('foo-id', 'F content\n'))])
1598
merge_obj = self.make_merge_obj(builder, 'G-id')
1600
self.assertEqual([], list(merge_obj._entries_lca()))
1602
def test_one_lca_supersedes_path(self):
1603
# Double-criss-cross merge, the ultimate base value is different from
1607
# B C B value 'bar', C = 'foo'
1609
# D E D = 'bar', E supersedes to 'bing'
1611
# F G F = 'bing', G supersedes to 'barry'
1613
# In this case, we technically should not care about the value 'bar' for
1614
# D, because it was clearly superseded by E's 'bing'. The
1615
# per-file/attribute graph would actually look like:
1624
# Because the other side of the merge never modifies the value, it just
1625
# takes the value from the merge.
1627
# ATM this fails because we will prune 'foo' from the LCAs, but we
1628
# won't prune 'bar'. This is getting far off into edge-case land, so we
1629
# aren't supporting it yet.
1631
builder = self.get_builder()
1632
builder.build_snapshot('A-id', None,
1633
[('add', (u'', 'a-root-id', 'directory', None)),
1634
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1635
builder.build_snapshot('C-id', ['A-id'], [])
1636
builder.build_snapshot('B-id', ['A-id'],
1637
[('rename', ('foo', 'bar'))])
1638
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1639
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1640
[('rename', ('foo', 'bing'))]) # override to bing
1641
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1642
[('rename', ('bing', 'barry'))]) # override to barry
1643
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1644
[('rename', ('bar', 'bing'))]) # Merge in E's change
1645
merge_obj = self.make_merge_obj(builder, 'G-id')
1647
self.expectFailure("We don't do an actual heads() check on lca values,"
1648
" or use the per-attribute graph",
1649
self.assertEqual, [], list(merge_obj._entries_lca()))
1651
def test_one_lca_accidentally_pruned(self):
1652
# Another incorrect resolution from the same basic flaw:
1655
# B C B value 'bar', C = 'foo'
1657
# D E D = 'bar', E reverts to 'foo'
1659
# F G F = 'bing', G switches to 'bar'
1661
# 'bar' will not be seen as an interesting change, because 'foo' will
1662
# be pruned from the LCAs, even though it was newly introduced by E
1664
builder = self.get_builder()
1665
builder.build_snapshot('A-id', None,
1666
[('add', (u'', 'a-root-id', 'directory', None)),
1667
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1668
builder.build_snapshot('C-id', ['A-id'], [])
1669
builder.build_snapshot('B-id', ['A-id'],
1670
[('rename', ('foo', 'bar'))])
1671
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1672
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1673
builder.build_snapshot('G-id', ['E-id', 'D-id'],
1674
[('rename', ('foo', 'bar'))])
1675
builder.build_snapshot('F-id', ['D-id', 'E-id'],
1676
[('rename', ('bar', 'bing'))]) # should end up conflicting
1677
merge_obj = self.make_merge_obj(builder, 'G-id')
1679
entries = list(merge_obj._entries_lca())
1680
root_id = 'a-root-id'
1681
self.expectFailure("We prune values from BASE even when relevant.",
1684
((root_id, [root_id, root_id]), root_id, root_id),
1685
((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
1686
((False, [False, False]), False, False)),
1689
def test_both_sides_revert(self):
1690
# Both sides of a criss-cross revert the text to the lca
1691
# A base, introduces 'foo'
1693
# B C B modifies 'foo', C modifies 'foo'
1695
# D E D reverts to B, E reverts to C
1696
# This should conflict
1697
builder = self.get_builder()
1698
builder.build_snapshot('A-id', None,
1699
[('add', (u'', 'a-root-id', 'directory', None)),
1700
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1701
builder.build_snapshot('B-id', ['A-id'],
1702
[('modify', ('foo-id', 'B content\n'))])
1703
builder.build_snapshot('C-id', ['A-id'],
1704
[('modify', ('foo-id', 'C content\n'))])
1705
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1706
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1707
merge_obj = self.make_merge_obj(builder, 'E-id')
1709
entries = list(merge_obj._entries_lca())
1710
root_id = 'a-root-id'
1711
self.assertEqual([('foo-id', True,
1712
((root_id, [root_id, root_id]), root_id, root_id),
1713
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1714
((False, [False, False]), False, False)),
1717
def test_different_lca_resolve_one_side_updates_content(self):
1718
# Both sides converge, but then one side updates the text.
1719
# A base, introduces 'foo'
1721
# B C B modifies 'foo', C modifies 'foo'
1723
# D E D reverts to B, E reverts to C
1725
# F F updates to a new value
1726
# We need to emit an entry for 'foo', because D & E differed on the
1728
builder = self.get_builder()
1729
builder.build_snapshot('A-id', None,
1730
[('add', (u'', 'a-root-id', 'directory', None)),
1731
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1732
builder.build_snapshot('B-id', ['A-id'],
1733
[('modify', ('foo-id', 'B content\n'))])
1734
builder.build_snapshot('C-id', ['A-id'],
1735
[('modify', ('foo-id', 'C content\n'))])
1736
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1737
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1738
builder.build_snapshot('F-id', ['D-id'],
1739
[('modify', ('foo-id', 'F content\n'))])
1740
merge_obj = self.make_merge_obj(builder, 'E-id')
1742
entries = list(merge_obj._entries_lca())
1743
root_id = 'a-root-id'
1744
self.assertEqual([('foo-id', True,
1745
((root_id, [root_id, root_id]), root_id, root_id),
1746
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
1747
((False, [False, False]), False, False)),
1750
def test_same_lca_resolution_one_side_updates_content(self):
1751
# Both sides converge, but then one side updates the text.
1752
# A base, introduces 'foo'
1754
# B C B modifies 'foo', C modifies 'foo'
1756
# D E D and E use C's value
1758
# F F updates to a new value
1759
# I think it is a bug that this conflicts, but we don't have a way to
1760
# detect otherwise. And because of:
1761
# test_different_lca_resolve_one_side_updates_content
1762
# We need to conflict.
1764
builder = self.get_builder()
1765
builder.build_snapshot('A-id', None,
1766
[('add', (u'', 'a-root-id', 'directory', None)),
1767
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
1768
builder.build_snapshot('B-id', ['A-id'],
1769
[('modify', ('foo-id', 'B content\n'))])
1770
builder.build_snapshot('C-id', ['A-id'],
1771
[('modify', ('foo-id', 'C content\n'))])
1772
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1773
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1774
[('modify', ('foo-id', 'C content\n'))]) # Same as E
1775
builder.build_snapshot('F-id', ['D-id'],
1776
[('modify', ('foo-id', 'F content\n'))])
1777
merge_obj = self.make_merge_obj(builder, 'E-id')
1779
entries = list(merge_obj._entries_lca())
1780
self.expectFailure("We don't detect that LCA resolution was the"
1781
" same on both sides",
1782
self.assertEqual, [], entries)
1784
def test_only_path_changed(self):
1785
builder = self.get_builder()
1786
builder.build_snapshot('A-id', None,
1787
[('add', (u'', 'a-root-id', 'directory', None)),
1788
('add', (u'a', 'a-id', 'file', 'content\n'))])
1789
builder.build_snapshot('B-id', ['A-id'], [])
1790
builder.build_snapshot('C-id', ['A-id'], [])
1791
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1792
[('rename', (u'a', u'b'))])
1793
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1794
merge_obj = self.make_merge_obj(builder, 'E-id')
1795
entries = list(merge_obj._entries_lca())
1796
root_id = 'a-root-id'
1797
# The content was not changed, only the path
1798
self.assertEqual([('a-id', False,
1799
((root_id, [root_id, root_id]), root_id, root_id),
1800
((u'a', [u'a', u'a']), u'b', u'a'),
1801
((False, [False, False]), False, False)),
1804
def test_kind_changed(self):
1805
# Identical content, except 'D' changes a-id into a directory
1806
builder = self.get_builder()
1807
builder.build_snapshot('A-id', None,
1808
[('add', (u'', 'a-root-id', 'directory', None)),
1809
('add', (u'a', 'a-id', 'file', 'content\n'))])
1810
builder.build_snapshot('B-id', ['A-id'], [])
1811
builder.build_snapshot('C-id', ['A-id'], [])
1812
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1813
[('unversion', 'a-id'),
1814
('add', (u'a', 'a-id', 'directory', None))])
1815
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1816
merge_obj = self.make_merge_obj(builder, 'E-id')
1817
entries = list(merge_obj._entries_lca())
1818
root_id = 'a-root-id'
1819
# Only the kind was changed (content)
1820
self.assertEqual([('a-id', True,
1821
((root_id, [root_id, root_id]), root_id, root_id),
1822
((u'a', [u'a', u'a']), u'a', u'a'),
1823
((False, [False, False]), False, False)),
1826
def test_this_changed_kind(self):
1827
# Identical content, but THIS changes a file to a directory
1828
builder = self.get_builder()
1829
builder.build_snapshot('A-id', None,
1830
[('add', (u'', 'a-root-id', 'directory', None)),
1831
('add', (u'a', 'a-id', 'file', 'content\n'))])
1832
builder.build_snapshot('B-id', ['A-id'], [])
1833
builder.build_snapshot('C-id', ['A-id'], [])
1834
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
1835
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1836
[('unversion', 'a-id'),
1837
('add', (u'a', 'a-id', 'directory', None))])
1838
merge_obj = self.make_merge_obj(builder, 'E-id')
1839
entries = list(merge_obj._entries_lca())
1840
# Only the kind was changed (content)
1841
self.assertEqual([], entries)
1843
def test_interesting_files(self):
1844
# Two files modified, but we should filter one of them
1845
builder = self.get_builder()
1846
builder.build_snapshot('A-id', None,
1847
[('add', (u'', 'a-root-id', 'directory', None)),
1848
('add', (u'a', 'a-id', 'file', 'content\n')),
1849
('add', (u'b', 'b-id', 'file', 'content\n'))])
1850
builder.build_snapshot('B-id', ['A-id'], [])
1851
builder.build_snapshot('C-id', ['A-id'], [])
1852
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1853
[('modify', ('a-id', 'new-content\n')),
1854
('modify', ('b-id', 'new-content\n'))])
1855
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1856
merge_obj = self.make_merge_obj(builder, 'E-id',
1857
interesting_files=['b'])
1858
entries = list(merge_obj._entries_lca())
1859
root_id = 'a-root-id'
1860
self.assertEqual([('b-id', True,
1861
((root_id, [root_id, root_id]), root_id, root_id),
1862
((u'b', [u'b', u'b']), u'b', u'b'),
1863
((False, [False, False]), False, False)),
1866
def test_interesting_file_in_this(self):
1867
# This renamed the file, but it should still match the entry in other
1868
builder = self.get_builder()
1869
builder.build_snapshot('A-id', None,
1870
[('add', (u'', 'a-root-id', 'directory', None)),
1871
('add', (u'a', 'a-id', 'file', 'content\n')),
1872
('add', (u'b', 'b-id', 'file', 'content\n'))])
1873
builder.build_snapshot('B-id', ['A-id'], [])
1874
builder.build_snapshot('C-id', ['A-id'], [])
1875
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1876
[('modify', ('a-id', 'new-content\n')),
1877
('modify', ('b-id', 'new-content\n'))])
1878
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1879
[('rename', ('b', 'c'))])
1880
merge_obj = self.make_merge_obj(builder, 'E-id',
1881
interesting_files=['c'])
1882
entries = list(merge_obj._entries_lca())
1883
root_id = 'a-root-id'
1884
self.assertEqual([('b-id', True,
1885
((root_id, [root_id, root_id]), root_id, root_id),
1886
((u'b', [u'b', u'b']), u'b', u'c'),
1887
((False, [False, False]), False, False)),
1890
def test_interesting_file_in_base(self):
1891
# This renamed the file, but it should still match the entry in BASE
1892
builder = self.get_builder()
1893
builder.build_snapshot('A-id', None,
1894
[('add', (u'', 'a-root-id', 'directory', None)),
1895
('add', (u'a', 'a-id', 'file', 'content\n')),
1896
('add', (u'c', 'c-id', 'file', 'content\n'))])
1897
builder.build_snapshot('B-id', ['A-id'],
1898
[('rename', ('c', 'b'))])
1899
builder.build_snapshot('C-id', ['A-id'],
1900
[('rename', ('c', 'b'))])
1901
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1902
[('modify', ('a-id', 'new-content\n')),
1903
('modify', ('c-id', 'new-content\n'))])
1904
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1905
merge_obj = self.make_merge_obj(builder, 'E-id',
1906
interesting_files=['c'])
1907
entries = list(merge_obj._entries_lca())
1908
root_id = 'a-root-id'
1909
self.assertEqual([('c-id', True,
1910
((root_id, [root_id, root_id]), root_id, root_id),
1911
((u'c', [u'b', u'b']), u'b', u'b'),
1912
((False, [False, False]), False, False)),
1915
def test_interesting_file_in_lca(self):
1916
# This renamed the file, but it should still match the entry in LCA
1917
builder = self.get_builder()
1918
builder.build_snapshot('A-id', None,
1919
[('add', (u'', 'a-root-id', 'directory', None)),
1920
('add', (u'a', 'a-id', 'file', 'content\n')),
1921
('add', (u'b', 'b-id', 'file', 'content\n'))])
1922
builder.build_snapshot('B-id', ['A-id'],
1923
[('rename', ('b', 'c'))])
1924
builder.build_snapshot('C-id', ['A-id'], [])
1925
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1926
[('modify', ('a-id', 'new-content\n')),
1927
('modify', ('b-id', 'new-content\n'))])
1928
builder.build_snapshot('D-id', ['B-id', 'C-id'],
1929
[('rename', ('c', 'b'))])
1930
merge_obj = self.make_merge_obj(builder, 'E-id',
1931
interesting_files=['c'])
1932
entries = list(merge_obj._entries_lca())
1933
root_id = 'a-root-id'
1934
self.assertEqual([('b-id', True,
1935
((root_id, [root_id, root_id]), root_id, root_id),
1936
((u'b', [u'c', u'b']), u'b', u'b'),
1937
((False, [False, False]), False, False)),
1940
def test_interesting_ids(self):
1941
# Two files modified, but we should filter one of them
1942
builder = self.get_builder()
1943
builder.build_snapshot('A-id', None,
1944
[('add', (u'', 'a-root-id', 'directory', None)),
1945
('add', (u'a', 'a-id', 'file', 'content\n')),
1946
('add', (u'b', 'b-id', 'file', 'content\n'))])
1947
builder.build_snapshot('B-id', ['A-id'], [])
1948
builder.build_snapshot('C-id', ['A-id'], [])
1949
builder.build_snapshot('E-id', ['C-id', 'B-id'],
1950
[('modify', ('a-id', 'new-content\n')),
1951
('modify', ('b-id', 'new-content\n'))])
1952
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
1953
merge_obj = self.make_merge_obj(builder, 'E-id',
1954
interesting_ids=['b-id'])
1955
entries = list(merge_obj._entries_lca())
1956
root_id = 'a-root-id'
1957
self.assertEqual([('b-id', True,
1958
((root_id, [root_id, root_id]), root_id, root_id),
1959
((u'b', [u'b', u'b']), u'b', u'b'),
1960
((False, [False, False]), False, False)),
1965
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
1967
def get_builder(self):
1968
builder = self.make_branch_builder('path')
1969
builder.start_series()
1970
self.addCleanup(builder.finish_series)
1973
def get_wt_from_builder(self, builder):
1974
"""Get a real WorkingTree from the builder."""
1975
the_branch = builder.get_branch()
1976
wt = the_branch.bzrdir.create_workingtree()
1977
# Note: This is a little bit ugly, but we are holding the branch
1978
# write-locked as part of the build process, and we would like to
1979
# maintain that. So we just force the WT to re-use the same
1981
wt._branch = the_branch
1983
self.addCleanup(wt.unlock)
1986
def do_merge(self, builder, other_revision_id):
1987
wt = self.get_wt_from_builder(builder)
1988
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
1989
wt, other_revision_id)
1990
merger.merge_type = _mod_merge.Merge3Merger
1991
return wt, merger.do_merge()
1993
def test_simple_lca(self):
1994
builder = self.get_builder()
1995
builder.build_snapshot('A-id', None,
1996
[('add', (u'', 'a-root-id', 'directory', None)),
1997
('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
1998
builder.build_snapshot('C-id', ['A-id'], [])
1999
builder.build_snapshot('B-id', ['A-id'], [])
2000
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2001
builder.build_snapshot('D-id', ['B-id', 'C-id'],
2002
[('modify', ('a-id', 'a\nb\nc\nd\ne\nf\n'))])
2003
wt, conflicts = self.do_merge(builder, 'E-id')
2004
self.assertEqual(0, conflicts)
2005
# The merge should have simply update the contents of 'a'
2006
self.assertEqual('a\nb\nc\nd\ne\nf\n', wt.get_file_text('a-id'))
2008
def test_conflict_without_lca(self):
2009
# This test would cause a merge conflict, unless we use the lca trees
2010
# to determine the real ancestry
2013
# B C Path renamed to 'bar' in B
2017
# D E Path at 'bar' in D and E
2019
# F Path at 'baz' in F, which supersedes 'bar' and 'foo'
2020
builder = self.get_builder()
2021
builder.build_snapshot('A-id', None,
2022
[('add', (u'', 'a-root-id', 'directory', None)),
2023
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2024
builder.build_snapshot('C-id', ['A-id'], [])
2025
builder.build_snapshot('B-id', ['A-id'],
2026
[('rename', ('foo', 'bar'))])
2027
builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
2028
[('rename', ('foo', 'bar'))])
2029
builder.build_snapshot('F-id', ['E-id'],
2030
[('rename', ('bar', 'baz'))])
2031
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2032
wt, conflicts = self.do_merge(builder, 'F-id')
2033
self.assertEqual(0, conflicts)
2034
# The merge should simply recognize that the final rename takes
2036
self.assertEqual('baz', wt.id2path('foo-id'))
2038
def test_other_deletes_lca_renames(self):
2039
# This test would cause a merge conflict, unless we use the lca trees
2040
# to determine the real ancestry
2043
# B C Path renamed to 'bar' in B
2047
# D E Path at 'bar' in D and E
2050
builder = self.get_builder()
2051
builder.build_snapshot('A-id', None,
2052
[('add', (u'', 'a-root-id', 'directory', None)),
2053
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2054
builder.build_snapshot('C-id', ['A-id'], [])
2055
builder.build_snapshot('B-id', ['A-id'],
2056
[('rename', ('foo', 'bar'))])
2057
builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
2058
[('rename', ('foo', 'bar'))])
2059
builder.build_snapshot('F-id', ['E-id'],
2060
[('unversion', 'foo-id')])
2061
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2062
wt, conflicts = self.do_merge(builder, 'F-id')
2063
self.assertEqual(0, conflicts)
2064
self.assertRaises(errors.NoSuchId, wt.id2path, 'foo-id')
2066
def test_executable_changes(self):
2075
# F Executable bit changed
2076
builder = self.get_builder()
2077
builder.build_snapshot('A-id', None,
2078
[('add', (u'', 'a-root-id', 'directory', None)),
2079
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2080
builder.build_snapshot('C-id', ['A-id'], [])
2081
builder.build_snapshot('B-id', ['A-id'], [])
2082
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2083
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2084
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2085
wt = self.get_wt_from_builder(builder)
2086
tt = transform.TreeTransform(wt)
2088
tt.set_executability(True, tt.trans_id_tree_file_id('foo-id'))
2093
self.assertTrue(wt.is_executable('foo-id'))
2094
wt.commit('F-id', rev_id='F-id')
2095
# Reset to D, so that we can merge F
2096
wt.set_parent_ids(['D-id'])
2097
wt.branch.set_last_revision_info(3, 'D-id')
2099
self.assertFalse(wt.is_executable('foo-id'))
2100
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2101
self.assertEqual(0, conflicts)
2102
self.assertTrue(wt.is_executable('foo-id'))
2104
def test_create_symlink(self):
2105
self.requireFeature(tests.SymlinkFeature)
2114
# F Add a symlink 'foo' => 'bar'
2115
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2116
# have symlink support
2117
builder = self.get_builder()
2118
builder.build_snapshot('A-id', None,
2119
[('add', (u'', 'a-root-id', 'directory', None))])
2120
builder.build_snapshot('C-id', ['A-id'], [])
2121
builder.build_snapshot('B-id', ['A-id'], [])
2122
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2123
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2124
# Have to use a real WT, because BranchBuilder doesn't support exec bit
2125
wt = self.get_wt_from_builder(builder)
2126
os.symlink('bar', 'path/foo')
2127
wt.add(['foo'], ['foo-id'])
2128
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2129
wt.commit('add symlink', rev_id='F-id')
2130
# Reset to D, so that we can merge F
2131
wt.set_parent_ids(['D-id'])
2132
wt.branch.set_last_revision_info(3, 'D-id')
2134
self.assertIs(None, wt.path2id('foo'))
2135
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2136
self.assertEqual(0, conflicts)
2137
self.assertEqual('foo-id', wt.path2id('foo'))
2138
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2140
def test_both_sides_revert(self):
2141
# Both sides of a criss-cross revert the text to the lca
2142
# A base, introduces 'foo'
2144
# B C B modifies 'foo', C modifies 'foo'
2146
# D E D reverts to B, E reverts to C
2147
# This should conflict
2148
# This must be done with a real WorkingTree, because normally their
2149
# inventory contains "None" rather than a real sha1
2150
builder = self.get_builder()
2151
builder.build_snapshot('A-id', None,
2152
[('add', (u'', 'a-root-id', 'directory', None)),
2153
('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
2154
builder.build_snapshot('B-id', ['A-id'],
2155
[('modify', ('foo-id', 'B content\n'))])
2156
builder.build_snapshot('C-id', ['A-id'],
2157
[('modify', ('foo-id', 'C content\n'))])
2158
builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
2159
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2160
wt, conflicts = self.do_merge(builder, 'E-id')
2161
self.assertEqual(1, conflicts)
2162
self.assertEqualDiff('<<<<<<< TREE\n'
2166
'>>>>>>> MERGE-SOURCE\n',
2167
wt.get_file_text('foo-id'))
2169
def test_modified_symlink(self):
2170
self.requireFeature(tests.SymlinkFeature)
2171
# A Create symlink foo => bar
2173
# B C B relinks foo => baz
2177
# D E D & E have foo => baz
2179
# F F changes it to bing
2181
# Merging D & F should result in F cleanly overriding D, because D's
2182
# value actually comes from B
2184
# Have to use a real WT, because BranchBuilder and MemoryTree don't
2185
# have symlink support
2186
wt = self.make_branch_and_tree('path')
2188
self.addCleanup(wt.unlock)
2189
os.symlink('bar', 'path/foo')
2190
wt.add(['foo'], ['foo-id'])
2191
wt.commit('add symlink', rev_id='A-id')
2192
os.remove('path/foo')
2193
os.symlink('baz', 'path/foo')
2194
wt.commit('foo => baz', rev_id='B-id')
2195
wt.set_last_revision('A-id')
2196
wt.branch.set_last_revision_info(1, 'A-id')
2198
wt.commit('C', rev_id='C-id')
2199
wt.merge_from_branch(wt.branch, 'B-id')
2200
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2201
wt.commit('E merges C & B', rev_id='E-id')
2202
os.remove('path/foo')
2203
os.symlink('bing', 'path/foo')
2204
wt.commit('F foo => bing', rev_id='F-id')
2205
wt.set_last_revision('B-id')
2206
wt.branch.set_last_revision_info(2, 'B-id')
2208
wt.merge_from_branch(wt.branch, 'C-id')
2209
wt.commit('D merges B & C', rev_id='D-id')
2210
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2211
self.assertEqual(0, conflicts)
2212
self.assertEqual('bing', wt.get_symlink_target('foo-id'))
2214
def test_renamed_symlink(self):
2215
self.requireFeature(tests.SymlinkFeature)
2216
# A Create symlink foo => bar
2218
# B C B renames foo => barry
2222
# D E D & E have barry
2224
# F F renames barry to blah
2226
# Merging D & F should result in F cleanly overriding D, because D's
2227
# value actually comes from B
2229
wt = self.make_branch_and_tree('path')
2231
self.addCleanup(wt.unlock)
2232
os.symlink('bar', 'path/foo')
2233
wt.add(['foo'], ['foo-id'])
2234
wt.commit('A add symlink', rev_id='A-id')
2235
wt.rename_one('foo', 'barry')
2236
wt.commit('B foo => barry', rev_id='B-id')
2237
wt.set_last_revision('A-id')
2238
wt.branch.set_last_revision_info(1, 'A-id')
2240
wt.commit('C', rev_id='C-id')
2241
wt.merge_from_branch(wt.branch, 'B-id')
2242
self.assertEqual('barry', wt.id2path('foo-id'))
2243
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2244
wt.commit('E merges C & B', rev_id='E-id')
2245
wt.rename_one('barry', 'blah')
2246
wt.commit('F barry => blah', rev_id='F-id')
2247
wt.set_last_revision('B-id')
2248
wt.branch.set_last_revision_info(2, 'B-id')
2250
wt.merge_from_branch(wt.branch, 'C-id')
2251
wt.commit('D merges B & C', rev_id='D-id')
2252
self.assertEqual('barry', wt.id2path('foo-id'))
2253
# Check the output of the Merger object directly
2254
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2256
merger.merge_type = _mod_merge.Merge3Merger
2257
merge_obj = merger.make_merger()
2258
root_id = wt.path2id('')
2259
entries = list(merge_obj._entries_lca())
2260
# No content change, just a path change
2261
self.assertEqual([('foo-id', False,
2262
((root_id, [root_id, root_id]), root_id, root_id),
2263
((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
2264
((False, [False, False]), False, False)),
2266
conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
2267
self.assertEqual(0, conflicts)
2268
self.assertEqual('blah', wt.id2path('foo-id'))
2270
def test_symlink_no_content_change(self):
2271
self.requireFeature(tests.SymlinkFeature)
2272
# A Create symlink foo => bar
2274
# B C B relinks foo => baz
2278
# D E D & E have foo => baz
2280
# F F has foo => bing
2282
# Merging E into F should not cause a conflict, because E doesn't have
2283
# a content change relative to the LCAs (it does relative to A)
2284
wt = self.make_branch_and_tree('path')
2286
self.addCleanup(wt.unlock)
2287
os.symlink('bar', 'path/foo')
2288
wt.add(['foo'], ['foo-id'])
2289
wt.commit('add symlink', rev_id='A-id')
2290
os.remove('path/foo')
2291
os.symlink('baz', 'path/foo')
2292
wt.commit('foo => baz', rev_id='B-id')
2293
wt.set_last_revision('A-id')
2294
wt.branch.set_last_revision_info(1, 'A-id')
2296
wt.commit('C', rev_id='C-id')
2297
wt.merge_from_branch(wt.branch, 'B-id')
2298
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2299
wt.commit('E merges C & B', rev_id='E-id')
2300
wt.set_last_revision('B-id')
2301
wt.branch.set_last_revision_info(2, 'B-id')
2303
wt.merge_from_branch(wt.branch, 'C-id')
2304
wt.commit('D merges B & C', rev_id='D-id')
2305
os.remove('path/foo')
2306
os.symlink('bing', 'path/foo')
2307
wt.commit('F foo => bing', rev_id='F-id')
2309
# Check the output of the Merger object directly
2310
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2312
merger.merge_type = _mod_merge.Merge3Merger
2313
merge_obj = merger.make_merger()
2314
# Nothing interesting happened in OTHER relative to BASE
2315
self.assertEqual([], list(merge_obj._entries_lca()))
2316
# Now do a real merge, just to test the rest of the stack
2317
conflicts = wt.merge_from_branch(wt.branch, to_revision='E-id')
2318
self.assertEqual(0, conflicts)
2319
self.assertEqual('bing', wt.get_symlink_target('foo-id'))
2321
def test_symlink_this_changed_kind(self):
2322
self.requireFeature(tests.SymlinkFeature)
2325
# B C B creates symlink foo => bar
2329
# D E D changes foo into a file, E has foo => bing
2331
# Mostly, this is trying to test that we don't try to os.readlink() on
2332
# a file, or when there is nothing there
2333
wt = self.make_branch_and_tree('path')
2335
self.addCleanup(wt.unlock)
2336
wt.commit('base', rev_id='A-id')
2337
os.symlink('bar', 'path/foo')
2338
wt.add(['foo'], ['foo-id'])
2339
wt.commit('add symlink foo => bar', rev_id='B-id')
2340
wt.set_last_revision('A-id')
2341
wt.branch.set_last_revision_info(1, 'A-id')
2343
wt.commit('C', rev_id='C-id')
2344
wt.merge_from_branch(wt.branch, 'B-id')
2345
self.assertEqual('bar', wt.get_symlink_target('foo-id'))
2346
os.remove('path/foo')
2347
# We have to change the link in E, or it won't try to do a comparison
2348
os.symlink('bing', 'path/foo')
2349
wt.commit('E merges C & B, overrides to bing', rev_id='E-id')
2350
wt.set_last_revision('B-id')
2351
wt.branch.set_last_revision_info(2, 'B-id')
2353
wt.merge_from_branch(wt.branch, 'C-id')
2354
os.remove('path/foo')
2355
self.build_tree_contents([('path/foo', 'file content\n')])
2356
# XXX: workaround, WT doesn't detect kind changes unless you do
2358
list(wt.iter_changes(wt.basis_tree()))
2359
wt.commit('D merges B & C, makes it a file', rev_id='D-id')
2361
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2363
merger.merge_type = _mod_merge.Merge3Merger
2364
merge_obj = merger.make_merger()
2365
entries = list(merge_obj._entries_lca())
2366
root_id = wt.path2id('')
2367
self.assertEqual([('foo-id', True,
2368
((None, [root_id, None]), root_id, root_id),
2369
((None, [u'foo', None]), u'foo', u'foo'),
2370
((None, [False, None]), False, False)),
2373
def test_symlink_all_wt(self):
2374
"""Check behavior if all trees are Working Trees."""
2375
self.requireFeature(tests.SymlinkFeature)
2376
# The big issue is that entry.symlink_target is None for WorkingTrees.
2377
# So we need to make sure we handle that case correctly.
2380
# B C B relinks foo => baz
2382
# D E D & E have foo => baz
2384
# F F changes it to bing
2385
# Merging D & F should result in F cleanly overriding D, because D's
2386
# value actually comes from B
2388
wt = self.make_branch_and_tree('path')
2390
self.addCleanup(wt.unlock)
2391
os.symlink('bar', 'path/foo')
2392
wt.add(['foo'], ['foo-id'])
2393
wt.commit('add symlink', rev_id='A-id')
2394
os.remove('path/foo')
2395
os.symlink('baz', 'path/foo')
2396
wt.commit('foo => baz', rev_id='B-id')
2397
wt.set_last_revision('A-id')
2398
wt.branch.set_last_revision_info(1, 'A-id')
2400
wt.commit('C', rev_id='C-id')
2401
wt.merge_from_branch(wt.branch, 'B-id')
2402
self.assertEqual('baz', wt.get_symlink_target('foo-id'))
2403
wt.commit('E merges C & B', rev_id='E-id')
2404
os.remove('path/foo')
2405
os.symlink('bing', 'path/foo')
2406
wt.commit('F foo => bing', rev_id='F-id')
2407
wt.set_last_revision('B-id')
2408
wt.branch.set_last_revision_info(2, 'B-id')
2410
wt.merge_from_branch(wt.branch, 'C-id')
2411
wt.commit('D merges B & C', rev_id='D-id')
2412
wt_base = wt.bzrdir.sprout('base', 'A-id').open_workingtree()
2414
self.addCleanup(wt_base.unlock)
2415
wt_lca1 = wt.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
2417
self.addCleanup(wt_lca1.unlock)
2418
wt_lca2 = wt.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
2420
self.addCleanup(wt_lca2.unlock)
2421
wt_other = wt.bzrdir.sprout('other', 'F-id').open_workingtree()
2422
wt_other.lock_read()
2423
self.addCleanup(wt_other.unlock)
2424
merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
2425
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2426
entries = list(merge_obj._entries_lca())
2427
root_id = wt.path2id('')
2428
self.assertEqual([('foo-id', True,
2429
((root_id, [root_id, root_id]), root_id, root_id),
2430
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2431
((False, [False, False]), False, False)),
2434
def test_other_reverted_path_to_base(self):
2437
# B C Path at 'bar' in B
2444
builder = self.get_builder()
2445
builder.build_snapshot('A-id', None,
2446
[('add', (u'', 'a-root-id', 'directory', None)),
2447
('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
2448
builder.build_snapshot('C-id', ['A-id'], [])
2449
builder.build_snapshot('B-id', ['A-id'],
2450
[('rename', ('foo', 'bar'))])
2451
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2452
[('rename', ('foo', 'bar'))]) # merge the rename
2453
builder.build_snapshot('F-id', ['E-id'],
2454
[('rename', ('bar', 'foo'))]) # Rename back to BASE
2455
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2456
wt, conflicts = self.do_merge(builder, 'F-id')
2457
self.assertEqual(0, conflicts)
2458
self.assertEqual('foo', wt.id2path('foo-id'))
2460
def test_other_reverted_content_to_base(self):
2461
builder = self.get_builder()
2462
builder.build_snapshot('A-id', None,
2463
[('add', (u'', 'a-root-id', 'directory', None)),
2464
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2465
builder.build_snapshot('C-id', ['A-id'], [])
2466
builder.build_snapshot('B-id', ['A-id'],
2467
[('modify', ('foo-id', 'B content\n'))])
2468
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2469
[('modify', ('foo-id', 'B content\n'))]) # merge the content
2470
builder.build_snapshot('F-id', ['E-id'],
2471
[('modify', ('foo-id', 'base content\n'))]) # Revert back to BASE
2472
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2473
wt, conflicts = self.do_merge(builder, 'F-id')
2474
self.assertEqual(0, conflicts)
2475
# TODO: We need to use the per-file graph to properly select a BASE
2476
# before this will work. Or at least use the LCA trees to find
2477
# the appropriate content base. (which is B, not A).
2478
self.assertEqual('base content\n', wt.get_file_text('foo-id'))
2480
def test_other_modified_content(self):
2481
builder = self.get_builder()
2482
builder.build_snapshot('A-id', None,
2483
[('add', (u'', 'a-root-id', 'directory', None)),
2484
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2485
builder.build_snapshot('C-id', ['A-id'], [])
2486
builder.build_snapshot('B-id', ['A-id'],
2487
[('modify', ('foo-id', 'B content\n'))])
2488
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2489
[('modify', ('foo-id', 'B content\n'))]) # merge the content
2490
builder.build_snapshot('F-id', ['E-id'],
2491
[('modify', ('foo-id', 'F content\n'))]) # Override B content
2492
builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
2493
wt, conflicts = self.do_merge(builder, 'F-id')
2494
self.assertEqual(0, conflicts)
2495
self.assertEqual('F content\n', wt.get_file_text('foo-id'))
2497
def test_all_wt(self):
2498
"""Check behavior if all trees are Working Trees."""
2499
# The big issue is that entry.revision is None for WorkingTrees. (as is
2500
# entry.text_sha1, etc. So we need to make sure we handle that case
2502
# A Content of 'foo', path of 'a'
2504
# B C B modifies content, C renames 'a' => 'b'
2506
# D E E updates content, renames 'b' => 'c'
2507
builder = self.get_builder()
2508
builder.build_snapshot('A-id', None,
2509
[('add', (u'', 'a-root-id', 'directory', None)),
2510
('add', (u'a', 'a-id', 'file', 'base content\n')),
2511
('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
2512
builder.build_snapshot('B-id', ['A-id'],
2513
[('modify', ('foo-id', 'B content\n'))])
2514
builder.build_snapshot('C-id', ['A-id'],
2515
[('rename', ('a', 'b'))])
2516
builder.build_snapshot('E-id', ['C-id', 'B-id'],
2517
[('rename', ('b', 'c')),
2518
('modify', ('foo-id', 'E content\n'))])
2519
builder.build_snapshot('D-id', ['B-id', 'C-id'],
2520
[('rename', ('a', 'b'))]) # merged change
2521
wt_this = self.get_wt_from_builder(builder)
2522
wt_base = wt_this.bzrdir.sprout('base', 'A-id').open_workingtree()
2524
self.addCleanup(wt_base.unlock)
2525
wt_lca1 = wt_this.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
2527
self.addCleanup(wt_lca1.unlock)
2528
wt_lca2 = wt_this.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
2530
self.addCleanup(wt_lca2.unlock)
2531
wt_other = wt_this.bzrdir.sprout('other', 'E-id').open_workingtree()
2532
wt_other.lock_read()
2533
self.addCleanup(wt_other.unlock)
2534
merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
2535
wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
2536
entries = list(merge_obj._entries_lca())
2537
root_id = 'a-root-id'
2538
self.assertEqual([('a-id', False,
2539
((root_id, [root_id, root_id]), root_id, root_id),
2540
((u'a', [u'a', u'b']), u'c', u'b'),
2541
((False, [False, False]), False, False)),
2543
((root_id, [root_id, root_id]), root_id, root_id),
2544
((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
2545
((False, [False, False]), False, False)),
2548
def test_nested_tree_unmodified(self):
2549
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2551
wt = self.make_branch_and_tree('tree',
2552
format='dirstate-with-subtree')
2554
self.addCleanup(wt.unlock)
2555
sub_tree = self.make_branch_and_tree('tree/sub-tree',
2556
format='dirstate-with-subtree')
2557
wt.set_root_id('a-root-id')
2558
sub_tree.set_root_id('sub-tree-root')
2559
self.build_tree_contents([('tree/sub-tree/file', 'text1')])
2560
sub_tree.add('file')
2561
sub_tree.commit('foo', rev_id='sub-A-id')
2562
wt.add_reference(sub_tree)
2563
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2564
# Now create a criss-cross merge in the parent, without modifying the
2566
wt.commit('B', rev_id='B-id', recursive=None)
2567
wt.set_last_revision('A-id')
2568
wt.branch.set_last_revision_info(1, 'A-id')
2569
wt.commit('C', rev_id='C-id', recursive=None)
2570
wt.merge_from_branch(wt.branch, to_revision='B-id')
2571
wt.commit('E', rev_id='E-id', recursive=None)
2572
wt.set_parent_ids(['B-id', 'C-id'])
2573
wt.branch.set_last_revision_info(2, 'B-id')
2574
wt.commit('D', rev_id='D-id', recursive=None)
2576
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2578
merger.merge_type = _mod_merge.Merge3Merger
2579
merge_obj = merger.make_merger()
2580
entries = list(merge_obj._entries_lca())
2581
self.assertEqual([], entries)
2583
def test_nested_tree_subtree_modified(self):
2584
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2586
wt = self.make_branch_and_tree('tree',
2587
format='dirstate-with-subtree')
2589
self.addCleanup(wt.unlock)
2590
sub_tree = self.make_branch_and_tree('tree/sub',
2591
format='dirstate-with-subtree')
2592
wt.set_root_id('a-root-id')
2593
sub_tree.set_root_id('sub-tree-root')
2594
self.build_tree_contents([('tree/sub/file', 'text1')])
2595
sub_tree.add('file')
2596
sub_tree.commit('foo', rev_id='sub-A-id')
2597
wt.add_reference(sub_tree)
2598
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2599
# Now create a criss-cross merge in the parent, without modifying the
2601
wt.commit('B', rev_id='B-id', recursive=None)
2602
wt.set_last_revision('A-id')
2603
wt.branch.set_last_revision_info(1, 'A-id')
2604
wt.commit('C', rev_id='C-id', recursive=None)
2605
wt.merge_from_branch(wt.branch, to_revision='B-id')
2606
self.build_tree_contents([('tree/sub/file', 'text2')])
2607
sub_tree.commit('modify contents', rev_id='sub-B-id')
2608
wt.commit('E', rev_id='E-id', recursive=None)
2609
wt.set_parent_ids(['B-id', 'C-id'])
2610
wt.branch.set_last_revision_info(2, 'B-id')
2611
wt.commit('D', rev_id='D-id', recursive=None)
2613
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2615
merger.merge_type = _mod_merge.Merge3Merger
2616
merge_obj = merger.make_merger()
2617
entries = list(merge_obj._entries_lca())
2618
# Nothing interesting about this sub-tree, because content changes are
2619
# computed at a higher level
2620
self.assertEqual([], entries)
2622
def test_nested_tree_subtree_renamed(self):
2623
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2625
wt = self.make_branch_and_tree('tree',
2626
format='dirstate-with-subtree')
2628
self.addCleanup(wt.unlock)
2629
sub_tree = self.make_branch_and_tree('tree/sub',
2630
format='dirstate-with-subtree')
2631
wt.set_root_id('a-root-id')
2632
sub_tree.set_root_id('sub-tree-root')
2633
self.build_tree_contents([('tree/sub/file', 'text1')])
2634
sub_tree.add('file')
2635
sub_tree.commit('foo', rev_id='sub-A-id')
2636
wt.add_reference(sub_tree)
2637
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2638
# Now create a criss-cross merge in the parent, without modifying the
2640
wt.commit('B', rev_id='B-id', recursive=None)
2641
wt.set_last_revision('A-id')
2642
wt.branch.set_last_revision_info(1, 'A-id')
2643
wt.commit('C', rev_id='C-id', recursive=None)
2644
wt.merge_from_branch(wt.branch, to_revision='B-id')
2645
wt.rename_one('sub', 'alt_sub')
2646
wt.commit('E', rev_id='E-id', recursive=None)
2647
wt.set_last_revision('B-id')
2649
wt.set_parent_ids(['B-id', 'C-id'])
2650
wt.branch.set_last_revision_info(2, 'B-id')
2651
wt.commit('D', rev_id='D-id', recursive=None)
2653
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2655
merger.merge_type = _mod_merge.Merge3Merger
2656
merge_obj = merger.make_merger()
2657
entries = list(merge_obj._entries_lca())
2658
root_id = 'a-root-id'
2659
self.assertEqual([('sub-tree-root', False,
2660
((root_id, [root_id, root_id]), root_id, root_id),
2661
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2662
((False, [False, False]), False, False)),
2665
def test_nested_tree_subtree_renamed_and_modified(self):
2666
# Tested with a real WT, because BranchBuilder/MemoryTree don't handle
2668
wt = self.make_branch_and_tree('tree',
2669
format='dirstate-with-subtree')
2671
self.addCleanup(wt.unlock)
2672
sub_tree = self.make_branch_and_tree('tree/sub',
2673
format='dirstate-with-subtree')
2674
wt.set_root_id('a-root-id')
2675
sub_tree.set_root_id('sub-tree-root')
2676
self.build_tree_contents([('tree/sub/file', 'text1')])
2677
sub_tree.add('file')
2678
sub_tree.commit('foo', rev_id='sub-A-id')
2679
wt.add_reference(sub_tree)
2680
wt.commit('set text to 1', rev_id='A-id', recursive=None)
2681
# Now create a criss-cross merge in the parent, without modifying the
2683
wt.commit('B', rev_id='B-id', recursive=None)
2684
wt.set_last_revision('A-id')
2685
wt.branch.set_last_revision_info(1, 'A-id')
2686
wt.commit('C', rev_id='C-id', recursive=None)
2687
wt.merge_from_branch(wt.branch, to_revision='B-id')
2688
self.build_tree_contents([('tree/sub/file', 'text2')])
2689
sub_tree.commit('modify contents', rev_id='sub-B-id')
2690
wt.rename_one('sub', 'alt_sub')
2691
wt.commit('E', rev_id='E-id', recursive=None)
2692
wt.set_last_revision('B-id')
2694
wt.set_parent_ids(['B-id', 'C-id'])
2695
wt.branch.set_last_revision_info(2, 'B-id')
2696
wt.commit('D', rev_id='D-id', recursive=None)
2698
merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
2700
merger.merge_type = _mod_merge.Merge3Merger
2701
merge_obj = merger.make_merger()
2702
entries = list(merge_obj._entries_lca())
2703
root_id = 'a-root-id'
2704
self.assertEqual([('sub-tree-root', False,
2705
((root_id, [root_id, root_id]), root_id, root_id),
2706
((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
2707
((False, [False, False]), False, False)),
2711
class TestLCAMultiWay(tests.TestCase):
2713
def assertLCAMultiWay(self, expected, base, lcas, other, this,
2714
allow_overriding_lca=True):
2715
self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
2716
(base, lcas), other, this,
2717
allow_overriding_lca=allow_overriding_lca))
2719
def test_other_equal_equal_lcas(self):
2720
"""Test when OTHER=LCA and all LCAs are identical."""
2721
self.assertLCAMultiWay('this',
2722
'bval', ['bval', 'bval'], 'bval', 'bval')
2723
self.assertLCAMultiWay('this',
2724
'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
2725
self.assertLCAMultiWay('this',
2726
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
2727
self.assertLCAMultiWay('this',
2728
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
2729
self.assertLCAMultiWay('this',
2730
'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
2732
def test_other_equal_this(self):
2733
"""Test when other and this are identical."""
2734
self.assertLCAMultiWay('this',
2735
'bval', ['bval', 'bval'], 'oval', 'oval')
2736
self.assertLCAMultiWay('this',
2737
'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
2738
self.assertLCAMultiWay('this',
2739
'bval', ['cval', 'dval'], 'oval', 'oval')
2740
self.assertLCAMultiWay('this',
2741
'bval', [None, 'lcaval'], 'oval', 'oval')
2742
self.assertLCAMultiWay('this',
2743
None, [None, 'lcaval'], 'oval', 'oval')
2744
self.assertLCAMultiWay('this',
2745
None, ['lcaval', 'lcaval'], 'oval', 'oval')
2746
self.assertLCAMultiWay('this',
2747
None, ['cval', 'dval'], 'oval', 'oval')
2748
self.assertLCAMultiWay('this',
2749
None, ['cval', 'dval'], None, None)
2750
self.assertLCAMultiWay('this',
2751
None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
2753
def test_no_lcas(self):
2754
self.assertLCAMultiWay('this',
2755
'bval', [], 'bval', 'tval')
2756
self.assertLCAMultiWay('other',
2757
'bval', [], 'oval', 'bval')
2758
self.assertLCAMultiWay('conflict',
2759
'bval', [], 'oval', 'tval')
2760
self.assertLCAMultiWay('this',
2761
'bval', [], 'oval', 'oval')
2763
def test_lca_supersedes_other_lca(self):
2764
"""If one lca == base, the other lca takes precedence"""
2765
self.assertLCAMultiWay('this',
2766
'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
2767
self.assertLCAMultiWay('this',
2768
'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
2769
# This is actually considered a 'revert' because the 'lcaval' in LCAS
2770
# supersedes the BASE val (in the other LCA) but then OTHER reverts it
2772
self.assertLCAMultiWay('other',
2773
'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
2774
self.assertLCAMultiWay('conflict',
2775
'bval', ['bval', 'lcaval'], 'bval', 'tval')
2777
def test_other_and_this_pick_different_lca(self):
2778
# OTHER and THIS resolve the lca conflict in different ways
2779
self.assertLCAMultiWay('conflict',
2780
'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
2781
self.assertLCAMultiWay('conflict',
2782
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
2783
self.assertLCAMultiWay('conflict',
2784
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
2786
def test_other_in_lca(self):
2787
# OTHER takes a value of one of the LCAs, THIS takes a new value, which
2788
# theoretically supersedes both LCA values and 'wins'
2789
self.assertLCAMultiWay('this',
2790
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
2791
self.assertLCAMultiWay('this',
2792
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval')
2793
self.assertLCAMultiWay('conflict',
2794
'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval',
2795
allow_overriding_lca=False)
2796
self.assertLCAMultiWay('conflict',
2797
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval',
2798
allow_overriding_lca=False)
2799
# THIS reverted back to BASE, but that is an explicit supersede of all
2801
self.assertLCAMultiWay('this',
2802
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval')
2803
self.assertLCAMultiWay('this',
2804
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
2805
self.assertLCAMultiWay('conflict',
2806
'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval',
2807
allow_overriding_lca=False)
2808
self.assertLCAMultiWay('conflict',
2809
'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval',
2810
allow_overriding_lca=False)
2812
def test_this_in_lca(self):
2813
# THIS takes a value of one of the LCAs, OTHER takes a new value, which
2814
# theoretically supersedes both LCA values and 'wins'
2815
self.assertLCAMultiWay('other',
2816
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
2817
self.assertLCAMultiWay('other',
2818
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
2819
self.assertLCAMultiWay('conflict',
2820
'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val',
2821
allow_overriding_lca=False)
2822
self.assertLCAMultiWay('conflict',
2823
'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val',
2824
allow_overriding_lca=False)
2825
# OTHER reverted back to BASE, but that is an explicit supersede of all
2827
self.assertLCAMultiWay('other',
2828
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val')
2829
self.assertLCAMultiWay('conflict',
2830
'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val',
2831
allow_overriding_lca=False)
2833
def test_all_differ(self):
2834
self.assertLCAMultiWay('conflict',
2835
'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
2836
self.assertLCAMultiWay('conflict',
2837
'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval', 'tval')
2838
self.assertLCAMultiWay('conflict',
2839
'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval', 'tval')
2842
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
2844
def test_affected_files_cached(self):
2845
"""Ensures that the config variable is cached"""
2846
class SimplePlan(_mod_merge.ConfigurableFileMerger):
2848
default_files = ["my default"]
2849
def merge_text(self, params):
2850
return ('not applicable', None)
2851
def factory(merger):
2852
result = SimplePlan(merger)
2853
self.assertEqual(None, result.affected_files)
2854
self.merger = result
2856
_mod_merge.Merger.hooks.install_named_hook('merge_file_content',
2857
factory, 'test factory')
2858
builder = test_merge_core.MergeBuilder(self.test_base_dir)
2859
self.addCleanup(builder.cleanup)
2860
builder.add_file('NEWS', builder.tree_root, 'name1', 'text1', True)
2861
builder.change_contents('NEWS', other='text4', this='text3')
2862
conflicts = builder.merge()
2863
# The hook should set the variable
2864
self.assertEqual(["my default"], self.merger.affected_files)