/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_merge.py

  • Committer: mernst at mit
  • Date: 2008-10-16 10:57:16 UTC
  • mto: This revision was merged to the branch mainline in revision 3799.
  • Revision ID: mernst@csail.mit.edu-20081016105716-v8x8n5t2pf7f6uds
Improved documentation of stacked and lightweight branches

These patches improve the User Guide's documentation of stacked and
lightweight branches.

Section "1.2.6 Putting the concepts together" should mention stacked
branches and the difference between them and lightweight branches.  It
should also contain links to further details of the common scenarios.

Section "5.3.4 Getting a lightweight checkout" should mention stacked
branches as an option, and should link to all the options, not just some of
them.  It should also clarify that lightweight only applies to checkouts,
not to arbitrary branches.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
from StringIO import StringIO
 
19
 
 
20
from bzrlib import (
 
21
    conflicts,
 
22
    errors,
 
23
    knit,
 
24
    memorytree,
 
25
    merge as _mod_merge,
 
26
    option,
 
27
    progress,
 
28
    tests,
 
29
    transform,
 
30
    versionedfile,
 
31
    )
 
32
from bzrlib.branch import Branch
 
33
from bzrlib.conflicts import ConflictList, TextConflict
 
34
from bzrlib.errors import UnrelatedBranches, NoCommits, BzrCommandError
 
35
from bzrlib.merge import transform_tree, merge_inner, _PlanMerge
 
36
from bzrlib.osutils import pathjoin, file_kind
 
37
from bzrlib.tests import TestCaseWithTransport, TestCaseWithMemoryTransport
 
38
from bzrlib.trace import (enable_test_log, disable_test_log)
 
39
from bzrlib.workingtree import WorkingTree
 
40
 
 
41
 
 
42
class TestMerge(TestCaseWithTransport):
 
43
    """Test appending more than one revision"""
 
44
 
 
45
    def test_pending(self):
 
46
        wt = self.make_branch_and_tree('.')
 
47
        rev_a = wt.commit("lala!")
 
48
        self.assertEqual([rev_a], wt.get_parent_ids())
 
49
        self.assertRaises(errors.PointlessMerge, wt.merge_from_branch,
 
50
                          wt.branch)
 
51
        self.assertEqual([rev_a], wt.get_parent_ids())
 
52
        return wt
 
53
 
 
54
    def test_undo(self):
 
55
        wt = self.make_branch_and_tree('.')
 
56
        wt.commit("lala!")
 
57
        wt.commit("haha!")
 
58
        wt.commit("blabla!")
 
59
        wt.merge_from_branch(wt.branch, wt.branch.get_rev_id(2),
 
60
                             wt.branch.get_rev_id(1))
 
61
 
 
62
    def test_nocommits(self):
 
63
        wt = self.test_pending()
 
64
        wt2 = self.make_branch_and_tree('branch2')
 
65
        self.assertRaises(NoCommits, wt.merge_from_branch, wt2.branch)
 
66
        return wt, wt2
 
67
 
 
68
    def test_unrelated(self):
 
69
        wt, wt2 = self.test_nocommits()
 
70
        wt2.commit("blah")
 
71
        self.assertRaises(UnrelatedBranches, wt.merge_from_branch, wt2.branch)
 
72
        return wt2
 
73
 
 
74
    def test_merge_one_file(self):
 
75
        """Do a partial merge of a tree which should not affect tree parents."""
 
76
        wt1 = self.make_branch_and_tree('branch1')
 
77
        tip = wt1.commit('empty commit')
 
78
        wt2 = self.make_branch_and_tree('branch2')
 
79
        wt2.pull(wt1.branch)
 
80
        file('branch1/foo', 'wb').write('foo')
 
81
        file('branch1/bar', 'wb').write('bar')
 
82
        wt1.add('foo')
 
83
        wt1.add('bar')
 
84
        wt1.commit('add foobar')
 
85
        os.chdir('branch2')
 
86
        self.run_bzr('merge ../branch1/baz', retcode=3)
 
87
        self.run_bzr('merge ../branch1/foo')
 
88
        self.failUnlessExists('foo')
 
89
        self.failIfExists('bar')
 
90
        wt2 = WorkingTree.open('.') # opens branch2
 
91
        self.assertEqual([tip], wt2.get_parent_ids())
 
92
        
 
93
    def test_pending_with_null(self):
 
94
        """When base is forced to revno 0, parent_ids are set"""
 
95
        wt2 = self.test_unrelated()
 
96
        wt1 = WorkingTree.open('.')
 
97
        br1 = wt1.branch
 
98
        br1.fetch(wt2.branch)
 
99
        # merge all of branch 2 into branch 1 even though they 
 
100
        # are not related.
 
101
        wt1.merge_from_branch(wt2.branch, wt2.last_revision(), 'null:')
 
102
        self.assertEqual([br1.last_revision(), wt2.branch.last_revision()],
 
103
            wt1.get_parent_ids())
 
104
        return (wt1, wt2.branch)
 
105
 
 
106
    def test_two_roots(self):
 
107
        """Merge base is sane when two unrelated branches are merged"""
 
108
        wt1, br2 = self.test_pending_with_null()
 
109
        wt1.commit("blah")
 
110
        wt1.lock_read()
 
111
        try:
 
112
            last = wt1.branch.last_revision()
 
113
            last2 = br2.last_revision()
 
114
            graph = wt1.branch.repository.get_graph()
 
115
            self.assertEqual(last2, graph.find_unique_lca(last, last2))
 
116
        finally:
 
117
            wt1.unlock()
 
118
 
 
119
    def test_create_rename(self):
 
120
        """Rename an inventory entry while creating the file"""
 
121
        tree =self.make_branch_and_tree('.')
 
122
        file('name1', 'wb').write('Hello')
 
123
        tree.add('name1')
 
124
        tree.commit(message="hello")
 
125
        tree.rename_one('name1', 'name2')
 
126
        os.unlink('name2')
 
127
        transform_tree(tree, tree.branch.basis_tree())
 
128
 
 
129
    def test_layered_rename(self):
 
130
        """Rename both child and parent at same time"""
 
131
        tree =self.make_branch_and_tree('.')
 
132
        os.mkdir('dirname1')
 
133
        tree.add('dirname1')
 
134
        filename = pathjoin('dirname1', 'name1')
 
135
        file(filename, 'wb').write('Hello')
 
136
        tree.add(filename)
 
137
        tree.commit(message="hello")
 
138
        filename2 = pathjoin('dirname1', 'name2')
 
139
        tree.rename_one(filename, filename2)
 
140
        tree.rename_one('dirname1', 'dirname2')
 
141
        transform_tree(tree, tree.branch.basis_tree())
 
142
 
 
143
    def test_ignore_zero_merge_inner(self):
 
144
        # Test that merge_inner's ignore zero parameter is effective
 
145
        tree_a =self.make_branch_and_tree('a')
 
146
        tree_a.commit(message="hello")
 
147
        dir_b = tree_a.bzrdir.sprout('b')
 
148
        tree_b = dir_b.open_workingtree()
 
149
        tree_b.lock_write()
 
150
        self.addCleanup(tree_b.unlock)
 
151
        tree_a.commit(message="hello again")
 
152
        log = StringIO()
 
153
        merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(), 
 
154
                    this_tree=tree_b, ignore_zero=True)
 
155
        log = self._get_log(keep_log_file=True)
 
156
        self.failUnless('All changes applied successfully.\n' not in log)
 
157
        tree_b.revert()
 
158
        merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(), 
 
159
                    this_tree=tree_b, ignore_zero=False)
 
160
        log = self._get_log(keep_log_file=True)
 
161
        self.failUnless('All changes applied successfully.\n' in log)
 
162
 
 
163
    def test_merge_inner_conflicts(self):
 
164
        tree_a = self.make_branch_and_tree('a')
 
165
        tree_a.set_conflicts(ConflictList([TextConflict('patha')]))
 
166
        merge_inner(tree_a.branch, tree_a, tree_a, this_tree=tree_a)
 
167
        self.assertEqual(1, len(tree_a.conflicts()))
 
168
 
 
169
    def test_rmdir_conflict(self):
 
170
        tree_a = self.make_branch_and_tree('a')
 
171
        self.build_tree(['a/b/'])
 
172
        tree_a.add('b', 'b-id')
 
173
        tree_a.commit('added b')
 
174
        # basis_tree() is only guaranteed to be valid as long as it is actually
 
175
        # the basis tree. This mutates the tree after grabbing basis, so go to
 
176
        # the repository.
 
177
        base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
 
178
        tree_z = tree_a.bzrdir.sprout('z').open_workingtree()
 
179
        self.build_tree(['a/b/c'])
 
180
        tree_a.add('b/c')
 
181
        tree_a.commit('added c')
 
182
        os.rmdir('z/b')
 
183
        tree_z.commit('removed b')
 
184
        merge_inner(tree_z.branch, tree_a, base_tree, this_tree=tree_z)
 
185
        self.assertEqual([
 
186
            conflicts.MissingParent('Created directory', 'b', 'b-id'),
 
187
            conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
 
188
            tree_z.conflicts())
 
189
        merge_inner(tree_a.branch, tree_z.basis_tree(), base_tree,
 
190
                    this_tree=tree_a)
 
191
        self.assertEqual([
 
192
            conflicts.DeletingParent('Not deleting', 'b', 'b-id'),
 
193
            conflicts.UnversionedParent('Versioned directory', 'b', 'b-id')],
 
194
            tree_a.conflicts())
 
195
 
 
196
    def test_nested_merge(self):
 
197
        tree = self.make_branch_and_tree('tree',
 
198
            format='dirstate-with-subtree')
 
199
        sub_tree = self.make_branch_and_tree('tree/sub-tree',
 
200
            format='dirstate-with-subtree')
 
201
        sub_tree.set_root_id('sub-tree-root')
 
202
        self.build_tree_contents([('tree/sub-tree/file', 'text1')])
 
203
        sub_tree.add('file')
 
204
        sub_tree.commit('foo')
 
205
        tree.add_reference(sub_tree)
 
206
        tree.commit('set text to 1')
 
207
        tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
 
208
        # modify the file in the subtree
 
209
        self.build_tree_contents([('tree2/sub-tree/file', 'text2')])
 
210
        # and merge the changes from the diverged subtree into the containing
 
211
        # tree
 
212
        tree2.commit('changed file text')
 
213
        tree.merge_from_branch(tree2.branch)
 
214
        self.assertFileEqual('text2', 'tree/sub-tree/file')
 
215
 
 
216
    def test_merge_with_missing(self):
 
217
        tree_a = self.make_branch_and_tree('tree_a')
 
218
        self.build_tree_contents([('tree_a/file', 'content_1')])
 
219
        tree_a.add('file')
 
220
        tree_a.commit('commit base')
 
221
        # basis_tree() is only guaranteed to be valid as long as it is actually
 
222
        # the basis tree. This mutates the tree after grabbing basis, so go to
 
223
        # the repository.
 
224
        base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
 
225
        tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
 
226
        self.build_tree_contents([('tree_a/file', 'content_2')])
 
227
        tree_a.commit('commit other')
 
228
        other_tree = tree_a.basis_tree()
 
229
        os.unlink('tree_b/file')
 
230
        merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
 
231
 
 
232
    def test_merge_kind_change(self):
 
233
        tree_a = self.make_branch_and_tree('tree_a')
 
234
        self.build_tree_contents([('tree_a/file', 'content_1')])
 
235
        tree_a.add('file', 'file-id')
 
236
        tree_a.commit('added file')
 
237
        tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
 
238
        os.unlink('tree_a/file')
 
239
        self.build_tree(['tree_a/file/'])
 
240
        tree_a.commit('changed file to directory')
 
241
        tree_b.merge_from_branch(tree_a.branch)
 
242
        self.assertEqual('directory', file_kind('tree_b/file'))
 
243
        tree_b.revert()
 
244
        self.assertEqual('file', file_kind('tree_b/file'))
 
245
        self.build_tree_contents([('tree_b/file', 'content_2')])
 
246
        tree_b.commit('content change')
 
247
        tree_b.merge_from_branch(tree_a.branch)
 
248
        self.assertEqual(tree_b.conflicts(),
 
249
                         [conflicts.ContentsConflict('file',
 
250
                          file_id='file-id')])
 
251
    
 
252
    def test_merge_type_registry(self):
 
253
        merge_type_option = option.Option.OPTIONS['merge-type']
 
254
        self.assertFalse('merge4' in [x[0] for x in 
 
255
                        merge_type_option.iter_switches()])
 
256
        registry = _mod_merge.get_merge_type_registry()
 
257
        registry.register_lazy('merge4', 'bzrlib.merge', 'Merge4Merger',
 
258
                               'time-travelling merge')
 
259
        self.assertTrue('merge4' in [x[0] for x in 
 
260
                        merge_type_option.iter_switches()])
 
261
        registry.remove('merge4')
 
262
        self.assertFalse('merge4' in [x[0] for x in 
 
263
                        merge_type_option.iter_switches()])
 
264
 
 
265
    def test_merge_other_moves_we_deleted(self):
 
266
        tree_a = self.make_branch_and_tree('A')
 
267
        tree_a.lock_write()
 
268
        self.addCleanup(tree_a.unlock)
 
269
        self.build_tree(['A/a'])
 
270
        tree_a.add('a')
 
271
        tree_a.commit('1', rev_id='rev-1')
 
272
        tree_a.flush()
 
273
        tree_a.rename_one('a', 'b')
 
274
        tree_a.commit('2')
 
275
        bzrdir_b = tree_a.bzrdir.sprout('B', revision_id='rev-1')
 
276
        tree_b = bzrdir_b.open_workingtree()
 
277
        tree_b.lock_write()
 
278
        self.addCleanup(tree_b.unlock)
 
279
        os.unlink('B/a')
 
280
        tree_b.commit('3')
 
281
        try:
 
282
            tree_b.merge_from_branch(tree_a.branch)
 
283
        except AttributeError:
 
284
            self.fail('tried to join a path when name was None')
 
285
 
 
286
    def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis(self):
 
287
        tree_a = self.make_branch_and_tree('a')
 
288
        self.build_tree(['a/file_1', 'a/file_2'])
 
289
        tree_a.add(['file_1'])
 
290
        tree_a.commit('commit 1')
 
291
        tree_a.add(['file_2'])
 
292
        tree_a.commit('commit 2')
 
293
        tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
 
294
        tree_b.rename_one('file_1', 'renamed')
 
295
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
 
296
                                                    progress.DummyProgress())
 
297
        merger.merge_type = _mod_merge.Merge3Merger
 
298
        merger.do_merge()
 
299
        self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
 
300
 
 
301
    def test_merge_uncommitted_otherbasis_ancestor_of_thisbasis_weave(self):
 
302
        tree_a = self.make_branch_and_tree('a')
 
303
        self.build_tree(['a/file_1', 'a/file_2'])
 
304
        tree_a.add(['file_1'])
 
305
        tree_a.commit('commit 1')
 
306
        tree_a.add(['file_2'])
 
307
        tree_a.commit('commit 2')
 
308
        tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
 
309
        tree_b.rename_one('file_1', 'renamed')
 
310
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
 
311
                                                    progress.DummyProgress())
 
312
        merger.merge_type = _mod_merge.WeaveMerger
 
313
        merger.do_merge()
 
314
        self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
 
315
 
 
316
    def prepare_cherrypick(self):
 
317
        """Prepare a pair of trees for cherrypicking tests.
 
318
 
 
319
        Both trees have a file, 'file'.
 
320
        rev1 sets content to 'a'.
 
321
        rev2b adds 'b'.
 
322
        rev3b adds 'c'.
 
323
        A full merge of rev2b and rev3b into this_tree would add both 'b' and
 
324
        'c'.  A successful cherrypick of rev2b-rev3b into this_tree will add
 
325
        'c', but not 'b'.
 
326
        """
 
327
        this_tree = self.make_branch_and_tree('this')
 
328
        self.build_tree_contents([('this/file', "a\n")])
 
329
        this_tree.add('file')
 
330
        this_tree.commit('rev1')
 
331
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
332
        self.build_tree_contents([('other/file', "a\nb\n")])
 
333
        other_tree.commit('rev2b', rev_id='rev2b')
 
334
        self.build_tree_contents([('other/file', "c\na\nb\n")])
 
335
        other_tree.commit('rev3b', rev_id='rev3b')
 
336
        this_tree.lock_write()
 
337
        self.addCleanup(this_tree.unlock)
 
338
        return this_tree, other_tree
 
339
 
 
340
    def test_weave_cherrypick(self):
 
341
        this_tree, other_tree = self.prepare_cherrypick()
 
342
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
343
            this_tree, 'rev3b', 'rev2b', other_tree.branch)
 
344
        merger.merge_type = _mod_merge.WeaveMerger
 
345
        merger.do_merge()
 
346
        self.assertFileEqual('c\na\n', 'this/file')
 
347
 
 
348
    def test_weave_cannot_reverse_cherrypick(self):
 
349
        this_tree, other_tree = self.prepare_cherrypick()
 
350
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
351
            this_tree, 'rev2b', 'rev3b', other_tree.branch)
 
352
        merger.merge_type = _mod_merge.WeaveMerger
 
353
        self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
 
354
 
 
355
    def test_merge3_can_reverse_cherrypick(self):
 
356
        this_tree, other_tree = self.prepare_cherrypick()
 
357
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
358
            this_tree, 'rev2b', 'rev3b', other_tree.branch)
 
359
        merger.merge_type = _mod_merge.Merge3Merger
 
360
        merger.do_merge()
 
361
 
 
362
    def test_merge3_will_detect_cherrypick(self):
 
363
        this_tree = self.make_branch_and_tree('this')
 
364
        self.build_tree_contents([('this/file', "a\n")])
 
365
        this_tree.add('file')
 
366
        this_tree.commit('rev1')
 
367
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
368
        self.build_tree_contents([('other/file', "a\nb\n")])
 
369
        other_tree.commit('rev2b', rev_id='rev2b')
 
370
        self.build_tree_contents([('other/file', "a\nb\nc\n")])
 
371
        other_tree.commit('rev3b', rev_id='rev3b')
 
372
        this_tree.lock_write()
 
373
        self.addCleanup(this_tree.unlock)
 
374
 
 
375
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
376
            this_tree, 'rev3b', 'rev2b', other_tree.branch)
 
377
        merger.merge_type = _mod_merge.Merge3Merger
 
378
        merger.do_merge()
 
379
        self.assertFileEqual('a\n'
 
380
                             '<<<<<<< TREE\n'
 
381
                             '=======\n'
 
382
                             'c\n'
 
383
                             '>>>>>>> MERGE-SOURCE\n',
 
384
                             'this/file')
 
385
 
 
386
    def test_make_merger(self):
 
387
        this_tree = self.make_branch_and_tree('this')
 
388
        this_tree.commit('rev1', rev_id='rev1')
 
389
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
390
        this_tree.commit('rev2', rev_id='rev2a')
 
391
        other_tree.commit('rev2', rev_id='rev2b')
 
392
        this_tree.lock_write()
 
393
        self.addCleanup(this_tree.unlock)
 
394
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress,
 
395
            this_tree, 'rev2b', other_branch=other_tree.branch)
 
396
        merger.merge_type = _mod_merge.Merge3Merger
 
397
        tree_merger = merger.make_merger()
 
398
        self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
 
399
        self.assertEqual('rev2b', tree_merger.other_tree.get_revision_id())
 
400
        self.assertEqual('rev1', tree_merger.base_tree.get_revision_id())
 
401
 
 
402
    def test_make_preview_transform(self):
 
403
        this_tree = self.make_branch_and_tree('this')
 
404
        self.build_tree_contents([('this/file', '1\n')])
 
405
        this_tree.add('file', 'file-id')
 
406
        this_tree.commit('rev1', rev_id='rev1')
 
407
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
408
        self.build_tree_contents([('this/file', '1\n2a\n')])
 
409
        this_tree.commit('rev2', rev_id='rev2a')
 
410
        self.build_tree_contents([('other/file', '2b\n1\n')])
 
411
        other_tree.commit('rev2', rev_id='rev2b')
 
412
        this_tree.lock_write()
 
413
        self.addCleanup(this_tree.unlock)
 
414
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
415
            this_tree, 'rev2b', other_branch=other_tree.branch)
 
416
        merger.merge_type = _mod_merge.Merge3Merger
 
417
        tree_merger = merger.make_merger()
 
418
        tt = tree_merger.make_preview_transform()
 
419
        self.addCleanup(tt.finalize)
 
420
        preview_tree = tt.get_preview_tree()
 
421
        tree_file = this_tree.get_file('file-id')
 
422
        try:
 
423
            self.assertEqual('1\n2a\n', tree_file.read())
 
424
        finally:
 
425
            tree_file.close()
 
426
        preview_file = preview_tree.get_file('file-id')
 
427
        try:
 
428
            self.assertEqual('2b\n1\n2a\n', preview_file.read())
 
429
        finally:
 
430
            preview_file.close()
 
431
 
 
432
    def test_do_merge(self):
 
433
        this_tree = self.make_branch_and_tree('this')
 
434
        self.build_tree_contents([('this/file', '1\n')])
 
435
        this_tree.add('file', 'file-id')
 
436
        this_tree.commit('rev1', rev_id='rev1')
 
437
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
438
        self.build_tree_contents([('this/file', '1\n2a\n')])
 
439
        this_tree.commit('rev2', rev_id='rev2a')
 
440
        self.build_tree_contents([('other/file', '2b\n1\n')])
 
441
        other_tree.commit('rev2', rev_id='rev2b')
 
442
        this_tree.lock_write()
 
443
        self.addCleanup(this_tree.unlock)
 
444
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
445
            this_tree, 'rev2b', other_branch=other_tree.branch)
 
446
        merger.merge_type = _mod_merge.Merge3Merger
 
447
        tree_merger = merger.make_merger()
 
448
        tt = tree_merger.do_merge()
 
449
        tree_file = this_tree.get_file('file-id')
 
450
        try:
 
451
            self.assertEqual('2b\n1\n2a\n', tree_file.read())
 
452
        finally:
 
453
            tree_file.close()
 
454
 
 
455
    def test_merge_add_into_deleted_root(self):
 
456
        # Yes, people actually do this.  And report bugs if it breaks.
 
457
        source = self.make_branch_and_tree('source', format='rich-root-pack')
 
458
        self.build_tree(['source/foo/'])
 
459
        source.add('foo', 'foo-id')
 
460
        source.commit('Add foo')
 
461
        target = source.bzrdir.sprout('target').open_workingtree()
 
462
        subtree = target.extract('foo-id')
 
463
        subtree.commit('Delete root')
 
464
        self.build_tree(['source/bar'])
 
465
        source.add('bar', 'bar-id')
 
466
        source.commit('Add bar')
 
467
        subtree.merge_from_branch(source.branch)
 
468
 
 
469
    def test_merge_joined_branch(self):
 
470
        source = self.make_branch_and_tree('source', format='rich-root-pack')
 
471
        self.build_tree(['source/foo'])
 
472
        source.add('foo')
 
473
        source.commit('Add foo')
 
474
        target = self.make_branch_and_tree('target', format='rich-root-pack')
 
475
        self.build_tree(['target/bla'])
 
476
        target.add('bla')
 
477
        target.commit('Add bla')
 
478
        nested = source.bzrdir.sprout('target/subtree').open_workingtree()
 
479
        target.subsume(nested)
 
480
        target.commit('Join nested')
 
481
        self.build_tree(['source/bar'])
 
482
        source.add('bar')
 
483
        source.commit('Add bar')
 
484
        target.merge_from_branch(source.branch)
 
485
        target.commit('Merge source')
 
486
 
 
487
 
 
488
class TestPlanMerge(TestCaseWithMemoryTransport):
 
489
 
 
490
    def setUp(self):
 
491
        TestCaseWithMemoryTransport.setUp(self)
 
492
        mapper = versionedfile.PrefixMapper()
 
493
        factory = knit.make_file_factory(True, mapper)
 
494
        self.vf = factory(self.get_transport())
 
495
        self.plan_merge_vf = versionedfile._PlanMergeVersionedFile('root')
 
496
        self.plan_merge_vf.fallback_versionedfiles.append(self.vf)
 
497
 
 
498
    def add_version(self, key, parents, text):
 
499
        self.vf.add_lines(key, parents, [c+'\n' for c in text])
 
500
 
 
501
    def add_rev(self, prefix, revision_id, parents, text):
 
502
        self.add_version((prefix, revision_id), [(prefix, p) for p in parents],
 
503
                         text)
 
504
 
 
505
    def add_uncommitted_version(self, key, parents, text):
 
506
        self.plan_merge_vf.add_lines(key, parents,
 
507
                                     [c+'\n' for c in text])
 
508
 
 
509
    def setup_plan_merge(self):
 
510
        self.add_rev('root', 'A', [], 'abc')
 
511
        self.add_rev('root', 'B', ['A'], 'acehg')
 
512
        self.add_rev('root', 'C', ['A'], 'fabg')
 
513
        return _PlanMerge('B', 'C', self.plan_merge_vf, ('root',))
 
514
 
 
515
    def setup_plan_merge_uncommitted(self):
 
516
        self.add_version(('root', 'A'), [], 'abc')
 
517
        self.add_uncommitted_version(('root', 'B:'), [('root', 'A')], 'acehg')
 
518
        self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')
 
519
        return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))
 
520
 
 
521
    def test_unique_lines(self):
 
522
        plan = self.setup_plan_merge()
 
523
        self.assertEqual(plan._unique_lines(
 
524
            plan._get_matching_blocks('B', 'C')),
 
525
            ([1, 2, 3], [0, 2]))
 
526
 
 
527
    def test_plan_merge(self):
 
528
        self.setup_plan_merge()
 
529
        plan = self.plan_merge_vf.plan_merge('B', 'C')
 
530
        self.assertEqual([
 
531
                          ('new-b', 'f\n'),
 
532
                          ('unchanged', 'a\n'),
 
533
                          ('killed-a', 'b\n'),
 
534
                          ('killed-b', 'c\n'),
 
535
                          ('new-a', 'e\n'),
 
536
                          ('new-a', 'h\n'),
 
537
                          ('new-a', 'g\n'),
 
538
                          ('new-b', 'g\n')],
 
539
                         list(plan))
 
540
 
 
541
    def test_plan_merge_cherrypick(self):
 
542
        self.add_rev('root', 'A', [], 'abc')
 
543
        self.add_rev('root', 'B', ['A'], 'abcde')
 
544
        self.add_rev('root', 'C', ['A'], 'abcefg')
 
545
        self.add_rev('root', 'D', ['A', 'B', 'C'], 'abcdegh')
 
546
        my_plan = _PlanMerge('B', 'D', self.plan_merge_vf, ('root',))
 
547
        # We shortcut when one text supersedes the other in the per-file graph.
 
548
        # We don't actually need to compare the texts at this point.
 
549
        self.assertEqual([
 
550
                          ('new-b', 'a\n'),
 
551
                          ('new-b', 'b\n'),
 
552
                          ('new-b', 'c\n'),
 
553
                          ('new-b', 'd\n'),
 
554
                          ('new-b', 'e\n'),
 
555
                          ('new-b', 'g\n'),
 
556
                          ('new-b', 'h\n')],
 
557
                          list(my_plan.plan_merge()))
 
558
 
 
559
    def test_plan_merge_no_common_ancestor(self):
 
560
        self.add_rev('root', 'A', [], 'abc')
 
561
        self.add_rev('root', 'B', [], 'xyz')
 
562
        my_plan = _PlanMerge('A', 'B', self.plan_merge_vf, ('root',))
 
563
        self.assertEqual([
 
564
                          ('new-a', 'a\n'),
 
565
                          ('new-a', 'b\n'),
 
566
                          ('new-a', 'c\n'),
 
567
                          ('new-b', 'x\n'),
 
568
                          ('new-b', 'y\n'),
 
569
                          ('new-b', 'z\n')],
 
570
                          list(my_plan.plan_merge()))
 
571
 
 
572
    def test_plan_merge_tail_ancestors(self):
 
573
        # The graph looks like this:
 
574
        #       A       # Common to all ancestors
 
575
        #      / \
 
576
        #     B   C     # Ancestors of E, only common to one side
 
577
        #     |\ /|
 
578
        #     D E F     # D, F are unique to G, H respectively
 
579
        #     |/ \|     # E is the LCA for G & H, and the unique LCA for
 
580
        #     G   H     # I, J
 
581
        #     |\ /|
 
582
        #     | X |
 
583
        #     |/ \|
 
584
        #     I   J     # criss-cross merge of G, H
 
585
        #
 
586
        # In this situation, a simple pruning of ancestors of E will leave D &
 
587
        # F "dangling", which looks like they introduce lines different from
 
588
        # the ones in E, but in actuality C&B introduced the lines, and they
 
589
        # are already present in E
 
590
 
 
591
        # Introduce the base text
 
592
        self.add_rev('root', 'A', [], 'abc')
 
593
        # Introduces a new line B
 
594
        self.add_rev('root', 'B', ['A'], 'aBbc')
 
595
        # Introduces a new line C
 
596
        self.add_rev('root', 'C', ['A'], 'abCc')
 
597
        # Introduce new line D
 
598
        self.add_rev('root', 'D', ['B'], 'DaBbc')
 
599
        # Merges B and C by just incorporating both
 
600
        self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
 
601
        # Introduce new line F
 
602
        self.add_rev('root', 'F', ['C'], 'abCcF')
 
603
        # Merge D & E by just combining the texts
 
604
        self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
 
605
        # Merge F & E by just combining the texts
 
606
        self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
 
607
        # Merge G & H by just combining texts
 
608
        self.add_rev('root', 'I', ['G', 'H'], 'DaBbCcF')
 
609
        # Merge G & H but supersede an old line in B
 
610
        self.add_rev('root', 'J', ['H', 'G'], 'DaJbCcF')
 
611
        plan = self.plan_merge_vf.plan_merge('I', 'J')
 
612
        self.assertEqual([
 
613
                          ('unchanged', 'D\n'),
 
614
                          ('unchanged', 'a\n'),
 
615
                          ('killed-b', 'B\n'),
 
616
                          ('new-b', 'J\n'),
 
617
                          ('unchanged', 'b\n'),
 
618
                          ('unchanged', 'C\n'),
 
619
                          ('unchanged', 'c\n'),
 
620
                          ('unchanged', 'F\n')],
 
621
                         list(plan))
 
622
 
 
623
    def test_plan_merge_tail_triple_ancestors(self):
 
624
        # The graph looks like this:
 
625
        #       A       # Common to all ancestors
 
626
        #      / \
 
627
        #     B   C     # Ancestors of E, only common to one side
 
628
        #     |\ /|
 
629
        #     D E F     # D, F are unique to G, H respectively
 
630
        #     |/|\|     # E is the LCA for G & H, and the unique LCA for
 
631
        #     G Q H     # I, J
 
632
        #     |\ /|     # Q is just an extra node which is merged into both
 
633
        #     | X |     # I and J
 
634
        #     |/ \|
 
635
        #     I   J     # criss-cross merge of G, H
 
636
        #
 
637
        # This is the same as the test_plan_merge_tail_ancestors, except we add
 
638
        # a third LCA that doesn't add new lines, but will trigger our more
 
639
        # involved ancestry logic
 
640
 
 
641
        self.add_rev('root', 'A', [], 'abc')
 
642
        self.add_rev('root', 'B', ['A'], 'aBbc')
 
643
        self.add_rev('root', 'C', ['A'], 'abCc')
 
644
        self.add_rev('root', 'D', ['B'], 'DaBbc')
 
645
        self.add_rev('root', 'E', ['B', 'C'], 'aBbCc')
 
646
        self.add_rev('root', 'F', ['C'], 'abCcF')
 
647
        self.add_rev('root', 'G', ['D', 'E'], 'DaBbCc')
 
648
        self.add_rev('root', 'H', ['F', 'E'], 'aBbCcF')
 
649
        self.add_rev('root', 'Q', ['E'], 'aBbCc')
 
650
        self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DaBbCcF')
 
651
        # Merge G & H but supersede an old line in B
 
652
        self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DaJbCcF')
 
653
        plan = self.plan_merge_vf.plan_merge('I', 'J')
 
654
        self.assertEqual([
 
655
                          ('unchanged', 'D\n'),
 
656
                          ('unchanged', 'a\n'),
 
657
                          ('killed-b', 'B\n'),
 
658
                          ('new-b', 'J\n'),
 
659
                          ('unchanged', 'b\n'),
 
660
                          ('unchanged', 'C\n'),
 
661
                          ('unchanged', 'c\n'),
 
662
                          ('unchanged', 'F\n')],
 
663
                         list(plan))
 
664
 
 
665
    def test_plan_merge_2_tail_triple_ancestors(self):
 
666
        # The graph looks like this:
 
667
        #     A   B     # 2 tails going back to NULL
 
668
        #     |\ /|
 
669
        #     D E F     # D, is unique to G, F to H
 
670
        #     |/|\|     # E is the LCA for G & H, and the unique LCA for
 
671
        #     G Q H     # I, J
 
672
        #     |\ /|     # Q is just an extra node which is merged into both
 
673
        #     | X |     # I and J
 
674
        #     |/ \|
 
675
        #     I   J     # criss-cross merge of G, H (and Q)
 
676
        #
 
677
 
 
678
        # This is meant to test after hitting a 3-way LCA, and multiple tail
 
679
        # ancestors (only have NULL_REVISION in common)
 
680
 
 
681
        self.add_rev('root', 'A', [], 'abc')
 
682
        self.add_rev('root', 'B', [], 'def')
 
683
        self.add_rev('root', 'D', ['A'], 'Dabc')
 
684
        self.add_rev('root', 'E', ['A', 'B'], 'abcdef')
 
685
        self.add_rev('root', 'F', ['B'], 'defF')
 
686
        self.add_rev('root', 'G', ['D', 'E'], 'Dabcdef')
 
687
        self.add_rev('root', 'H', ['F', 'E'], 'abcdefF')
 
688
        self.add_rev('root', 'Q', ['E'], 'abcdef')
 
689
        self.add_rev('root', 'I', ['G', 'Q', 'H'], 'DabcdefF')
 
690
        # Merge G & H but supersede an old line in B
 
691
        self.add_rev('root', 'J', ['H', 'Q', 'G'], 'DabcdJfF')
 
692
        plan = self.plan_merge_vf.plan_merge('I', 'J')
 
693
        self.assertEqual([
 
694
                          ('unchanged', 'D\n'),
 
695
                          ('unchanged', 'a\n'),
 
696
                          ('unchanged', 'b\n'),
 
697
                          ('unchanged', 'c\n'),
 
698
                          ('unchanged', 'd\n'),
 
699
                          ('killed-b', 'e\n'),
 
700
                          ('new-b', 'J\n'),
 
701
                          ('unchanged', 'f\n'),
 
702
                          ('unchanged', 'F\n')],
 
703
                         list(plan))
 
704
 
 
705
    def test_plan_merge_uncommitted_files(self):
 
706
        self.setup_plan_merge_uncommitted()
 
707
        plan = self.plan_merge_vf.plan_merge('B:', 'C:')
 
708
        self.assertEqual([
 
709
                          ('new-b', 'f\n'),
 
710
                          ('unchanged', 'a\n'),
 
711
                          ('killed-a', 'b\n'),
 
712
                          ('killed-b', 'c\n'),
 
713
                          ('new-a', 'e\n'),
 
714
                          ('new-a', 'h\n'),
 
715
                          ('new-a', 'g\n'),
 
716
                          ('new-b', 'g\n')],
 
717
                         list(plan))
 
718
 
 
719
    def test_plan_merge_insert_order(self):
 
720
        """Weave merges are sensitive to the order of insertion.
 
721
        
 
722
        Specifically for overlapping regions, it effects which region gets put
 
723
        'first'. And when a user resolves an overlapping merge, if they use the
 
724
        same ordering, then the lines match the parents, if they don't only
 
725
        *some* of the lines match.
 
726
        """
 
727
        self.add_rev('root', 'A', [], 'abcdef')
 
728
        self.add_rev('root', 'B', ['A'], 'abwxcdef')
 
729
        self.add_rev('root', 'C', ['A'], 'abyzcdef')
 
730
        # Merge, and resolve the conflict by adding *both* sets of lines
 
731
        # If we get the ordering wrong, these will look like new lines in D,
 
732
        # rather than carried over from B, C
 
733
        self.add_rev('root', 'D', ['B', 'C'],
 
734
                         'abwxyzcdef')
 
735
        # Supersede the lines in B and delete the lines in C, which will
 
736
        # conflict if they are treated as being in D
 
737
        self.add_rev('root', 'E', ['C', 'B'],
 
738
                         'abnocdef')
 
739
        # Same thing for the lines in C
 
740
        self.add_rev('root', 'F', ['C'], 'abpqcdef')
 
741
        plan = self.plan_merge_vf.plan_merge('D', 'E')
 
742
        self.assertEqual([
 
743
                          ('unchanged', 'a\n'),
 
744
                          ('unchanged', 'b\n'),
 
745
                          ('killed-b', 'w\n'),
 
746
                          ('killed-b', 'x\n'),
 
747
                          ('killed-b', 'y\n'),
 
748
                          ('killed-b', 'z\n'),
 
749
                          ('new-b', 'n\n'),
 
750
                          ('new-b', 'o\n'),
 
751
                          ('unchanged', 'c\n'),
 
752
                          ('unchanged', 'd\n'),
 
753
                          ('unchanged', 'e\n'),
 
754
                          ('unchanged', 'f\n')],
 
755
                         list(plan))
 
756
        plan = self.plan_merge_vf.plan_merge('E', 'D')
 
757
        # Going in the opposite direction shows the effect of the opposite plan
 
758
        self.assertEqual([
 
759
                          ('unchanged', 'a\n'),
 
760
                          ('unchanged', 'b\n'),
 
761
                          ('new-b', 'w\n'),
 
762
                          ('new-b', 'x\n'),
 
763
                          ('killed-a', 'y\n'),
 
764
                          ('killed-a', 'z\n'),
 
765
                          ('killed-both', 'w\n'),
 
766
                          ('killed-both', 'x\n'),
 
767
                          ('new-a', 'n\n'),
 
768
                          ('new-a', 'o\n'),
 
769
                          ('unchanged', 'c\n'),
 
770
                          ('unchanged', 'd\n'),
 
771
                          ('unchanged', 'e\n'),
 
772
                          ('unchanged', 'f\n')],
 
773
                         list(plan))
 
774
 
 
775
    def test_plan_merge_criss_cross(self):
 
776
        # This is specificly trying to trigger problems when using limited
 
777
        # ancestry and weaves. The ancestry graph looks like:
 
778
        #       XX      unused ancestor, should not show up in the weave
 
779
        #       |
 
780
        #       A       Unique LCA
 
781
        #       |\
 
782
        #       B \     Introduces a line 'foo'
 
783
        #      / \ \
 
784
        #     C   D E   C & D both have 'foo', E has different changes
 
785
        #     |\ /| |
 
786
        #     | X | |
 
787
        #     |/ \|/
 
788
        #     F   G      All of C, D, E are merged into F and G, so they are
 
789
        #                all common ancestors.
 
790
        #
 
791
        # The specific issue with weaves:
 
792
        #   B introduced a text ('foo') that is present in both C and D.
 
793
        #   If we do not include B (because it isn't an ancestor of E), then
 
794
        #   the A=>C and A=>D look like both sides independently introduce the
 
795
        #   text ('foo'). If F does not modify the text, it would still appear
 
796
        #   to have deleted on of the versions from C or D. If G then modifies
 
797
        #   'foo', it should appear as superseding the value in F (since it
 
798
        #   came from B), rather than conflict because of the resolution during
 
799
        #   C & D.
 
800
        self.add_rev('root', 'XX', [], 'qrs')
 
801
        self.add_rev('root', 'A', ['XX'], 'abcdef')
 
802
        self.add_rev('root', 'B', ['A'], 'axcdef')
 
803
        self.add_rev('root', 'C', ['B'], 'axcdefg')
 
804
        self.add_rev('root', 'D', ['B'], 'haxcdef')
 
805
        self.add_rev('root', 'E', ['A'], 'abcdyf')
 
806
        # Simple combining of all texts
 
807
        self.add_rev('root', 'F', ['C', 'D', 'E'], 'haxcdyfg')
 
808
        # combine and supersede 'x'
 
809
        self.add_rev('root', 'G', ['C', 'D', 'E'], 'hazcdyfg')
 
810
        plan = self.plan_merge_vf.plan_merge('F', 'G')
 
811
        self.assertEqual([
 
812
                          ('unchanged', 'h\n'),
 
813
                          ('unchanged', 'a\n'),
 
814
                          ('killed-base', 'b\n'),
 
815
                          ('killed-b', 'x\n'),
 
816
                          ('new-b', 'z\n'),
 
817
                          ('unchanged', 'c\n'),
 
818
                          ('unchanged', 'd\n'),
 
819
                          ('killed-base', 'e\n'),
 
820
                          ('unchanged', 'y\n'),
 
821
                          ('unchanged', 'f\n'),
 
822
                          ('unchanged', 'g\n')],
 
823
                         list(plan))
 
824
 
 
825
    def assertRemoveExternalReferences(self, filtered_parent_map,
 
826
                                       child_map, tails, parent_map):
 
827
        """Assert results for _PlanMerge._remove_external_references."""
 
828
        (act_filtered_parent_map, act_child_map,
 
829
         act_tails) = _PlanMerge._remove_external_references(parent_map)
 
830
 
 
831
        # The parent map *should* preserve ordering, but the ordering of
 
832
        # children is not strictly defined
 
833
        # child_map = dict((k, sorted(children))
 
834
        #                  for k, children in child_map.iteritems())
 
835
        # act_child_map = dict(k, sorted(children)
 
836
        #                      for k, children in act_child_map.iteritems())
 
837
        self.assertEqual(filtered_parent_map, act_filtered_parent_map)
 
838
        self.assertEqual(child_map, act_child_map)
 
839
        self.assertEqual(sorted(tails), sorted(act_tails))
 
840
 
 
841
    def test__remove_external_references(self):
 
842
        # First, nothing to remove
 
843
        self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
 
844
            {1: [2], 2: [3], 3: []}, [1], {3: [2], 2: [1], 1: []})
 
845
        # The reverse direction
 
846
        self.assertRemoveExternalReferences({1: [2], 2: [3], 3: []},
 
847
            {3: [2], 2: [1], 1: []}, [3], {1: [2], 2: [3], 3: []})
 
848
        # Extra references
 
849
        self.assertRemoveExternalReferences({3: [2], 2: [1], 1: []},
 
850
            {1: [2], 2: [3], 3: []}, [1], {3: [2, 4], 2: [1, 5], 1: [6]})
 
851
        # Multiple tails
 
852
        self.assertRemoveExternalReferences(
 
853
            {4: [2, 3], 3: [], 2: [1], 1: []},
 
854
            {1: [2], 2: [4], 3: [4], 4: []},
 
855
            [1, 3],
 
856
            {4: [2, 3], 3: [5], 2: [1], 1: [6]})
 
857
        # Multiple children
 
858
        self.assertRemoveExternalReferences(
 
859
            {1: [3], 2: [3, 4], 3: [], 4: []},
 
860
            {1: [], 2: [], 3: [1, 2], 4: [2]},
 
861
            [3, 4],
 
862
            {1: [3], 2: [3, 4], 3: [5], 4: []})
 
863
 
 
864
    def assertPruneTails(self, pruned_map, tails, parent_map):
 
865
        child_map = {}
 
866
        for key, parent_keys in parent_map.iteritems():
 
867
            child_map.setdefault(key, [])
 
868
            for pkey in parent_keys:
 
869
                child_map.setdefault(pkey, []).append(key)
 
870
        _PlanMerge._prune_tails(parent_map, child_map, tails)
 
871
        self.assertEqual(pruned_map, parent_map)
 
872
 
 
873
    def test__prune_tails(self):
 
874
        # Nothing requested to prune
 
875
        self.assertPruneTails({1: [], 2: [], 3: []}, [],
 
876
                              {1: [], 2: [], 3: []})
 
877
        # Prune a single entry
 
878
        self.assertPruneTails({1: [], 3: []}, [2],
 
879
                              {1: [], 2: [], 3: []})
 
880
        # Prune a chain
 
881
        self.assertPruneTails({1: []}, [3],
 
882
                              {1: [], 2: [3], 3: []})
 
883
        # Prune a chain with a diamond
 
884
        self.assertPruneTails({1: []}, [5],
 
885
                              {1: [], 2: [3, 4], 3: [5], 4: [5], 5: []})
 
886
        # Prune a partial chain
 
887
        self.assertPruneTails({1: [6], 6:[]}, [5],
 
888
                              {1: [2, 6], 2: [3, 4], 3: [5], 4: [5], 5: [],
 
889
                               6: []})
 
890
        # Prune a chain with multiple tips, that pulls out intermediates
 
891
        self.assertPruneTails({1:[3], 3:[]}, [4, 5],
 
892
                              {1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
 
893
        self.assertPruneTails({1:[3], 3:[]}, [5, 4],
 
894
                              {1: [2, 3], 2: [4, 5], 3: [], 4:[], 5:[]})
 
895
 
 
896
    def test_subtract_plans(self):
 
897
        old_plan = [
 
898
        ('unchanged', 'a\n'),
 
899
        ('new-a', 'b\n'),
 
900
        ('killed-a', 'c\n'),
 
901
        ('new-b', 'd\n'),
 
902
        ('new-b', 'e\n'),
 
903
        ('killed-b', 'f\n'),
 
904
        ('killed-b', 'g\n'),
 
905
        ]
 
906
        new_plan = [
 
907
        ('unchanged', 'a\n'),
 
908
        ('new-a', 'b\n'),
 
909
        ('killed-a', 'c\n'),
 
910
        ('new-b', 'd\n'),
 
911
        ('new-b', 'h\n'),
 
912
        ('killed-b', 'f\n'),
 
913
        ('killed-b', 'i\n'),
 
914
        ]
 
915
        subtracted_plan = [
 
916
        ('unchanged', 'a\n'),
 
917
        ('new-a', 'b\n'),
 
918
        ('killed-a', 'c\n'),
 
919
        ('new-b', 'h\n'),
 
920
        ('unchanged', 'f\n'),
 
921
        ('killed-b', 'i\n'),
 
922
        ]
 
923
        self.assertEqual(subtracted_plan,
 
924
            list(_PlanMerge._subtract_plans(old_plan, new_plan)))
 
925
 
 
926
    def setup_merge_with_base(self):
 
927
        self.add_rev('root', 'COMMON', [], 'abc')
 
928
        self.add_rev('root', 'THIS', ['COMMON'], 'abcd')
 
929
        self.add_rev('root', 'BASE', ['COMMON'], 'eabc')
 
930
        self.add_rev('root', 'OTHER', ['BASE'], 'eafb')
 
931
 
 
932
    def test_plan_merge_with_base(self):
 
933
        self.setup_merge_with_base()
 
934
        plan = self.plan_merge_vf.plan_merge('THIS', 'OTHER', 'BASE')
 
935
        self.assertEqual([('unchanged', 'a\n'),
 
936
                          ('new-b', 'f\n'),
 
937
                          ('unchanged', 'b\n'),
 
938
                          ('killed-b', 'c\n'),
 
939
                          ('new-a', 'd\n')
 
940
                         ], list(plan))
 
941
 
 
942
    def test_plan_lca_merge(self):
 
943
        self.setup_plan_merge()
 
944
        plan = self.plan_merge_vf.plan_lca_merge('B', 'C')
 
945
        self.assertEqual([
 
946
                          ('new-b', 'f\n'),
 
947
                          ('unchanged', 'a\n'),
 
948
                          ('killed-b', 'c\n'),
 
949
                          ('new-a', 'e\n'),
 
950
                          ('new-a', 'h\n'),
 
951
                          ('killed-a', 'b\n'),
 
952
                          ('unchanged', 'g\n')],
 
953
                         list(plan))
 
954
 
 
955
    def test_plan_lca_merge_uncommitted_files(self):
 
956
        self.setup_plan_merge_uncommitted()
 
957
        plan = self.plan_merge_vf.plan_lca_merge('B:', 'C:')
 
958
        self.assertEqual([
 
959
                          ('new-b', 'f\n'),
 
960
                          ('unchanged', 'a\n'),
 
961
                          ('killed-b', 'c\n'),
 
962
                          ('new-a', 'e\n'),
 
963
                          ('new-a', 'h\n'),
 
964
                          ('killed-a', 'b\n'),
 
965
                          ('unchanged', 'g\n')],
 
966
                         list(plan))
 
967
 
 
968
    def test_plan_lca_merge_with_base(self):
 
969
        self.setup_merge_with_base()
 
970
        plan = self.plan_merge_vf.plan_lca_merge('THIS', 'OTHER', 'BASE')
 
971
        self.assertEqual([('unchanged', 'a\n'),
 
972
                          ('new-b', 'f\n'),
 
973
                          ('unchanged', 'b\n'),
 
974
                          ('killed-b', 'c\n'),
 
975
                          ('new-a', 'd\n')
 
976
                         ], list(plan))
 
977
 
 
978
    def test_plan_lca_merge_with_criss_cross(self):
 
979
        self.add_version(('root', 'ROOT'), [], 'abc')
 
980
        # each side makes a change
 
981
        self.add_version(('root', 'REV1'), [('root', 'ROOT')], 'abcd')
 
982
        self.add_version(('root', 'REV2'), [('root', 'ROOT')], 'abce')
 
983
        # both sides merge, discarding others' changes
 
984
        self.add_version(('root', 'LCA1'),
 
985
            [('root', 'REV1'), ('root', 'REV2')], 'abcd')
 
986
        self.add_version(('root', 'LCA2'),
 
987
            [('root', 'REV1'), ('root', 'REV2')], 'fabce')
 
988
        plan = self.plan_merge_vf.plan_lca_merge('LCA1', 'LCA2')
 
989
        self.assertEqual([('new-b', 'f\n'),
 
990
                          ('unchanged', 'a\n'),
 
991
                          ('unchanged', 'b\n'),
 
992
                          ('unchanged', 'c\n'),
 
993
                          ('conflicted-a', 'd\n'),
 
994
                          ('conflicted-b', 'e\n'),
 
995
                         ], list(plan))
 
996
 
 
997
    def test_plan_lca_merge_with_null(self):
 
998
        self.add_version(('root', 'A'), [], 'ab')
 
999
        self.add_version(('root', 'B'), [], 'bc')
 
1000
        plan = self.plan_merge_vf.plan_lca_merge('A', 'B')
 
1001
        self.assertEqual([('new-a', 'a\n'),
 
1002
                          ('unchanged', 'b\n'),
 
1003
                          ('new-b', 'c\n'),
 
1004
                         ], list(plan))
 
1005
 
 
1006
    def test_plan_merge_with_delete_and_change(self):
 
1007
        self.add_rev('root', 'C', [], 'a')
 
1008
        self.add_rev('root', 'A', ['C'], 'b')
 
1009
        self.add_rev('root', 'B', ['C'], '')
 
1010
        plan = self.plan_merge_vf.plan_merge('A', 'B')
 
1011
        self.assertEqual([('killed-both', 'a\n'),
 
1012
                          ('new-a', 'b\n'),
 
1013
                         ], list(plan))
 
1014
 
 
1015
    def test_plan_merge_with_move_and_change(self):
 
1016
        self.add_rev('root', 'C', [], 'abcd')
 
1017
        self.add_rev('root', 'A', ['C'], 'acbd')
 
1018
        self.add_rev('root', 'B', ['C'], 'aBcd')
 
1019
        plan = self.plan_merge_vf.plan_merge('A', 'B')
 
1020
        self.assertEqual([('unchanged', 'a\n'),
 
1021
                          ('new-a', 'c\n'),
 
1022
                          ('killed-b', 'b\n'),
 
1023
                          ('new-b', 'B\n'),
 
1024
                          ('killed-a', 'c\n'),
 
1025
                          ('unchanged', 'd\n'),
 
1026
                         ], list(plan))
 
1027
 
 
1028
 
 
1029
class TestMergeImplementation(object):
 
1030
 
 
1031
    def do_merge(self, target_tree, source_tree, **kwargs):
 
1032
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
1033
            target_tree, source_tree.last_revision(),
 
1034
            other_branch=source_tree.branch)
 
1035
        merger.merge_type=self.merge_type
 
1036
        for name, value in kwargs.items():
 
1037
            setattr(merger, name, value)
 
1038
        merger.do_merge()
 
1039
 
 
1040
    def test_merge_specific_file(self):
 
1041
        this_tree = self.make_branch_and_tree('this')
 
1042
        this_tree.lock_write()
 
1043
        self.addCleanup(this_tree.unlock)
 
1044
        self.build_tree_contents([
 
1045
            ('this/file1', 'a\nb\n'),
 
1046
            ('this/file2', 'a\nb\n')
 
1047
        ])
 
1048
        this_tree.add(['file1', 'file2'])
 
1049
        this_tree.commit('Added files')
 
1050
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
1051
        self.build_tree_contents([
 
1052
            ('other/file1', 'a\nb\nc\n'),
 
1053
            ('other/file2', 'a\nb\nc\n')
 
1054
        ])
 
1055
        other_tree.commit('modified both')
 
1056
        self.build_tree_contents([
 
1057
            ('this/file1', 'd\na\nb\n'),
 
1058
            ('this/file2', 'd\na\nb\n')
 
1059
        ])
 
1060
        this_tree.commit('modified both')
 
1061
        self.do_merge(this_tree, other_tree, interesting_files=['file1'])
 
1062
        self.assertFileEqual('d\na\nb\nc\n', 'this/file1')
 
1063
        self.assertFileEqual('d\na\nb\n', 'this/file2')
 
1064
 
 
1065
    def test_merge_move_and_change(self):
 
1066
        this_tree = self.make_branch_and_tree('this')
 
1067
        this_tree.lock_write()
 
1068
        self.addCleanup(this_tree.unlock)
 
1069
        self.build_tree_contents([
 
1070
            ('this/file1', 'line 1\nline 2\nline 3\nline 4\n'),
 
1071
        ])
 
1072
        this_tree.add('file1',)
 
1073
        this_tree.commit('Added file')
 
1074
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
1075
        self.build_tree_contents([
 
1076
            ('other/file1', 'line 1\nline 2 to 2.1\nline 3\nline 4\n'),
 
1077
        ])
 
1078
        other_tree.commit('Changed 2 to 2.1')
 
1079
        self.build_tree_contents([
 
1080
            ('this/file1', 'line 1\nline 3\nline 2\nline 4\n'),
 
1081
        ])
 
1082
        this_tree.commit('Swapped 2 & 3')
 
1083
        self.do_merge(this_tree, other_tree)
 
1084
        self.assertFileEqual('line 1\n'
 
1085
            '<<<<<<< TREE\n'
 
1086
            'line 3\n'
 
1087
            'line 2\n'
 
1088
            '=======\n'
 
1089
            'line 2 to 2.1\n'
 
1090
            'line 3\n'
 
1091
            '>>>>>>> MERGE-SOURCE\n'
 
1092
            'line 4\n', 'this/file1')
 
1093
 
 
1094
 
 
1095
class TestMerge3Merge(TestCaseWithTransport, TestMergeImplementation):
 
1096
 
 
1097
    merge_type = _mod_merge.Merge3Merger
 
1098
 
 
1099
 
 
1100
class TestWeaveMerge(TestCaseWithTransport, TestMergeImplementation):
 
1101
 
 
1102
    merge_type = _mod_merge.WeaveMerger
 
1103
 
 
1104
 
 
1105
class TestLCAMerge(TestCaseWithTransport, TestMergeImplementation):
 
1106
 
 
1107
    merge_type = _mod_merge.LCAMerger
 
1108
 
 
1109
    def test_merge_move_and_change(self):
 
1110
        self.expectFailure("lca merge doesn't conflict for move and change",
 
1111
            super(TestLCAMerge, self).test_merge_move_and_change)
 
1112
 
 
1113
 
 
1114
class LoggingMerger(object):
 
1115
    # These seem to be the required attributes
 
1116
    requires_base = False
 
1117
    supports_reprocess = False
 
1118
    supports_show_base = False
 
1119
    supports_cherrypick = False
 
1120
    # We intentionally do not define supports_lca_trees
 
1121
 
 
1122
    def __init__(self, *args, **kwargs):
 
1123
        self.args = args
 
1124
        self.kwargs = kwargs
 
1125
 
 
1126
 
 
1127
class TestMergerBase(TestCaseWithMemoryTransport):
 
1128
    """Common functionality for Merger tests that don't write to disk."""
 
1129
 
 
1130
    def get_builder(self):
 
1131
        builder = self.make_branch_builder('path')
 
1132
        builder.start_series()
 
1133
        self.addCleanup(builder.finish_series)
 
1134
        return builder
 
1135
 
 
1136
    def setup_simple_graph(self):
 
1137
        """Create a simple 3-node graph.
 
1138
 
 
1139
        :return: A BranchBuilder
 
1140
        """
 
1141
        #
 
1142
        #  A
 
1143
        #  |\
 
1144
        #  B C
 
1145
        #
 
1146
        builder = self.get_builder()
 
1147
        builder.build_snapshot('A-id', None,
 
1148
            [('add', ('', None, 'directory', None))])
 
1149
        builder.build_snapshot('C-id', ['A-id'], [])
 
1150
        builder.build_snapshot('B-id', ['A-id'], [])
 
1151
        return builder
 
1152
 
 
1153
    def setup_criss_cross_graph(self):
 
1154
        """Create a 5-node graph with a criss-cross.
 
1155
 
 
1156
        :return: A BranchBuilder
 
1157
        """
 
1158
        # A
 
1159
        # |\
 
1160
        # B C
 
1161
        # |X|
 
1162
        # D E
 
1163
        builder = self.setup_simple_graph()
 
1164
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1165
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1166
        return builder
 
1167
 
 
1168
    def make_Merger(self, builder, other_revision_id,
 
1169
                    interesting_files=None, interesting_ids=None):
 
1170
        """Make a Merger object from a branch builder"""
 
1171
        mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
 
1172
        mem_tree.lock_write()
 
1173
        self.addCleanup(mem_tree.unlock)
 
1174
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
1175
            mem_tree, other_revision_id)
 
1176
        merger.set_interesting_files(interesting_files)
 
1177
        # It seems there is no matching function for set_interesting_ids
 
1178
        merger.interesting_ids = interesting_ids
 
1179
        merger.merge_type = _mod_merge.Merge3Merger
 
1180
        return merger
 
1181
 
 
1182
 
 
1183
class TestMergerInMemory(TestMergerBase):
 
1184
 
 
1185
    def test_find_base(self):
 
1186
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
 
1187
        self.assertEqual('A-id', merger.base_rev_id)
 
1188
        self.assertFalse(merger._is_criss_cross)
 
1189
        self.assertIs(None, merger._lca_trees)
 
1190
 
 
1191
    def test_find_base_criss_cross(self):
 
1192
        builder = self.setup_criss_cross_graph()
 
1193
        merger = self.make_Merger(builder, 'E-id')
 
1194
        self.assertEqual('A-id', merger.base_rev_id)
 
1195
        self.assertTrue(merger._is_criss_cross)
 
1196
        self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
 
1197
                                            for t in merger._lca_trees])
 
1198
        # If we swap the order, we should get a different lca order
 
1199
        builder.build_snapshot('F-id', ['E-id'], [])
 
1200
        merger = self.make_Merger(builder, 'D-id')
 
1201
        self.assertEqual(['C-id', 'B-id'], [t.get_revision_id()
 
1202
                                            for t in merger._lca_trees])
 
1203
 
 
1204
    def test_find_base_triple_criss_cross(self):
 
1205
        #       A-.
 
1206
        #      / \ \
 
1207
        #     B   C F # F is merged into both branches
 
1208
        #     |\ /| |
 
1209
        #     | X | |\
 
1210
        #     |/ \| | :
 
1211
        #   : D   E |
 
1212
        #    \|   |/
 
1213
        #     G   H
 
1214
        builder = self.setup_criss_cross_graph()
 
1215
        builder.build_snapshot('F-id', ['A-id'], [])
 
1216
        builder.build_snapshot('H-id', ['E-id', 'F-id'], [])
 
1217
        builder.build_snapshot('G-id', ['D-id', 'F-id'], [])
 
1218
        merger = self.make_Merger(builder, 'H-id')
 
1219
        self.assertEqual(['B-id', 'C-id', 'F-id'],
 
1220
                         [t.get_revision_id() for t in merger._lca_trees])
 
1221
 
 
1222
    def test_no_criss_cross_passed_to_merge_type(self):
 
1223
        class LCATreesMerger(LoggingMerger):
 
1224
            supports_lca_trees = True
 
1225
 
 
1226
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
 
1227
        merger.merge_type = LCATreesMerger
 
1228
        merge_obj = merger.make_merger()
 
1229
        self.assertIsInstance(merge_obj, LCATreesMerger)
 
1230
        self.assertFalse('lca_trees' in merge_obj.kwargs)
 
1231
 
 
1232
    def test_criss_cross_passed_to_merge_type(self):
 
1233
        merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
 
1234
        merger.merge_type = _mod_merge.Merge3Merger
 
1235
        merge_obj = merger.make_merger()
 
1236
        self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
 
1237
                                            for t in merger._lca_trees])
 
1238
 
 
1239
    def test_criss_cross_not_supported_merge_type(self):
 
1240
        merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
 
1241
        # We explicitly do not define supports_lca_trees
 
1242
        merger.merge_type = LoggingMerger
 
1243
        merge_obj = merger.make_merger()
 
1244
        self.assertIsInstance(merge_obj, LoggingMerger)
 
1245
        self.assertFalse('lca_trees' in merge_obj.kwargs)
 
1246
 
 
1247
    def test_criss_cross_unsupported_merge_type(self):
 
1248
        class UnsupportedLCATreesMerger(LoggingMerger):
 
1249
            supports_lca_trees = False
 
1250
 
 
1251
        merger = self.make_Merger(self.setup_criss_cross_graph(), 'E-id')
 
1252
        merger.merge_type = UnsupportedLCATreesMerger
 
1253
        merge_obj = merger.make_merger()
 
1254
        self.assertIsInstance(merge_obj, UnsupportedLCATreesMerger)
 
1255
        self.assertFalse('lca_trees' in merge_obj.kwargs)
 
1256
 
 
1257
 
 
1258
class TestMergerEntriesLCA(TestMergerBase):
 
1259
 
 
1260
    def make_merge_obj(self, builder, other_revision_id,
 
1261
                       interesting_files=None, interesting_ids=None):
 
1262
        merger = self.make_Merger(builder, other_revision_id,
 
1263
            interesting_files=interesting_files,
 
1264
            interesting_ids=interesting_ids)
 
1265
        return merger.make_merger()
 
1266
 
 
1267
    def test_simple(self):
 
1268
        builder = self.get_builder()
 
1269
        builder.build_snapshot('A-id', None,
 
1270
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1271
             ('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1272
        builder.build_snapshot('C-id', ['A-id'],
 
1273
            [('modify', ('a-id', 'a\nb\nC\nc\n'))])
 
1274
        builder.build_snapshot('B-id', ['A-id'],
 
1275
            [('modify', ('a-id', 'a\nB\nb\nc\n'))])
 
1276
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1277
            [('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
 
1278
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1279
            [('modify', ('a-id', 'a\nB\nb\nC\nc\n'))])
 
1280
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1281
 
 
1282
        self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
 
1283
                                            for t in merge_obj._lca_trees])
 
1284
        self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
 
1285
        entries = list(merge_obj._entries_lca())
 
1286
 
 
1287
        # (file_id, changed, parents, names, executable)
 
1288
        # BASE, lca1, lca2, OTHER, THIS
 
1289
        root_id = 'a-root-id'
 
1290
        self.assertEqual([('a-id', True,
 
1291
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1292
                           ((u'a', [u'a', u'a']), u'a', u'a'),
 
1293
                           ((False, [False, False]), False, False)),
 
1294
                         ], entries)
 
1295
 
 
1296
    def test_not_in_base(self):
 
1297
        # LCAs all have the same last-modified revision for the file, as do
 
1298
        # the tips, but the base has something different
 
1299
        #       A    base, doesn't have the file
 
1300
        #       |\
 
1301
        #       B C  B introduces 'foo', C introduces 'bar'
 
1302
        #       |X|
 
1303
        #       D E  D and E now both have 'foo' and 'bar'
 
1304
        #       |X|
 
1305
        #       F G  the files are now in F, G, D and E, but not in A
 
1306
        #            G modifies 'bar'
 
1307
 
 
1308
        builder = self.get_builder()
 
1309
        builder.build_snapshot('A-id', None,
 
1310
            [('add', (u'', 'a-root-id', 'directory', None))])
 
1311
        builder.build_snapshot('B-id', ['A-id'],
 
1312
            [('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
1313
        builder.build_snapshot('C-id', ['A-id'],
 
1314
            [('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
 
1315
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1316
            [('add', (u'bar', 'bar-id', 'file', 'd\ne\nf\n'))])
 
1317
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1318
            [('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
1319
        builder.build_snapshot('G-id', ['E-id', 'D-id'],
 
1320
            [('modify', (u'bar-id', 'd\ne\nf\nG\n'))])
 
1321
        builder.build_snapshot('F-id', ['D-id', 'E-id'], [])
 
1322
        merge_obj = self.make_merge_obj(builder, 'G-id')
 
1323
 
 
1324
        self.assertEqual(['D-id', 'E-id'], [t.get_revision_id()
 
1325
                                            for t in merge_obj._lca_trees])
 
1326
        self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
 
1327
        entries = list(merge_obj._entries_lca())
 
1328
        root_id = 'a-root-id'
 
1329
        self.assertEqual([('bar-id', True,
 
1330
                           ((None, [root_id, root_id]), root_id, root_id),
 
1331
                           ((None, [u'bar', u'bar']), u'bar', u'bar'),
 
1332
                           ((None, [False, False]), False, False)),
 
1333
                         ], entries)
 
1334
 
 
1335
    def test_not_in_this(self):
 
1336
        builder = self.get_builder()
 
1337
        builder.build_snapshot('A-id', None,
 
1338
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1339
             ('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1340
        builder.build_snapshot('B-id', ['A-id'],
 
1341
            [('modify', ('a-id', 'a\nB\nb\nc\n'))])
 
1342
        builder.build_snapshot('C-id', ['A-id'],
 
1343
            [('modify', ('a-id', 'a\nb\nC\nc\n'))])
 
1344
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1345
            [('modify', ('a-id', 'a\nB\nb\nC\nc\nE\n'))])
 
1346
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1347
            [('unversion', 'a-id')])
 
1348
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1349
 
 
1350
        self.assertEqual(['B-id', 'C-id'], [t.get_revision_id()
 
1351
                                            for t in merge_obj._lca_trees])
 
1352
        self.assertEqual('A-id', merge_obj.base_tree.get_revision_id())
 
1353
 
 
1354
        entries = list(merge_obj._entries_lca())
 
1355
        root_id = 'a-root-id'
 
1356
        self.assertEqual([('a-id', True,
 
1357
                           ((root_id, [root_id, root_id]), root_id, None),
 
1358
                           ((u'a', [u'a', u'a']), u'a', None),
 
1359
                           ((False, [False, False]), False, None)),
 
1360
                         ], entries)
 
1361
 
 
1362
    def test_file_not_in_one_lca(self):
 
1363
        #   A   # just root
 
1364
        #   |\
 
1365
        #   B C # B no file, C introduces a file
 
1366
        #   |X|
 
1367
        #   D E # D and E both have the file, unchanged from C
 
1368
        builder = self.get_builder()
 
1369
        builder.build_snapshot('A-id', None,
 
1370
            [('add', (u'', 'a-root-id', 'directory', None))])
 
1371
        builder.build_snapshot('B-id', ['A-id'], [])
 
1372
        builder.build_snapshot('C-id', ['A-id'],
 
1373
            [('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1374
        builder.build_snapshot('E-id', ['C-id', 'B-id'], []) # Inherited from C
 
1375
        builder.build_snapshot('D-id', ['B-id', 'C-id'], # Merged from C
 
1376
            [('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1377
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1378
 
 
1379
        self.assertEqual(['B-id', 'C-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
 
 
1383
        entries = list(merge_obj._entries_lca())
 
1384
        root_id = 'a-root-id'
 
1385
        self.assertEqual([], entries)
 
1386
 
 
1387
    def test_not_in_other(self):
 
1388
        builder = self.get_builder()
 
1389
        builder.build_snapshot('A-id', None,
 
1390
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1391
             ('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1392
        builder.build_snapshot('B-id', ['A-id'], [])
 
1393
        builder.build_snapshot('C-id', ['A-id'], [])
 
1394
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1395
            [('unversion', 'a-id')])
 
1396
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1397
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1398
 
 
1399
        entries = list(merge_obj._entries_lca())
 
1400
        root_id = 'a-root-id'
 
1401
        self.assertEqual([('a-id', True,
 
1402
                           ((root_id, [root_id, root_id]), None, root_id),
 
1403
                           ((u'a', [u'a', u'a']), None, u'a'),
 
1404
                           ((False, [False, False]), None, False)),
 
1405
                         ], entries)
 
1406
 
 
1407
    def test_not_in_other_or_lca(self):
 
1408
        #       A    base, introduces 'foo'
 
1409
        #       |\
 
1410
        #       B C  B nothing, C deletes foo
 
1411
        #       |X|
 
1412
        #       D E  D restores foo (same as B), E leaves it deleted
 
1413
        # We should emit an entry for this
 
1414
        builder = self.get_builder()
 
1415
        builder.build_snapshot('A-id', None,
 
1416
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1417
             ('add', (u'foo', 'foo-id', 'file', 'content\n'))])
 
1418
        builder.build_snapshot('B-id', ['A-id'], [])
 
1419
        builder.build_snapshot('C-id', ['A-id'],
 
1420
            [('unversion', 'foo-id')])
 
1421
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1422
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1423
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1424
 
 
1425
        entries = list(merge_obj._entries_lca())
 
1426
        root_id = 'a-root-id'
 
1427
        self.assertEqual([('foo-id', True,
 
1428
                           ((root_id, [root_id, None]), None, root_id),
 
1429
                           ((u'foo', [u'foo', None]), None, 'foo'),
 
1430
                           ((False, [False, None]), None, False)),
 
1431
                         ], entries)
 
1432
 
 
1433
    def test_only_in_one_lca(self):
 
1434
        builder = self.get_builder()
 
1435
        builder.build_snapshot('A-id', None,
 
1436
            [('add', (u'', 'a-root-id', 'directory', None))])
 
1437
        builder.build_snapshot('B-id', ['A-id'], [])
 
1438
        builder.build_snapshot('C-id', ['A-id'],
 
1439
            [('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1440
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1441
            [('unversion', 'a-id')])
 
1442
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1443
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1444
 
 
1445
        entries = list(merge_obj._entries_lca())
 
1446
        root_id = 'a-root-id'
 
1447
        self.assertEqual([('a-id', True,
 
1448
                           ((None, [None, root_id]), None, None),
 
1449
                           ((None, [None, u'a']), None, None),
 
1450
                           ((None, [None, False]), None, None)),
 
1451
                         ], entries)
 
1452
 
 
1453
    def test_only_in_other(self):
 
1454
        builder = self.get_builder()
 
1455
        builder.build_snapshot('A-id', None,
 
1456
            [('add', (u'', 'a-root-id', 'directory', None))])
 
1457
        builder.build_snapshot('B-id', ['A-id'], [])
 
1458
        builder.build_snapshot('C-id', ['A-id'], [])
 
1459
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1460
            [('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1461
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1462
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1463
 
 
1464
        entries = list(merge_obj._entries_lca())
 
1465
        root_id = 'a-root-id'
 
1466
        self.assertEqual([('a-id', True,
 
1467
                           ((None, [None, None]), root_id, None),
 
1468
                           ((None, [None, None]), u'a', None),
 
1469
                           ((None, [None, None]), False, None)),
 
1470
                         ], entries)
 
1471
 
 
1472
    def test_one_lca_supersedes(self):
 
1473
        # One LCA supersedes the other LCAs last modified value, but the
 
1474
        # value is not the same as BASE.
 
1475
        #       A    base, introduces 'foo', last mod A
 
1476
        #       |\
 
1477
        #       B C  B modifies 'foo' (mod B), C does nothing (mod A)
 
1478
        #       |X|
 
1479
        #       D E  D does nothing (mod B), E updates 'foo' (mod E)
 
1480
        #       |X|
 
1481
        #       F G  F updates 'foo' (mod F). G does nothing (mod E)
 
1482
        #
 
1483
        #   At this point, G should not be considered to modify 'foo', even
 
1484
        #   though its LCAs disagree. This is because the modification in E
 
1485
        #   completely supersedes the value in D.
 
1486
        builder = self.get_builder()
 
1487
        builder.build_snapshot('A-id', None,
 
1488
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1489
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1490
        builder.build_snapshot('C-id', ['A-id'], [])
 
1491
        builder.build_snapshot('B-id', ['A-id'],
 
1492
            [('modify', ('foo-id', 'B content\n'))])
 
1493
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1494
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1495
            [('modify', ('foo-id', 'E content\n'))])
 
1496
        builder.build_snapshot('G-id', ['E-id', 'D-id'], [])
 
1497
        builder.build_snapshot('F-id', ['D-id', 'E-id'],
 
1498
            [('modify', ('foo-id', 'F content\n'))])
 
1499
        merge_obj = self.make_merge_obj(builder, 'G-id')
 
1500
 
 
1501
        self.assertEqual([], list(merge_obj._entries_lca()))
 
1502
 
 
1503
    def test_one_lca_supersedes_path(self):
 
1504
        # Double-criss-cross merge, the ultimate base value is different from
 
1505
        # the intermediate.
 
1506
        #   A    value 'foo'
 
1507
        #   |\
 
1508
        #   B C  B value 'bar', C = 'foo'
 
1509
        #   |X|
 
1510
        #   D E  D = 'bar', E supersedes to 'bing'
 
1511
        #   |X|
 
1512
        #   F G  F = 'bing', G supersedes to 'barry'
 
1513
        #
 
1514
        # In this case, we technically should not care about the value 'bar' for
 
1515
        # D, because it was clearly superseded by E's 'bing'. The
 
1516
        # per-file/attribute graph would actually look like:
 
1517
        #   A
 
1518
        #   |
 
1519
        #   B
 
1520
        #   |
 
1521
        #   E
 
1522
        #   |
 
1523
        #   G
 
1524
        #
 
1525
        # Because the other side of the merge never modifies the value, it just
 
1526
        # takes the value from the merge.
 
1527
        #
 
1528
        # ATM this fails because we will prune 'foo' from the LCAs, but we
 
1529
        # won't prune 'bar'. This is getting far off into edge-case land, so we
 
1530
        # aren't supporting it yet.
 
1531
        #
 
1532
        builder = self.get_builder()
 
1533
        builder.build_snapshot('A-id', None,
 
1534
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1535
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1536
        builder.build_snapshot('C-id', ['A-id'], [])
 
1537
        builder.build_snapshot('B-id', ['A-id'],
 
1538
            [('rename', ('foo', 'bar'))])
 
1539
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1540
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1541
            [('rename', ('foo', 'bing'))]) # override to bing
 
1542
        builder.build_snapshot('G-id', ['E-id', 'D-id'],
 
1543
            [('rename', ('bing', 'barry'))]) # override to barry
 
1544
        builder.build_snapshot('F-id', ['D-id', 'E-id'],
 
1545
            [('rename', ('bar', 'bing'))]) # Merge in E's change
 
1546
        merge_obj = self.make_merge_obj(builder, 'G-id')
 
1547
 
 
1548
        self.expectFailure("We don't do an actual heads() check on lca values,"
 
1549
            " or use the per-attribute graph",
 
1550
            self.assertEqual, [], list(merge_obj._entries_lca()))
 
1551
 
 
1552
    def test_one_lca_accidentally_pruned(self):
 
1553
        # Another incorrect resolution from the same basic flaw:
 
1554
        #   A    value 'foo'
 
1555
        #   |\
 
1556
        #   B C  B value 'bar', C = 'foo'
 
1557
        #   |X|
 
1558
        #   D E  D = 'bar', E reverts to 'foo'
 
1559
        #   |X|
 
1560
        #   F G  F = 'bing', G switches to 'bar'
 
1561
        #
 
1562
        # 'bar' will not be seen as an interesting change, because 'foo' will
 
1563
        # be pruned from the LCAs, even though it was newly introduced by E
 
1564
        # (superseding B).
 
1565
        builder = self.get_builder()
 
1566
        builder.build_snapshot('A-id', None,
 
1567
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1568
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1569
        builder.build_snapshot('C-id', ['A-id'], [])
 
1570
        builder.build_snapshot('B-id', ['A-id'],
 
1571
            [('rename', ('foo', 'bar'))])
 
1572
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1573
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1574
        builder.build_snapshot('G-id', ['E-id', 'D-id'],
 
1575
            [('rename', ('foo', 'bar'))])
 
1576
        builder.build_snapshot('F-id', ['D-id', 'E-id'],
 
1577
            [('rename', ('bar', 'bing'))]) # should end up conflicting
 
1578
        merge_obj = self.make_merge_obj(builder, 'G-id')
 
1579
 
 
1580
        entries = list(merge_obj._entries_lca())
 
1581
        root_id = 'a-root-id'
 
1582
        self.expectFailure("We prune values from BASE even when relevant.",
 
1583
            self.assertEqual,
 
1584
                [('foo-id', False,
 
1585
                  ((root_id, [root_id, root_id]), root_id, root_id),
 
1586
                  ((u'foo', [u'bar', u'foo']), u'bar', u'bing'),
 
1587
                  ((False, [False, False]), False, False)),
 
1588
                ], entries)
 
1589
 
 
1590
    def test_both_sides_revert(self):
 
1591
        # Both sides of a criss-cross revert the text to the lca
 
1592
        #       A    base, introduces 'foo'
 
1593
        #       |\
 
1594
        #       B C  B modifies 'foo', C modifies 'foo'
 
1595
        #       |X|
 
1596
        #       D E  D reverts to B, E reverts to C
 
1597
        # This should conflict
 
1598
        builder = self.get_builder()
 
1599
        builder.build_snapshot('A-id', None,
 
1600
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1601
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1602
        builder.build_snapshot('B-id', ['A-id'],
 
1603
            [('modify', ('foo-id', 'B content\n'))])
 
1604
        builder.build_snapshot('C-id', ['A-id'],
 
1605
            [('modify', ('foo-id', 'C content\n'))])
 
1606
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1607
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1608
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1609
 
 
1610
        entries = list(merge_obj._entries_lca())
 
1611
        root_id = 'a-root-id'
 
1612
        self.assertEqual([('foo-id', True,
 
1613
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1614
                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
 
1615
                           ((False, [False, False]), False, False)),
 
1616
                         ], entries)
 
1617
 
 
1618
    def test_different_lca_resolve_one_side_updates_content(self):
 
1619
        # Both sides converge, but then one side updates the text.
 
1620
        #       A    base, introduces 'foo'
 
1621
        #       |\
 
1622
        #       B C  B modifies 'foo', C modifies 'foo'
 
1623
        #       |X|
 
1624
        #       D E  D reverts to B, E reverts to C
 
1625
        #       |
 
1626
        #       F    F updates to a new value
 
1627
        # We need to emit an entry for 'foo', because D & E differed on the
 
1628
        # merge resolution
 
1629
        builder = self.get_builder()
 
1630
        builder.build_snapshot('A-id', None,
 
1631
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1632
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1633
        builder.build_snapshot('B-id', ['A-id'],
 
1634
            [('modify', ('foo-id', 'B content\n'))])
 
1635
        builder.build_snapshot('C-id', ['A-id'],
 
1636
            [('modify', ('foo-id', 'C content\n'))])
 
1637
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1638
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1639
        builder.build_snapshot('F-id', ['D-id'],
 
1640
            [('modify', ('foo-id', 'F content\n'))])
 
1641
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1642
 
 
1643
        entries = list(merge_obj._entries_lca())
 
1644
        root_id = 'a-root-id'
 
1645
        self.assertEqual([('foo-id', True,
 
1646
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1647
                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
 
1648
                           ((False, [False, False]), False, False)),
 
1649
                         ], entries)
 
1650
 
 
1651
    def test_same_lca_resolution_one_side_updates_content(self):
 
1652
        # Both sides converge, but then one side updates the text.
 
1653
        #       A    base, introduces 'foo'
 
1654
        #       |\
 
1655
        #       B C  B modifies 'foo', C modifies 'foo'
 
1656
        #       |X|
 
1657
        #       D E  D and E use C's value
 
1658
        #       |
 
1659
        #       F    F updates to a new value
 
1660
        # I think it is a bug that this conflicts, but we don't have a way to
 
1661
        # detect otherwise. And because of:
 
1662
        #   test_different_lca_resolve_one_side_updates_content
 
1663
        # We need to conflict.
 
1664
 
 
1665
        builder = self.get_builder()
 
1666
        builder.build_snapshot('A-id', None,
 
1667
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1668
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
1669
        builder.build_snapshot('B-id', ['A-id'],
 
1670
            [('modify', ('foo-id', 'B content\n'))])
 
1671
        builder.build_snapshot('C-id', ['A-id'],
 
1672
            [('modify', ('foo-id', 'C content\n'))])
 
1673
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1674
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1675
            [('modify', ('foo-id', 'C content\n'))]) # Same as E
 
1676
        builder.build_snapshot('F-id', ['D-id'],
 
1677
            [('modify', ('foo-id', 'F content\n'))])
 
1678
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1679
 
 
1680
        entries = list(merge_obj._entries_lca())
 
1681
        root_id = 'a-root-id'
 
1682
        self.expectFailure("We don't detect that LCA resolution was the"
 
1683
                           " same on both sides",
 
1684
            self.assertEqual, [], entries)
 
1685
 
 
1686
    def test_only_path_changed(self):
 
1687
        builder = self.get_builder()
 
1688
        builder.build_snapshot('A-id', None,
 
1689
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1690
             ('add', (u'a', 'a-id', 'file', 'content\n'))])
 
1691
        builder.build_snapshot('B-id', ['A-id'], [])
 
1692
        builder.build_snapshot('C-id', ['A-id'], [])
 
1693
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1694
            [('rename', (u'a', u'b'))])
 
1695
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1696
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1697
        entries = list(merge_obj._entries_lca())
 
1698
        root_id = 'a-root-id'
 
1699
        # The content was not changed, only the path
 
1700
        self.assertEqual([('a-id', False,
 
1701
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1702
                           ((u'a', [u'a', u'a']), u'b', u'a'),
 
1703
                           ((False, [False, False]), False, False)),
 
1704
                         ], entries)
 
1705
 
 
1706
    def test_kind_changed(self):
 
1707
        # Identical content, except 'D' changes a-id into a directory
 
1708
        builder = self.get_builder()
 
1709
        builder.build_snapshot('A-id', None,
 
1710
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1711
             ('add', (u'a', 'a-id', 'file', 'content\n'))])
 
1712
        builder.build_snapshot('B-id', ['A-id'], [])
 
1713
        builder.build_snapshot('C-id', ['A-id'], [])
 
1714
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1715
            [('unversion', 'a-id'),
 
1716
             ('add', (u'a', 'a-id', 'directory', None))])
 
1717
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1718
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1719
        entries = list(merge_obj._entries_lca())
 
1720
        root_id = 'a-root-id'
 
1721
        # Only the kind was changed (content)
 
1722
        self.assertEqual([('a-id', True,
 
1723
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1724
                           ((u'a', [u'a', u'a']), u'a', u'a'),
 
1725
                           ((False, [False, False]), False, False)),
 
1726
                         ], entries)
 
1727
 
 
1728
    def test_this_changed_kind(self):
 
1729
        # Identical content, but THIS changes a file to a directory
 
1730
        builder = self.get_builder()
 
1731
        builder.build_snapshot('A-id', None,
 
1732
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1733
             ('add', (u'a', 'a-id', 'file', 'content\n'))])
 
1734
        builder.build_snapshot('B-id', ['A-id'], [])
 
1735
        builder.build_snapshot('C-id', ['A-id'], [])
 
1736
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1737
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1738
            [('unversion', 'a-id'),
 
1739
             ('add', (u'a', 'a-id', 'directory', None))])
 
1740
        merge_obj = self.make_merge_obj(builder, 'E-id')
 
1741
        entries = list(merge_obj._entries_lca())
 
1742
        root_id = 'a-root-id'
 
1743
        # Only the kind was changed (content)
 
1744
        self.assertEqual([], entries)
 
1745
 
 
1746
    def test_interesting_files(self):
 
1747
        # Two files modified, but we should filter one of them
 
1748
        builder = self.get_builder()
 
1749
        builder.build_snapshot('A-id', None,
 
1750
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1751
             ('add', (u'a', 'a-id', 'file', 'content\n')),
 
1752
             ('add', (u'b', 'b-id', 'file', 'content\n'))])
 
1753
        builder.build_snapshot('B-id', ['A-id'], [])
 
1754
        builder.build_snapshot('C-id', ['A-id'], [])
 
1755
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1756
            [('modify', ('a-id', 'new-content\n')),
 
1757
             ('modify', ('b-id', 'new-content\n'))])
 
1758
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1759
        merge_obj = self.make_merge_obj(builder, 'E-id',
 
1760
                                        interesting_files=['b'])
 
1761
        entries = list(merge_obj._entries_lca())
 
1762
        root_id = 'a-root-id'
 
1763
        self.assertEqual([('b-id', True,
 
1764
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1765
                           ((u'b', [u'b', u'b']), u'b', u'b'),
 
1766
                           ((False, [False, False]), False, False)),
 
1767
                         ], entries)
 
1768
 
 
1769
    def test_interesting_file_in_this(self):
 
1770
        # This renamed the file, but it should still match the entry in other
 
1771
        builder = self.get_builder()
 
1772
        builder.build_snapshot('A-id', None,
 
1773
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1774
             ('add', (u'a', 'a-id', 'file', 'content\n')),
 
1775
             ('add', (u'b', 'b-id', 'file', 'content\n'))])
 
1776
        builder.build_snapshot('B-id', ['A-id'], [])
 
1777
        builder.build_snapshot('C-id', ['A-id'], [])
 
1778
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1779
            [('modify', ('a-id', 'new-content\n')),
 
1780
             ('modify', ('b-id', 'new-content\n'))])
 
1781
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1782
            [('rename', ('b', 'c'))])
 
1783
        merge_obj = self.make_merge_obj(builder, 'E-id',
 
1784
                                        interesting_files=['c'])
 
1785
        entries = list(merge_obj._entries_lca())
 
1786
        root_id = 'a-root-id'
 
1787
        self.assertEqual([('b-id', True,
 
1788
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1789
                           ((u'b', [u'b', u'b']), u'b', u'c'),
 
1790
                           ((False, [False, False]), False, False)),
 
1791
                         ], entries)
 
1792
 
 
1793
    def test_interesting_file_in_base(self):
 
1794
        # This renamed the file, but it should still match the entry in BASE
 
1795
        builder = self.get_builder()
 
1796
        builder.build_snapshot('A-id', None,
 
1797
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1798
             ('add', (u'a', 'a-id', 'file', 'content\n')),
 
1799
             ('add', (u'c', 'c-id', 'file', 'content\n'))])
 
1800
        builder.build_snapshot('B-id', ['A-id'],
 
1801
            [('rename', ('c', 'b'))])
 
1802
        builder.build_snapshot('C-id', ['A-id'],
 
1803
            [('rename', ('c', 'b'))])
 
1804
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1805
            [('modify', ('a-id', 'new-content\n')),
 
1806
             ('modify', ('c-id', 'new-content\n'))])
 
1807
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1808
        merge_obj = self.make_merge_obj(builder, 'E-id',
 
1809
                                        interesting_files=['c'])
 
1810
        entries = list(merge_obj._entries_lca())
 
1811
        root_id = 'a-root-id'
 
1812
        self.assertEqual([('c-id', True,
 
1813
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1814
                           ((u'c', [u'b', u'b']), u'b', u'b'),
 
1815
                           ((False, [False, False]), False, False)),
 
1816
                         ], entries)
 
1817
 
 
1818
    def test_interesting_file_in_lca(self):
 
1819
        # This renamed the file, but it should still match the entry in LCA
 
1820
        builder = self.get_builder()
 
1821
        builder.build_snapshot('A-id', None,
 
1822
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1823
             ('add', (u'a', 'a-id', 'file', 'content\n')),
 
1824
             ('add', (u'b', 'b-id', 'file', 'content\n'))])
 
1825
        builder.build_snapshot('B-id', ['A-id'],
 
1826
            [('rename', ('b', 'c'))])
 
1827
        builder.build_snapshot('C-id', ['A-id'], [])
 
1828
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
1829
            [('modify', ('a-id', 'new-content\n')),
 
1830
             ('modify', ('b-id', 'new-content\n'))])
 
1831
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1832
            [('rename', ('c', 'b'))])
 
1833
        merge_obj = self.make_merge_obj(builder, 'E-id',
 
1834
                                        interesting_files=['c'])
 
1835
        entries = list(merge_obj._entries_lca())
 
1836
        root_id = 'a-root-id'
 
1837
        self.assertEqual([('b-id', True,
 
1838
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
1839
                           ((u'b', [u'c', u'b']), u'b', u'b'),
 
1840
                           ((False, [False, False]), False, False)),
 
1841
                         ], entries)
 
1842
 
 
1843
    def test_interesting_ids(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_ids=['b-id'])
 
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)),
 
1864
                         ], entries)
 
1865
 
 
1866
 
 
1867
 
 
1868
class TestMergerEntriesLCAOnDisk(tests.TestCaseWithTransport):
 
1869
 
 
1870
    def get_builder(self):
 
1871
        builder = self.make_branch_builder('path')
 
1872
        builder.start_series()
 
1873
        self.addCleanup(builder.finish_series)
 
1874
        return builder
 
1875
 
 
1876
    def get_wt_from_builder(self, builder):
 
1877
        """Get a real WorkingTree from the builder."""
 
1878
        the_branch = builder.get_branch()
 
1879
        wt = the_branch.bzrdir.create_workingtree()
 
1880
        # Note: This is a little bit ugly, but we are holding the branch
 
1881
        #       write-locked as part of the build process, and we would like to
 
1882
        #       maintain that. So we just force the WT to re-use the same
 
1883
        #       branch object.
 
1884
        wt._branch = the_branch
 
1885
        wt.lock_write()
 
1886
        self.addCleanup(wt.unlock)
 
1887
        return wt
 
1888
 
 
1889
    def do_merge(self, builder, other_revision_id):
 
1890
        wt = self.get_wt_from_builder(builder)
 
1891
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
1892
            wt, other_revision_id)
 
1893
        merger.merge_type = _mod_merge.Merge3Merger
 
1894
        return wt, merger.do_merge()
 
1895
 
 
1896
    def test_simple_lca(self):
 
1897
        builder = self.get_builder()
 
1898
        builder.build_snapshot('A-id', None,
 
1899
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1900
             ('add', (u'a', 'a-id', 'file', 'a\nb\nc\n'))])
 
1901
        builder.build_snapshot('C-id', ['A-id'], [])
 
1902
        builder.build_snapshot('B-id', ['A-id'], [])
 
1903
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1904
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
1905
            [('modify', ('a-id', 'a\nb\nc\nd\ne\nf\n'))])
 
1906
        wt, conflicts = self.do_merge(builder, 'E-id')
 
1907
        self.assertEqual(0, conflicts)
 
1908
        # The merge should have simply update the contents of 'a'
 
1909
        self.assertEqual('a\nb\nc\nd\ne\nf\n', wt.get_file_text('a-id'))
 
1910
 
 
1911
    def test_conflict_without_lca(self):
 
1912
        # This test would cause a merge conflict, unless we use the lca trees
 
1913
        # to determine the real ancestry
 
1914
        #   A       Path at 'foo'
 
1915
        #  / \
 
1916
        # B   C     Path renamed to 'bar' in B
 
1917
        # |\ /|
 
1918
        # | X |
 
1919
        # |/ \|
 
1920
        # D   E     Path at 'bar' in D and E
 
1921
        #     |
 
1922
        #     F     Path at 'baz' in F, which supersedes 'bar' and 'foo'
 
1923
        builder = self.get_builder()
 
1924
        builder.build_snapshot('A-id', None,
 
1925
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1926
             ('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
1927
        builder.build_snapshot('C-id', ['A-id'], [])
 
1928
        builder.build_snapshot('B-id', ['A-id'],
 
1929
            [('rename', ('foo', 'bar'))])
 
1930
        builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
 
1931
            [('rename', ('foo', 'bar'))])
 
1932
        builder.build_snapshot('F-id', ['E-id'],
 
1933
            [('rename', ('bar', 'baz'))])
 
1934
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1935
        wt, conflicts = self.do_merge(builder, 'F-id')
 
1936
        self.assertEqual(0, conflicts)
 
1937
        # The merge should simply recognize that the final rename takes
 
1938
        # precedence
 
1939
        self.assertEqual('baz', wt.id2path('foo-id'))
 
1940
 
 
1941
    def test_other_deletes_lca_renames(self):
 
1942
        # This test would cause a merge conflict, unless we use the lca trees
 
1943
        # to determine the real ancestry
 
1944
        #   A       Path at 'foo'
 
1945
        #  / \
 
1946
        # B   C     Path renamed to 'bar' in B
 
1947
        # |\ /|
 
1948
        # | X |
 
1949
        # |/ \|
 
1950
        # D   E     Path at 'bar' in D and E
 
1951
        #     |
 
1952
        #     F     F deletes 'bar'
 
1953
        builder = self.get_builder()
 
1954
        builder.build_snapshot('A-id', None,
 
1955
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1956
             ('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
1957
        builder.build_snapshot('C-id', ['A-id'], [])
 
1958
        builder.build_snapshot('B-id', ['A-id'],
 
1959
            [('rename', ('foo', 'bar'))])
 
1960
        builder.build_snapshot('E-id', ['C-id', 'B-id'], # merge the rename
 
1961
            [('rename', ('foo', 'bar'))])
 
1962
        builder.build_snapshot('F-id', ['E-id'],
 
1963
            [('unversion', 'foo-id')])
 
1964
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1965
        wt, conflicts = self.do_merge(builder, 'F-id')
 
1966
        self.assertEqual(0, conflicts)
 
1967
        self.assertRaises(errors.NoSuchId, wt.id2path, 'foo-id')
 
1968
 
 
1969
    def test_executable_changes(self):
 
1970
        #   A       Path at 'foo'
 
1971
        #  / \
 
1972
        # B   C
 
1973
        # |\ /|
 
1974
        # | X |
 
1975
        # |/ \|
 
1976
        # D   E
 
1977
        #     |
 
1978
        #     F     Executable bit changed
 
1979
        builder = self.get_builder()
 
1980
        builder.build_snapshot('A-id', None,
 
1981
            [('add', (u'', 'a-root-id', 'directory', None)),
 
1982
             ('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
1983
        builder.build_snapshot('C-id', ['A-id'], [])
 
1984
        builder.build_snapshot('B-id', ['A-id'], [])
 
1985
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
1986
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
1987
        # Have to use a real WT, because BranchBuilder doesn't support exec bit
 
1988
        wt = self.get_wt_from_builder(builder)
 
1989
        tt = transform.TreeTransform(wt)
 
1990
        try:
 
1991
            tt.set_executability(True, tt.trans_id_tree_file_id('foo-id'))
 
1992
            tt.apply()
 
1993
        except:
 
1994
            tt.finalize()
 
1995
            raise
 
1996
        self.assertTrue(wt.is_executable('foo-id'))
 
1997
        wt.commit('F-id', rev_id='F-id')
 
1998
        # Reset to D, so that we can merge F
 
1999
        wt.set_parent_ids(['D-id'])
 
2000
        wt.branch.set_last_revision_info(3, 'D-id')
 
2001
        wt.revert()
 
2002
        self.assertFalse(wt.is_executable('foo-id'))
 
2003
        conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
 
2004
        self.assertEqual(0, conflicts)
 
2005
        self.assertTrue(wt.is_executable('foo-id'))
 
2006
 
 
2007
    def test_create_symlink(self):
 
2008
        self.requireFeature(tests.SymlinkFeature)
 
2009
        #   A
 
2010
        #  / \
 
2011
        # B   C
 
2012
        # |\ /|
 
2013
        # | X |
 
2014
        # |/ \|
 
2015
        # D   E
 
2016
        #     |
 
2017
        #     F     Add a symlink 'foo' => 'bar'
 
2018
        # Have to use a real WT, because BranchBuilder and MemoryTree don't
 
2019
        # have symlink support
 
2020
        builder = self.get_builder()
 
2021
        builder.build_snapshot('A-id', None,
 
2022
            [('add', (u'', 'a-root-id', 'directory', None))])
 
2023
        builder.build_snapshot('C-id', ['A-id'], [])
 
2024
        builder.build_snapshot('B-id', ['A-id'], [])
 
2025
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
2026
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
2027
        # Have to use a real WT, because BranchBuilder doesn't support exec bit
 
2028
        wt = self.get_wt_from_builder(builder)
 
2029
        os.symlink('bar', 'path/foo')
 
2030
        wt.add(['foo'], ['foo-id'])
 
2031
        self.assertEqual('bar', wt.get_symlink_target('foo-id'))
 
2032
        wt.commit('add symlink', rev_id='F-id')
 
2033
        # Reset to D, so that we can merge F
 
2034
        wt.set_parent_ids(['D-id'])
 
2035
        wt.branch.set_last_revision_info(3, 'D-id')
 
2036
        wt.revert()
 
2037
        self.assertIs(None, wt.path2id('foo'))
 
2038
        conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
 
2039
        self.assertEqual(0, conflicts)
 
2040
        self.assertEqual('foo-id', wt.path2id('foo'))
 
2041
        self.assertEqual('bar', wt.get_symlink_target('foo-id'))
 
2042
 
 
2043
    def test_both_sides_revert(self):
 
2044
        # Both sides of a criss-cross revert the text to the lca
 
2045
        #       A    base, introduces 'foo'
 
2046
        #       |\
 
2047
        #       B C  B modifies 'foo', C modifies 'foo'
 
2048
        #       |X|
 
2049
        #       D E  D reverts to B, E reverts to C
 
2050
        # This should conflict
 
2051
        # This must be done with a real WorkingTree, because normally their
 
2052
        # inventory contains "None" rather than a real sha1
 
2053
        builder = self.get_builder()
 
2054
        builder.build_snapshot('A-id', None,
 
2055
            [('add', (u'', 'a-root-id', 'directory', None)),
 
2056
             ('add', (u'foo', 'foo-id', 'file', 'A content\n'))])
 
2057
        builder.build_snapshot('B-id', ['A-id'],
 
2058
            [('modify', ('foo-id', 'B content\n'))])
 
2059
        builder.build_snapshot('C-id', ['A-id'],
 
2060
            [('modify', ('foo-id', 'C content\n'))])
 
2061
        builder.build_snapshot('E-id', ['C-id', 'B-id'], [])
 
2062
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
2063
        wt, conflicts = self.do_merge(builder, 'E-id')
 
2064
        self.assertEqual(1, conflicts)
 
2065
        self.assertEqualDiff('<<<<<<< TREE\n'
 
2066
                             'B content\n'
 
2067
                             '=======\n'
 
2068
                             'C content\n'
 
2069
                             '>>>>>>> MERGE-SOURCE\n',
 
2070
                             wt.get_file_text('foo-id'))
 
2071
 
 
2072
    def test_modified_symlink(self):
 
2073
        self.requireFeature(tests.SymlinkFeature)
 
2074
        #   A       Create symlink foo => bar
 
2075
        #  / \
 
2076
        # B   C     B relinks foo => baz
 
2077
        # |\ /|
 
2078
        # | X |
 
2079
        # |/ \|
 
2080
        # D   E     D & E have foo => baz
 
2081
        #     |
 
2082
        #     F     F changes it to bing
 
2083
        #
 
2084
        # Merging D & F should result in F cleanly overriding D, because D's
 
2085
        # value actually comes from B
 
2086
 
 
2087
        # Have to use a real WT, because BranchBuilder and MemoryTree don't
 
2088
        # have symlink support
 
2089
        wt = self.make_branch_and_tree('path')
 
2090
        wt.lock_write()
 
2091
        self.addCleanup(wt.unlock)
 
2092
        os.symlink('bar', 'path/foo')
 
2093
        wt.add(['foo'], ['foo-id'])
 
2094
        wt.commit('add symlink', rev_id='A-id')
 
2095
        os.remove('path/foo')
 
2096
        os.symlink('baz', 'path/foo')
 
2097
        wt.commit('foo => baz', rev_id='B-id')
 
2098
        wt.set_last_revision('A-id')
 
2099
        wt.branch.set_last_revision_info(1, 'A-id')
 
2100
        wt.revert()
 
2101
        wt.commit('C', rev_id='C-id')
 
2102
        wt.merge_from_branch(wt.branch, 'B-id')
 
2103
        self.assertEqual('baz', wt.get_symlink_target('foo-id'))
 
2104
        wt.commit('E merges C & B', rev_id='E-id')
 
2105
        os.remove('path/foo')
 
2106
        os.symlink('bing', 'path/foo')
 
2107
        wt.commit('F foo => bing', rev_id='F-id')
 
2108
        wt.set_last_revision('B-id')
 
2109
        wt.branch.set_last_revision_info(2, 'B-id')
 
2110
        wt.revert()
 
2111
        wt.merge_from_branch(wt.branch, 'C-id')
 
2112
        wt.commit('D merges B & C', rev_id='D-id')
 
2113
        conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
 
2114
        self.expectFailure("Merge3Merger doesn't use lcas for symlink content",
 
2115
            self.assertEqual, 0, conflicts)
 
2116
        self.assertEqual('bing', wt.get_symlink_target('foo-id'))
 
2117
 
 
2118
    def test_renamed_symlink(self):
 
2119
        self.requireFeature(tests.SymlinkFeature)
 
2120
        #   A       Create symlink foo => bar
 
2121
        #  / \
 
2122
        # B   C     B renames foo => barry
 
2123
        # |\ /|
 
2124
        # | X |
 
2125
        # |/ \|
 
2126
        # D   E     D & E have barry
 
2127
        #     |
 
2128
        #     F     F renames barry to blah
 
2129
        #
 
2130
        # Merging D & F should result in F cleanly overriding D, because D's
 
2131
        # value actually comes from B
 
2132
 
 
2133
        wt = self.make_branch_and_tree('path')
 
2134
        wt.lock_write()
 
2135
        self.addCleanup(wt.unlock)
 
2136
        os.symlink('bar', 'path/foo')
 
2137
        wt.add(['foo'], ['foo-id'])
 
2138
        wt.commit('A add symlink', rev_id='A-id')
 
2139
        wt.rename_one('foo', 'barry')
 
2140
        wt.commit('B foo => barry', rev_id='B-id')
 
2141
        wt.set_last_revision('A-id')
 
2142
        wt.branch.set_last_revision_info(1, 'A-id')
 
2143
        wt.revert()
 
2144
        wt.commit('C', rev_id='C-id')
 
2145
        wt.merge_from_branch(wt.branch, 'B-id')
 
2146
        self.assertEqual('barry', wt.id2path('foo-id'))
 
2147
        self.assertEqual('bar', wt.get_symlink_target('foo-id'))
 
2148
        wt.commit('E merges C & B', rev_id='E-id')
 
2149
        wt.rename_one('barry', 'blah')
 
2150
        wt.commit('F barry => blah', rev_id='F-id')
 
2151
        wt.set_last_revision('B-id')
 
2152
        wt.branch.set_last_revision_info(2, 'B-id')
 
2153
        wt.revert()
 
2154
        wt.merge_from_branch(wt.branch, 'C-id')
 
2155
        wt.commit('D merges B & C', rev_id='D-id')
 
2156
        self.assertEqual('barry', wt.id2path('foo-id'))
 
2157
        # Check the output of the Merger object directly
 
2158
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2159
            wt, 'F-id')
 
2160
        merger.merge_type = _mod_merge.Merge3Merger
 
2161
        merge_obj = merger.make_merger()
 
2162
        root_id = wt.path2id('')
 
2163
        entries = list(merge_obj._entries_lca())
 
2164
        # No content change, just a path change
 
2165
        self.assertEqual([('foo-id', False,
 
2166
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2167
                           ((u'foo', [u'barry', u'foo']), u'blah', u'barry'),
 
2168
                           ((False, [False, False]), False, False)),
 
2169
                         ], entries)
 
2170
        conflicts = wt.merge_from_branch(wt.branch, to_revision='F-id')
 
2171
        self.assertEqual(0, conflicts)
 
2172
        self.assertEqual('blah', wt.id2path('foo-id'))
 
2173
 
 
2174
    def test_symlink_no_content_change(self):
 
2175
        self.requireFeature(tests.SymlinkFeature)
 
2176
        #   A       Create symlink foo => bar
 
2177
        #  / \
 
2178
        # B   C     B relinks foo => baz
 
2179
        # |\ /|
 
2180
        # | X |
 
2181
        # |/ \|
 
2182
        # D   E     D & E have foo => baz
 
2183
        # |
 
2184
        # F         F has foo => bing
 
2185
        #
 
2186
        # Merging E into F should not cause a conflict, because E doesn't have
 
2187
        # a content change relative to the LCAs (it does relative to A)
 
2188
        wt = self.make_branch_and_tree('path')
 
2189
        wt.lock_write()
 
2190
        self.addCleanup(wt.unlock)
 
2191
        os.symlink('bar', 'path/foo')
 
2192
        wt.add(['foo'], ['foo-id'])
 
2193
        wt.commit('add symlink', rev_id='A-id')
 
2194
        os.remove('path/foo')
 
2195
        os.symlink('baz', 'path/foo')
 
2196
        wt.commit('foo => baz', rev_id='B-id')
 
2197
        wt.set_last_revision('A-id')
 
2198
        wt.branch.set_last_revision_info(1, 'A-id')
 
2199
        wt.revert()
 
2200
        wt.commit('C', rev_id='C-id')
 
2201
        wt.merge_from_branch(wt.branch, 'B-id')
 
2202
        self.assertEqual('baz', wt.get_symlink_target('foo-id'))
 
2203
        wt.commit('E merges C & B', rev_id='E-id')
 
2204
        wt.set_last_revision('B-id')
 
2205
        wt.branch.set_last_revision_info(2, 'B-id')
 
2206
        wt.revert()
 
2207
        wt.merge_from_branch(wt.branch, 'C-id')
 
2208
        wt.commit('D merges B & C', rev_id='D-id')
 
2209
        os.remove('path/foo')
 
2210
        os.symlink('bing', 'path/foo')
 
2211
        wt.commit('F foo => bing', rev_id='F-id')
 
2212
 
 
2213
        # Check the output of the Merger object directly
 
2214
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2215
            wt, 'E-id')
 
2216
        merger.merge_type = _mod_merge.Merge3Merger
 
2217
        merge_obj = merger.make_merger()
 
2218
        # Nothing interesting happened in OTHER relative to BASE
 
2219
        self.assertEqual([], list(merge_obj._entries_lca()))
 
2220
        # Now do a real merge, just to test the rest of the stack
 
2221
        conflicts = wt.merge_from_branch(wt.branch, to_revision='E-id')
 
2222
        self.assertEqual(0, conflicts)
 
2223
        self.assertEqual('bing', wt.get_symlink_target('foo-id'))
 
2224
 
 
2225
    def test_symlink_this_changed_kind(self):
 
2226
        self.requireFeature(tests.SymlinkFeature)
 
2227
        #   A       Nothing
 
2228
        #  / \
 
2229
        # B   C     B creates symlink foo => bar
 
2230
        # |\ /|
 
2231
        # | X |
 
2232
        # |/ \|
 
2233
        # D   E     D changes foo into a file, E has foo => bing
 
2234
        #
 
2235
        # Mostly, this is trying to test that we don't try to os.readlink() on
 
2236
        # a file, or when there is nothing there
 
2237
        wt = self.make_branch_and_tree('path')
 
2238
        wt.lock_write()
 
2239
        self.addCleanup(wt.unlock)
 
2240
        wt.commit('base', rev_id='A-id')
 
2241
        os.symlink('bar', 'path/foo')
 
2242
        wt.add(['foo'], ['foo-id'])
 
2243
        wt.commit('add symlink foo => bar', rev_id='B-id')
 
2244
        wt.set_last_revision('A-id')
 
2245
        wt.branch.set_last_revision_info(1, 'A-id')
 
2246
        wt.revert()
 
2247
        wt.commit('C', rev_id='C-id')
 
2248
        wt.merge_from_branch(wt.branch, 'B-id')
 
2249
        self.assertEqual('bar', wt.get_symlink_target('foo-id'))
 
2250
        os.remove('path/foo')
 
2251
        # We have to change the link in E, or it won't try to do a comparison
 
2252
        os.symlink('bing', 'path/foo')
 
2253
        wt.commit('E merges C & B, overrides to bing', rev_id='E-id')
 
2254
        wt.set_last_revision('B-id')
 
2255
        wt.branch.set_last_revision_info(2, 'B-id')
 
2256
        wt.revert()
 
2257
        wt.merge_from_branch(wt.branch, 'C-id')
 
2258
        os.remove('path/foo')
 
2259
        self.build_tree_contents([('path/foo', 'file content\n')])
 
2260
        # XXX: workaround, WT doesn't detect kind changes unless you do
 
2261
        # iter_changes()
 
2262
        list(wt.iter_changes(wt.basis_tree()))
 
2263
        wt.commit('D merges B & C, makes it a file', rev_id='D-id')
 
2264
 
 
2265
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2266
            wt, 'E-id')
 
2267
        merger.merge_type = _mod_merge.Merge3Merger
 
2268
        merge_obj = merger.make_merger()
 
2269
        entries = list(merge_obj._entries_lca())
 
2270
        root_id = wt.path2id('')
 
2271
        self.assertEqual([('foo-id', True,
 
2272
                           ((None, [root_id, None]), root_id, root_id),
 
2273
                           ((None, [u'foo', None]), u'foo', u'foo'),
 
2274
                           ((None, [False, None]), False, False)),
 
2275
                         ], entries)
 
2276
 
 
2277
    def test_symlink_all_wt(self):
 
2278
        """Check behavior if all trees are Working Trees."""
 
2279
        self.requireFeature(tests.SymlinkFeature)
 
2280
        # The big issue is that entry.symlink_target is None for WorkingTrees.
 
2281
        # So we need to make sure we handle that case correctly.
 
2282
        #   A   foo => bar
 
2283
        #   |\
 
2284
        #   B C B relinks foo => baz
 
2285
        #   |X|
 
2286
        #   D E D & E have foo => baz
 
2287
        #     |
 
2288
        #     F F changes it to bing
 
2289
        # Merging D & F should result in F cleanly overriding D, because D's
 
2290
        # value actually comes from B
 
2291
 
 
2292
        wt = self.make_branch_and_tree('path')
 
2293
        wt.lock_write()
 
2294
        self.addCleanup(wt.unlock)
 
2295
        os.symlink('bar', 'path/foo')
 
2296
        wt.add(['foo'], ['foo-id'])
 
2297
        wt.commit('add symlink', rev_id='A-id')
 
2298
        os.remove('path/foo')
 
2299
        os.symlink('baz', 'path/foo')
 
2300
        wt.commit('foo => baz', rev_id='B-id')
 
2301
        wt.set_last_revision('A-id')
 
2302
        wt.branch.set_last_revision_info(1, 'A-id')
 
2303
        wt.revert()
 
2304
        wt.commit('C', rev_id='C-id')
 
2305
        wt.merge_from_branch(wt.branch, 'B-id')
 
2306
        self.assertEqual('baz', wt.get_symlink_target('foo-id'))
 
2307
        wt.commit('E merges C & B', rev_id='E-id')
 
2308
        os.remove('path/foo')
 
2309
        os.symlink('bing', 'path/foo')
 
2310
        wt.commit('F foo => bing', rev_id='F-id')
 
2311
        wt.set_last_revision('B-id')
 
2312
        wt.branch.set_last_revision_info(2, 'B-id')
 
2313
        wt.revert()
 
2314
        wt.merge_from_branch(wt.branch, 'C-id')
 
2315
        wt.commit('D merges B & C', rev_id='D-id')
 
2316
        wt_base = wt.bzrdir.sprout('base', 'A-id').open_workingtree()
 
2317
        wt_base.lock_read()
 
2318
        self.addCleanup(wt_base.unlock)
 
2319
        wt_lca1 = wt.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
 
2320
        wt_lca1.lock_read()
 
2321
        self.addCleanup(wt_lca1.unlock)
 
2322
        wt_lca2 = wt.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
 
2323
        wt_lca2.lock_read()
 
2324
        self.addCleanup(wt_lca2.unlock)
 
2325
        wt_other = wt.bzrdir.sprout('other', 'F-id').open_workingtree()
 
2326
        wt_other.lock_read()
 
2327
        self.addCleanup(wt_other.unlock)
 
2328
        merge_obj = _mod_merge.Merge3Merger(wt, wt, wt_base,
 
2329
            wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
 
2330
        entries = list(merge_obj._entries_lca())
 
2331
        root_id = wt.path2id('')
 
2332
        self.assertEqual([('foo-id', True,
 
2333
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2334
                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
 
2335
                           ((False, [False, False]), False, False)),
 
2336
                         ], entries)
 
2337
 
 
2338
    def test_other_reverted_path_to_base(self):
 
2339
        #   A       Path at 'foo'
 
2340
        #  / \
 
2341
        # B   C     Path at 'bar' in B
 
2342
        # |\ /|
 
2343
        # | X |
 
2344
        # |/ \|
 
2345
        # D   E     Path at 'bar'
 
2346
        #     |
 
2347
        #     F     Path at 'foo'
 
2348
        builder = self.get_builder()
 
2349
        builder.build_snapshot('A-id', None,
 
2350
            [('add', (u'', 'a-root-id', 'directory', None)),
 
2351
             ('add', (u'foo', 'foo-id', 'file', 'a\nb\nc\n'))])
 
2352
        builder.build_snapshot('C-id', ['A-id'], [])
 
2353
        builder.build_snapshot('B-id', ['A-id'],
 
2354
            [('rename', ('foo', 'bar'))])
 
2355
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
2356
            [('rename', ('foo', 'bar'))]) # merge the rename
 
2357
        builder.build_snapshot('F-id', ['E-id'],
 
2358
            [('rename', ('bar', 'foo'))]) # Rename back to BASE
 
2359
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
2360
        wt, conflicts = self.do_merge(builder, 'F-id')
 
2361
        self.assertEqual(0, conflicts)
 
2362
        self.assertEqual('foo', wt.id2path('foo-id'))
 
2363
 
 
2364
    def test_other_reverted_content_to_base(self):
 
2365
        builder = self.get_builder()
 
2366
        builder.build_snapshot('A-id', None,
 
2367
            [('add', (u'', 'a-root-id', 'directory', None)),
 
2368
             ('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
 
2369
        builder.build_snapshot('C-id', ['A-id'], [])
 
2370
        builder.build_snapshot('B-id', ['A-id'],
 
2371
            [('modify', ('foo-id', 'B content\n'))])
 
2372
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
2373
            [('modify', ('foo-id', 'B content\n'))]) # merge the content
 
2374
        builder.build_snapshot('F-id', ['E-id'],
 
2375
            [('modify', ('foo-id', 'base content\n'))]) # Revert back to BASE
 
2376
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
2377
        wt, conflicts = self.do_merge(builder, 'F-id')
 
2378
        self.assertEqual(0, conflicts)
 
2379
        # TODO: We need to use the per-file graph to properly select a BASE
 
2380
        #       before this will work. Or at least use the LCA trees to find
 
2381
        #       the appropriate content base. (which is B, not A).
 
2382
        self.expectFailure("Merge3Merger doesn't recognize reverted content",
 
2383
            self.assertEqual, 'base content\n', wt.get_file_text('foo-id'))
 
2384
 
 
2385
    def test_other_modified_content(self):
 
2386
        builder = self.get_builder()
 
2387
        builder.build_snapshot('A-id', None,
 
2388
            [('add', (u'', 'a-root-id', 'directory', None)),
 
2389
             ('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
 
2390
        builder.build_snapshot('C-id', ['A-id'], [])
 
2391
        builder.build_snapshot('B-id', ['A-id'],
 
2392
            [('modify', ('foo-id', 'B content\n'))])
 
2393
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
2394
            [('modify', ('foo-id', 'B content\n'))]) # merge the content
 
2395
        builder.build_snapshot('F-id', ['E-id'],
 
2396
            [('modify', ('foo-id', 'F content\n'))]) # Override B content
 
2397
        builder.build_snapshot('D-id', ['B-id', 'C-id'], [])
 
2398
        wt, conflicts = self.do_merge(builder, 'F-id')
 
2399
        self.expectFailure("Merge3Merger only uses BASE for content",
 
2400
            self.assertEqual, 'F content\n', wt.get_file_text('foo-id'))
 
2401
        self.assertEqual(0, conflicts)
 
2402
        self.assertEqual('F content\n', wt.get_file_text('foo-id'))
 
2403
 
 
2404
    def test_all_wt(self):
 
2405
        """Check behavior if all trees are Working Trees."""
 
2406
        # The big issue is that entry.revision is None for WorkingTrees. (as is
 
2407
        # entry.text_sha1, etc. So we need to make sure we handle that case
 
2408
        # correctly.
 
2409
        #   A   Content of 'foo', path of 'a'
 
2410
        #   |\
 
2411
        #   B C B modifies content, C renames 'a' => 'b'
 
2412
        #   |X|
 
2413
        #   D E E updates content, renames 'b' => 'c'
 
2414
        builder = self.get_builder()
 
2415
        builder.build_snapshot('A-id', None,
 
2416
            [('add', (u'', 'a-root-id', 'directory', None)),
 
2417
             ('add', (u'a', 'a-id', 'file', 'base content\n')),
 
2418
             ('add', (u'foo', 'foo-id', 'file', 'base content\n'))])
 
2419
        builder.build_snapshot('B-id', ['A-id'],
 
2420
            [('modify', ('foo-id', 'B content\n'))])
 
2421
        builder.build_snapshot('C-id', ['A-id'],
 
2422
            [('rename', ('a', 'b'))])
 
2423
        builder.build_snapshot('E-id', ['C-id', 'B-id'],
 
2424
            [('rename', ('b', 'c')),
 
2425
             ('modify', ('foo-id', 'E content\n'))])
 
2426
        builder.build_snapshot('D-id', ['B-id', 'C-id'],
 
2427
            [('rename', ('a', 'b'))]) # merged change
 
2428
        wt_this = self.get_wt_from_builder(builder)
 
2429
        wt_base = wt_this.bzrdir.sprout('base', 'A-id').open_workingtree()
 
2430
        wt_base.lock_read()
 
2431
        self.addCleanup(wt_base.unlock)
 
2432
        wt_lca1 = wt_this.bzrdir.sprout('b-tree', 'B-id').open_workingtree()
 
2433
        wt_lca1.lock_read()
 
2434
        self.addCleanup(wt_lca1.unlock)
 
2435
        wt_lca2 = wt_this.bzrdir.sprout('c-tree', 'C-id').open_workingtree()
 
2436
        wt_lca2.lock_read()
 
2437
        self.addCleanup(wt_lca2.unlock)
 
2438
        wt_other = wt_this.bzrdir.sprout('other', 'E-id').open_workingtree()
 
2439
        wt_other.lock_read()
 
2440
        self.addCleanup(wt_other.unlock)
 
2441
        merge_obj = _mod_merge.Merge3Merger(wt_this, wt_this, wt_base,
 
2442
            wt_other, lca_trees=[wt_lca1, wt_lca2], do_merge=False)
 
2443
        entries = list(merge_obj._entries_lca())
 
2444
        root_id = 'a-root-id'
 
2445
        self.assertEqual([('a-id', False,
 
2446
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2447
                           ((u'a', [u'a', u'b']), u'c', u'b'),
 
2448
                           ((False, [False, False]), False, False)),
 
2449
                          ('foo-id', True,
 
2450
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2451
                           ((u'foo', [u'foo', u'foo']), u'foo', u'foo'),
 
2452
                           ((False, [False, False]), False, False)),
 
2453
                         ], entries)
 
2454
 
 
2455
    def test_nested_tree_unmodified(self):
 
2456
        # Tested with a real WT, because BranchBuilder/MemoryTree don't handle
 
2457
        # 'tree-reference'
 
2458
        wt = self.make_branch_and_tree('tree',
 
2459
            format='dirstate-with-subtree')
 
2460
        wt.lock_write()
 
2461
        self.addCleanup(wt.unlock)
 
2462
        sub_tree = self.make_branch_and_tree('tree/sub-tree',
 
2463
            format='dirstate-with-subtree')
 
2464
        wt.set_root_id('a-root-id')
 
2465
        sub_tree.set_root_id('sub-tree-root')
 
2466
        self.build_tree_contents([('tree/sub-tree/file', 'text1')])
 
2467
        sub_tree.add('file')
 
2468
        sub_tree.commit('foo', rev_id='sub-A-id')
 
2469
        wt.add_reference(sub_tree)
 
2470
        wt.commit('set text to 1', rev_id='A-id', recursive=None)
 
2471
        # Now create a criss-cross merge in the parent, without modifying the
 
2472
        # subtree
 
2473
        wt.commit('B', rev_id='B-id', recursive=None)
 
2474
        wt.set_last_revision('A-id')
 
2475
        wt.branch.set_last_revision_info(1, 'A-id')
 
2476
        wt.commit('C', rev_id='C-id', recursive=None)
 
2477
        wt.merge_from_branch(wt.branch, to_revision='B-id')
 
2478
        wt.commit('E', rev_id='E-id', recursive=None)
 
2479
        wt.set_parent_ids(['B-id', 'C-id'])
 
2480
        wt.branch.set_last_revision_info(2, 'B-id')
 
2481
        wt.commit('D', rev_id='D-id', recursive=None)
 
2482
 
 
2483
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2484
            wt, 'E-id')
 
2485
        merger.merge_type = _mod_merge.Merge3Merger
 
2486
        merge_obj = merger.make_merger()
 
2487
        entries = list(merge_obj._entries_lca())
 
2488
        self.assertEqual([], entries)
 
2489
 
 
2490
    def test_nested_tree_subtree_modified(self):
 
2491
        # Tested with a real WT, because BranchBuilder/MemoryTree don't handle
 
2492
        # 'tree-reference'
 
2493
        wt = self.make_branch_and_tree('tree',
 
2494
            format='dirstate-with-subtree')
 
2495
        wt.lock_write()
 
2496
        self.addCleanup(wt.unlock)
 
2497
        sub_tree = self.make_branch_and_tree('tree/sub',
 
2498
            format='dirstate-with-subtree')
 
2499
        wt.set_root_id('a-root-id')
 
2500
        sub_tree.set_root_id('sub-tree-root')
 
2501
        self.build_tree_contents([('tree/sub/file', 'text1')])
 
2502
        sub_tree.add('file')
 
2503
        sub_tree.commit('foo', rev_id='sub-A-id')
 
2504
        wt.add_reference(sub_tree)
 
2505
        wt.commit('set text to 1', rev_id='A-id', recursive=None)
 
2506
        # Now create a criss-cross merge in the parent, without modifying the
 
2507
        # subtree
 
2508
        wt.commit('B', rev_id='B-id', recursive=None)
 
2509
        wt.set_last_revision('A-id')
 
2510
        wt.branch.set_last_revision_info(1, 'A-id')
 
2511
        wt.commit('C', rev_id='C-id', recursive=None)
 
2512
        wt.merge_from_branch(wt.branch, to_revision='B-id')
 
2513
        self.build_tree_contents([('tree/sub/file', 'text2')])
 
2514
        sub_tree.commit('modify contents', rev_id='sub-B-id')
 
2515
        wt.commit('E', rev_id='E-id', recursive=None)
 
2516
        wt.set_parent_ids(['B-id', 'C-id'])
 
2517
        wt.branch.set_last_revision_info(2, 'B-id')
 
2518
        wt.commit('D', rev_id='D-id', recursive=None)
 
2519
 
 
2520
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2521
            wt, 'E-id')
 
2522
        merger.merge_type = _mod_merge.Merge3Merger
 
2523
        merge_obj = merger.make_merger()
 
2524
        entries = list(merge_obj._entries_lca())
 
2525
        # Nothing interesting about this sub-tree, because content changes are
 
2526
        # computed at a higher level
 
2527
        self.assertEqual([], entries)
 
2528
 
 
2529
    def test_nested_tree_subtree_renamed(self):
 
2530
        # Tested with a real WT, because BranchBuilder/MemoryTree don't handle
 
2531
        # 'tree-reference'
 
2532
        wt = self.make_branch_and_tree('tree',
 
2533
            format='dirstate-with-subtree')
 
2534
        wt.lock_write()
 
2535
        self.addCleanup(wt.unlock)
 
2536
        sub_tree = self.make_branch_and_tree('tree/sub',
 
2537
            format='dirstate-with-subtree')
 
2538
        wt.set_root_id('a-root-id')
 
2539
        sub_tree.set_root_id('sub-tree-root')
 
2540
        self.build_tree_contents([('tree/sub/file', 'text1')])
 
2541
        sub_tree.add('file')
 
2542
        sub_tree.commit('foo', rev_id='sub-A-id')
 
2543
        wt.add_reference(sub_tree)
 
2544
        wt.commit('set text to 1', rev_id='A-id', recursive=None)
 
2545
        # Now create a criss-cross merge in the parent, without modifying the
 
2546
        # subtree
 
2547
        wt.commit('B', rev_id='B-id', recursive=None)
 
2548
        wt.set_last_revision('A-id')
 
2549
        wt.branch.set_last_revision_info(1, 'A-id')
 
2550
        wt.commit('C', rev_id='C-id', recursive=None)
 
2551
        wt.merge_from_branch(wt.branch, to_revision='B-id')
 
2552
        wt.rename_one('sub', 'alt_sub')
 
2553
        wt.commit('E', rev_id='E-id', recursive=None)
 
2554
        wt.set_last_revision('B-id')
 
2555
        wt.revert()
 
2556
        wt.set_parent_ids(['B-id', 'C-id'])
 
2557
        wt.branch.set_last_revision_info(2, 'B-id')
 
2558
        wt.commit('D', rev_id='D-id', recursive=None)
 
2559
 
 
2560
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2561
            wt, 'E-id')
 
2562
        merger.merge_type = _mod_merge.Merge3Merger
 
2563
        merge_obj = merger.make_merger()
 
2564
        entries = list(merge_obj._entries_lca())
 
2565
        root_id = 'a-root-id'
 
2566
        self.assertEqual([('sub-tree-root', False,
 
2567
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2568
                           ((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
 
2569
                           ((False, [False, False]), False, False)),
 
2570
                         ], entries)
 
2571
 
 
2572
    def test_nested_tree_subtree_renamed_and_modified(self):
 
2573
        # Tested with a real WT, because BranchBuilder/MemoryTree don't handle
 
2574
        # 'tree-reference'
 
2575
        wt = self.make_branch_and_tree('tree',
 
2576
            format='dirstate-with-subtree')
 
2577
        wt.lock_write()
 
2578
        self.addCleanup(wt.unlock)
 
2579
        sub_tree = self.make_branch_and_tree('tree/sub',
 
2580
            format='dirstate-with-subtree')
 
2581
        wt.set_root_id('a-root-id')
 
2582
        sub_tree.set_root_id('sub-tree-root')
 
2583
        self.build_tree_contents([('tree/sub/file', 'text1')])
 
2584
        sub_tree.add('file')
 
2585
        sub_tree.commit('foo', rev_id='sub-A-id')
 
2586
        wt.add_reference(sub_tree)
 
2587
        wt.commit('set text to 1', rev_id='A-id', recursive=None)
 
2588
        # Now create a criss-cross merge in the parent, without modifying the
 
2589
        # subtree
 
2590
        wt.commit('B', rev_id='B-id', recursive=None)
 
2591
        wt.set_last_revision('A-id')
 
2592
        wt.branch.set_last_revision_info(1, 'A-id')
 
2593
        wt.commit('C', rev_id='C-id', recursive=None)
 
2594
        wt.merge_from_branch(wt.branch, to_revision='B-id')
 
2595
        self.build_tree_contents([('tree/sub/file', 'text2')])
 
2596
        sub_tree.commit('modify contents', rev_id='sub-B-id')
 
2597
        wt.rename_one('sub', 'alt_sub')
 
2598
        wt.commit('E', rev_id='E-id', recursive=None)
 
2599
        wt.set_last_revision('B-id')
 
2600
        wt.revert()
 
2601
        wt.set_parent_ids(['B-id', 'C-id'])
 
2602
        wt.branch.set_last_revision_info(2, 'B-id')
 
2603
        wt.commit('D', rev_id='D-id', recursive=None)
 
2604
 
 
2605
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2606
            wt, 'E-id')
 
2607
        merger.merge_type = _mod_merge.Merge3Merger
 
2608
        merge_obj = merger.make_merger()
 
2609
        entries = list(merge_obj._entries_lca())
 
2610
        root_id = 'a-root-id'
 
2611
        self.assertEqual([('sub-tree-root', False,
 
2612
                           ((root_id, [root_id, root_id]), root_id, root_id),
 
2613
                           ((u'sub', [u'sub', u'sub']), u'alt_sub', u'sub'),
 
2614
                           ((False, [False, False]), False, False)),
 
2615
                         ], entries)
 
2616
 
 
2617
 
 
2618
class TestLCAMultiWay(tests.TestCase):
 
2619
 
 
2620
    def assertLCAMultiWay(self, expected, base, lcas, other, this,
 
2621
                          allow_overriding_lca=True):
 
2622
        self.assertEqual(expected, _mod_merge.Merge3Merger._lca_multi_way(
 
2623
                                (base, lcas), other, this,
 
2624
                                allow_overriding_lca=allow_overriding_lca))
 
2625
 
 
2626
    def test_other_equal_equal_lcas(self):
 
2627
        """Test when OTHER=LCA and all LCAs are identical."""
 
2628
        self.assertLCAMultiWay('this',
 
2629
            'bval', ['bval', 'bval'], 'bval', 'bval')
 
2630
        self.assertLCAMultiWay('this',
 
2631
            'bval', ['lcaval', 'lcaval'], 'lcaval', 'bval')
 
2632
        self.assertLCAMultiWay('this',
 
2633
            'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'bval')
 
2634
        self.assertLCAMultiWay('this',
 
2635
            'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', 'tval')
 
2636
        self.assertLCAMultiWay('this',
 
2637
            'bval', ['lcaval', 'lcaval', 'lcaval'], 'lcaval', None)
 
2638
 
 
2639
    def test_other_equal_this(self):
 
2640
        """Test when other and this are identical."""
 
2641
        self.assertLCAMultiWay('this',
 
2642
            'bval', ['bval', 'bval'], 'oval', 'oval')
 
2643
        self.assertLCAMultiWay('this',
 
2644
            'bval', ['lcaval', 'lcaval'], 'oval', 'oval')
 
2645
        self.assertLCAMultiWay('this',
 
2646
            'bval', ['cval', 'dval'], 'oval', 'oval')
 
2647
        self.assertLCAMultiWay('this',
 
2648
            'bval', [None, 'lcaval'], 'oval', 'oval')
 
2649
        self.assertLCAMultiWay('this',
 
2650
            None, [None, 'lcaval'], 'oval', 'oval')
 
2651
        self.assertLCAMultiWay('this',
 
2652
            None, ['lcaval', 'lcaval'], 'oval', 'oval')
 
2653
        self.assertLCAMultiWay('this',
 
2654
            None, ['cval', 'dval'], 'oval', 'oval')
 
2655
        self.assertLCAMultiWay('this',
 
2656
            None, ['cval', 'dval'], None, None)
 
2657
        self.assertLCAMultiWay('this',
 
2658
            None, ['cval', 'dval', 'eval', 'fval'], 'oval', 'oval')
 
2659
 
 
2660
    def test_no_lcas(self):
 
2661
        self.assertLCAMultiWay('this',
 
2662
            'bval', [], 'bval', 'tval')
 
2663
        self.assertLCAMultiWay('other',
 
2664
            'bval', [], 'oval', 'bval')
 
2665
        self.assertLCAMultiWay('conflict',
 
2666
            'bval', [], 'oval', 'tval')
 
2667
        self.assertLCAMultiWay('this',
 
2668
            'bval', [], 'oval', 'oval')
 
2669
 
 
2670
    def test_lca_supersedes_other_lca(self):
 
2671
        """If one lca == base, the other lca takes precedence"""
 
2672
        self.assertLCAMultiWay('this',
 
2673
            'bval', ['bval', 'lcaval'], 'lcaval', 'tval')
 
2674
        self.assertLCAMultiWay('this',
 
2675
            'bval', ['bval', 'lcaval'], 'lcaval', 'bval')
 
2676
        # This is actually considered a 'revert' because the 'lcaval' in LCAS
 
2677
        # supersedes the BASE val (in the other LCA) but then OTHER reverts it
 
2678
        # back to bval.
 
2679
        self.assertLCAMultiWay('other',
 
2680
            'bval', ['bval', 'lcaval'], 'bval', 'lcaval')
 
2681
        self.assertLCAMultiWay('conflict',
 
2682
            'bval', ['bval', 'lcaval'], 'bval', 'tval')
 
2683
 
 
2684
    def test_other_and_this_pick_different_lca(self):
 
2685
        # OTHER and THIS resolve the lca conflict in different ways
 
2686
        self.assertLCAMultiWay('conflict',
 
2687
            'bval', ['lca1val', 'lca2val'], 'lca1val', 'lca2val')
 
2688
        self.assertLCAMultiWay('conflict',
 
2689
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'lca2val')
 
2690
        self.assertLCAMultiWay('conflict',
 
2691
            'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'lca2val')
 
2692
 
 
2693
    def test_other_in_lca(self):
 
2694
        # OTHER takes a value of one of the LCAs, THIS takes a new value, which
 
2695
        # theoretically supersedes both LCA values and 'wins'
 
2696
        self.assertLCAMultiWay('this',
 
2697
            'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval')
 
2698
        self.assertLCAMultiWay('this',
 
2699
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval')
 
2700
        self.assertLCAMultiWay('conflict',
 
2701
            'bval', ['lca1val', 'lca2val'], 'lca1val', 'newval',
 
2702
            allow_overriding_lca=False)
 
2703
        self.assertLCAMultiWay('conflict',
 
2704
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'newval',
 
2705
            allow_overriding_lca=False)
 
2706
        # THIS reverted back to BASE, but that is an explicit supersede of all
 
2707
        # LCAs
 
2708
        self.assertLCAMultiWay('this',
 
2709
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval')
 
2710
        self.assertLCAMultiWay('this',
 
2711
            'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval')
 
2712
        self.assertLCAMultiWay('conflict',
 
2713
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'lca1val', 'bval',
 
2714
            allow_overriding_lca=False)
 
2715
        self.assertLCAMultiWay('conflict',
 
2716
            'bval', ['lca1val', 'lca2val', 'bval'], 'lca1val', 'bval',
 
2717
            allow_overriding_lca=False)
 
2718
 
 
2719
    def test_this_in_lca(self):
 
2720
        # THIS takes a value of one of the LCAs, OTHER takes a new value, which
 
2721
        # theoretically supersedes both LCA values and 'wins'
 
2722
        self.assertLCAMultiWay('other',
 
2723
            'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val')
 
2724
        self.assertLCAMultiWay('other',
 
2725
            'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val')
 
2726
        self.assertLCAMultiWay('conflict',
 
2727
            'bval', ['lca1val', 'lca2val'], 'oval', 'lca1val',
 
2728
            allow_overriding_lca=False)
 
2729
        self.assertLCAMultiWay('conflict',
 
2730
            'bval', ['lca1val', 'lca2val'], 'oval', 'lca2val',
 
2731
            allow_overriding_lca=False)
 
2732
        # OTHER reverted back to BASE, but that is an explicit supersede of all
 
2733
        # LCAs
 
2734
        self.assertLCAMultiWay('other',
 
2735
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val')
 
2736
        self.assertLCAMultiWay('conflict',
 
2737
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'bval', 'lca3val',
 
2738
            allow_overriding_lca=False)
 
2739
 
 
2740
    def test_all_differ(self):
 
2741
        self.assertLCAMultiWay('conflict',
 
2742
            'bval', ['lca1val', 'lca2val'], 'oval', 'tval')
 
2743
        self.assertLCAMultiWay('conflict',
 
2744
            'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval', 'tval')
 
2745
        self.assertLCAMultiWay('conflict',
 
2746
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval', 'tval')