~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_merge.py

  • Committer: John Arbash Meinel
  • Date: 2010-08-13 19:08:57 UTC
  • mto: (5050.17.7 2.2)
  • mto: This revision was merged to the branch mainline in revision 5379.
  • Revision ID: john@arbash-meinel.com-20100813190857-mvzwnimrxvm0zimp
Lots of documentation updates.

We had a lot of http links pointing to the old domain. They should
all now be properly updated to the new domain. (only bazaar-vcs.org
entry left is for pqm, which seems to still reside at the old url.)

Also removed one 'TODO' doc entry about switching to binary xdelta, since
we basically did just that with groupcompress.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
18
18
from StringIO import StringIO
19
19
 
20
20
from bzrlib import (
 
21
    branch as _mod_branch,
 
22
    cleanup,
21
23
    conflicts,
22
24
    errors,
 
25
    inventory,
23
26
    knit,
24
27
    memorytree,
25
28
    merge as _mod_merge,
26
29
    option,
27
 
    progress,
 
30
    revision as _mod_revision,
28
31
    tests,
29
32
    transform,
30
33
    versionedfile,
31
34
    )
32
 
from bzrlib.branch import Branch
33
35
from bzrlib.conflicts import ConflictList, TextConflict
34
 
from bzrlib.errors import UnrelatedBranches, NoCommits, BzrCommandError
 
36
from bzrlib.errors import UnrelatedBranches, NoCommits
35
37
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.osutils import basename, pathjoin, file_kind
 
39
from bzrlib.tests import (
 
40
    TestCaseWithMemoryTransport,
 
41
    TestCaseWithTransport,
 
42
    test_merge_core,
 
43
    )
38
44
from bzrlib.workingtree import WorkingTree
39
45
 
40
46
 
151
157
        log = StringIO()
152
158
        merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
153
159
                    this_tree=tree_b, ignore_zero=True)
154
 
        log = self._get_log(keep_log_file=True)
155
 
        self.failUnless('All changes applied successfully.\n' not in log)
 
160
        self.failUnless('All changes applied successfully.\n' not in
 
161
            self.get_log())
156
162
        tree_b.revert()
157
163
        merge_inner(tree_b.branch, tree_a, tree_b.basis_tree(),
158
164
                    this_tree=tree_b, ignore_zero=False)
159
 
        log = self._get_log(keep_log_file=True)
160
 
        self.failUnless('All changes applied successfully.\n' in log)
 
165
        self.failUnless('All changes applied successfully.\n' in self.get_log())
161
166
 
162
167
    def test_merge_inner_conflicts(self):
163
168
        tree_a = self.make_branch_and_tree('a')
218
223
        tree_a.add('file')
219
224
        tree_a.commit('commit base')
220
225
        # basis_tree() is only guaranteed to be valid as long as it is actually
221
 
        # the basis tree. This mutates the tree after grabbing basis, so go to
222
 
        # the repository.
 
226
        # the basis tree. This test commits to the tree after grabbing basis,
 
227
        # so we go to the repository.
223
228
        base_tree = tree_a.branch.repository.revision_tree(tree_a.last_revision())
224
229
        tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
225
230
        self.build_tree_contents([('tree_a/file', 'content_2')])
226
231
        tree_a.commit('commit other')
227
232
        other_tree = tree_a.basis_tree()
 
233
        # 'file' is now missing but isn't altered in any commit in b so no
 
234
        # change should be applied.
228
235
        os.unlink('tree_b/file')
229
236
        merge_inner(tree_b.branch, other_tree, base_tree, this_tree=tree_b)
230
237
 
291
298
        tree_a.commit('commit 2')
292
299
        tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
293
300
        tree_b.rename_one('file_1', 'renamed')
294
 
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
295
 
                                                    progress.DummyProgress())
 
301
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
296
302
        merger.merge_type = _mod_merge.Merge3Merger
297
303
        merger.do_merge()
298
304
        self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
306
312
        tree_a.commit('commit 2')
307
313
        tree_b = tree_a.bzrdir.sprout('b').open_workingtree()
308
314
        tree_b.rename_one('file_1', 'renamed')
309
 
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b,
310
 
                                                    progress.DummyProgress())
 
315
        merger = _mod_merge.Merger.from_uncommitted(tree_a, tree_b)
311
316
        merger.merge_type = _mod_merge.WeaveMerger
312
317
        merger.do_merge()
313
318
        self.assertEqual(tree_a.get_parent_ids(), [tree_b.last_revision()])
314
319
 
315
 
    def test_Merger_defaults_to_DummyProgress(self):
316
 
        branch = self.make_branch('branch')
317
 
        merger = _mod_merge.Merger(branch, pb=None)
318
 
        self.assertIsInstance(merger._pb, progress.DummyProgress)
319
 
 
320
320
    def prepare_cherrypick(self):
321
321
        """Prepare a pair of trees for cherrypicking tests.
322
322
 
343
343
 
344
344
    def test_weave_cherrypick(self):
345
345
        this_tree, other_tree = self.prepare_cherrypick()
346
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
346
        merger = _mod_merge.Merger.from_revision_ids(None,
347
347
            this_tree, 'rev3b', 'rev2b', other_tree.branch)
348
348
        merger.merge_type = _mod_merge.WeaveMerger
349
349
        merger.do_merge()
351
351
 
352
352
    def test_weave_cannot_reverse_cherrypick(self):
353
353
        this_tree, other_tree = self.prepare_cherrypick()
354
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
354
        merger = _mod_merge.Merger.from_revision_ids(None,
355
355
            this_tree, 'rev2b', 'rev3b', other_tree.branch)
356
356
        merger.merge_type = _mod_merge.WeaveMerger
357
357
        self.assertRaises(errors.CannotReverseCherrypick, merger.do_merge)
358
358
 
359
359
    def test_merge3_can_reverse_cherrypick(self):
360
360
        this_tree, other_tree = self.prepare_cherrypick()
361
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
361
        merger = _mod_merge.Merger.from_revision_ids(None,
362
362
            this_tree, 'rev2b', 'rev3b', other_tree.branch)
363
363
        merger.merge_type = _mod_merge.Merge3Merger
364
364
        merger.do_merge()
376
376
        this_tree.lock_write()
377
377
        self.addCleanup(this_tree.unlock)
378
378
 
379
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
379
        merger = _mod_merge.Merger.from_revision_ids(None,
380
380
            this_tree, 'rev3b', 'rev2b', other_tree.branch)
381
381
        merger.merge_type = _mod_merge.Merge3Merger
382
382
        merger.do_merge()
395
395
        other_tree.commit('rev2', rev_id='rev2b')
396
396
        this_tree.lock_write()
397
397
        self.addCleanup(this_tree.unlock)
398
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress,
 
398
        merger = _mod_merge.Merger.from_revision_ids(None,
399
399
            this_tree, 'rev2b', other_branch=other_tree.branch)
400
400
        merger.merge_type = _mod_merge.Merge3Merger
401
401
        tree_merger = merger.make_merger()
415
415
        other_tree.commit('rev2', rev_id='rev2b')
416
416
        this_tree.lock_write()
417
417
        self.addCleanup(this_tree.unlock)
418
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
418
        merger = _mod_merge.Merger.from_revision_ids(None,
419
419
            this_tree, 'rev2b', other_branch=other_tree.branch)
420
420
        merger.merge_type = _mod_merge.Merge3Merger
421
421
        tree_merger = merger.make_merger()
445
445
        other_tree.commit('rev2', rev_id='rev2b')
446
446
        this_tree.lock_write()
447
447
        self.addCleanup(this_tree.unlock)
448
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
448
        merger = _mod_merge.Merger.from_revision_ids(None,
449
449
            this_tree, 'rev2b', other_branch=other_tree.branch)
450
450
        merger.merge_type = _mod_merge.Merge3Merger
451
451
        tree_merger = merger.make_merger()
522
522
        self.add_uncommitted_version(('root', 'C:'), [('root', 'A')], 'fabg')
523
523
        return _PlanMerge('B:', 'C:', self.plan_merge_vf, ('root',))
524
524
 
 
525
    def test_base_from_plan(self):
 
526
        self.setup_plan_merge()
 
527
        plan = self.plan_merge_vf.plan_merge('B', 'C')
 
528
        pwm = versionedfile.PlanWeaveMerge(plan)
 
529
        self.assertEqual(['a\n', 'b\n', 'c\n'], pwm.base_from_plan())
 
530
 
525
531
    def test_unique_lines(self):
526
532
        plan = self.setup_plan_merge()
527
533
        self.assertEqual(plan._unique_lines(
825
831
                          ('unchanged', 'f\n'),
826
832
                          ('unchanged', 'g\n')],
827
833
                         list(plan))
 
834
        plan = self.plan_merge_vf.plan_lca_merge('F', 'G')
 
835
        # This is one of the main differences between plan_merge and
 
836
        # plan_lca_merge. plan_lca_merge generates a conflict for 'x => z',
 
837
        # because 'x' was not present in one of the bases. However, in this
 
838
        # case it is spurious because 'x' does not exist in the global base A.
 
839
        self.assertEqual([
 
840
                          ('unchanged', 'h\n'),
 
841
                          ('unchanged', 'a\n'),
 
842
                          ('conflicted-a', 'x\n'),
 
843
                          ('new-b', 'z\n'),
 
844
                          ('unchanged', 'c\n'),
 
845
                          ('unchanged', 'd\n'),
 
846
                          ('unchanged', 'y\n'),
 
847
                          ('unchanged', 'f\n'),
 
848
                          ('unchanged', 'g\n')],
 
849
                         list(plan))
 
850
 
 
851
    def test_criss_cross_flip_flop(self):
 
852
        # This is specificly trying to trigger problems when using limited
 
853
        # ancestry and weaves. The ancestry graph looks like:
 
854
        #       XX      unused ancestor, should not show up in the weave
 
855
        #       |
 
856
        #       A       Unique LCA
 
857
        #      / \  
 
858
        #     B   C     B & C both introduce a new line
 
859
        #     |\ /|  
 
860
        #     | X |  
 
861
        #     |/ \| 
 
862
        #     D   E     B & C are both merged, so both are common ancestors
 
863
        #               In the process of merging, both sides order the new
 
864
        #               lines differently
 
865
        #
 
866
        self.add_rev('root', 'XX', [], 'qrs')
 
867
        self.add_rev('root', 'A', ['XX'], 'abcdef')
 
868
        self.add_rev('root', 'B', ['A'], 'abcdgef')
 
869
        self.add_rev('root', 'C', ['A'], 'abcdhef')
 
870
        self.add_rev('root', 'D', ['B', 'C'], 'abcdghef')
 
871
        self.add_rev('root', 'E', ['C', 'B'], 'abcdhgef')
 
872
        plan = list(self.plan_merge_vf.plan_merge('D', 'E'))
 
873
        self.assertEqual([
 
874
                          ('unchanged', 'a\n'),
 
875
                          ('unchanged', 'b\n'),
 
876
                          ('unchanged', 'c\n'),
 
877
                          ('unchanged', 'd\n'),
 
878
                          ('new-b', 'h\n'),
 
879
                          ('unchanged', 'g\n'),
 
880
                          ('killed-b', 'h\n'),
 
881
                          ('unchanged', 'e\n'),
 
882
                          ('unchanged', 'f\n'),
 
883
                         ], plan)
 
884
        pwm = versionedfile.PlanWeaveMerge(plan)
 
885
        self.assertEqualDiff('\n'.join('abcdghef') + '\n',
 
886
                             ''.join(pwm.base_from_plan()))
 
887
        # Reversing the order reverses the merge plan, and final order of 'hg'
 
888
        # => 'gh'
 
889
        plan = list(self.plan_merge_vf.plan_merge('E', 'D'))
 
890
        self.assertEqual([
 
891
                          ('unchanged', 'a\n'),
 
892
                          ('unchanged', 'b\n'),
 
893
                          ('unchanged', 'c\n'),
 
894
                          ('unchanged', 'd\n'),
 
895
                          ('new-b', 'g\n'),
 
896
                          ('unchanged', 'h\n'),
 
897
                          ('killed-b', 'g\n'),
 
898
                          ('unchanged', 'e\n'),
 
899
                          ('unchanged', 'f\n'),
 
900
                         ], plan)
 
901
        pwm = versionedfile.PlanWeaveMerge(plan)
 
902
        self.assertEqualDiff('\n'.join('abcdhgef') + '\n',
 
903
                             ''.join(pwm.base_from_plan()))
 
904
        # This is where lca differs, in that it (fairly correctly) determines
 
905
        # that there is a conflict because both sides resolved the merge
 
906
        # differently
 
907
        plan = list(self.plan_merge_vf.plan_lca_merge('D', 'E'))
 
908
        self.assertEqual([
 
909
                          ('unchanged', 'a\n'),
 
910
                          ('unchanged', 'b\n'),
 
911
                          ('unchanged', 'c\n'),
 
912
                          ('unchanged', 'd\n'),
 
913
                          ('conflicted-b', 'h\n'),
 
914
                          ('unchanged', 'g\n'),
 
915
                          ('conflicted-a', 'h\n'),
 
916
                          ('unchanged', 'e\n'),
 
917
                          ('unchanged', 'f\n'),
 
918
                         ], plan)
 
919
        pwm = versionedfile.PlanWeaveMerge(plan)
 
920
        self.assertEqualDiff('\n'.join('abcdgef') + '\n',
 
921
                             ''.join(pwm.base_from_plan()))
 
922
        # Reversing it changes what line is doubled, but still gives a
 
923
        # double-conflict
 
924
        plan = list(self.plan_merge_vf.plan_lca_merge('E', 'D'))
 
925
        self.assertEqual([
 
926
                          ('unchanged', 'a\n'),
 
927
                          ('unchanged', 'b\n'),
 
928
                          ('unchanged', 'c\n'),
 
929
                          ('unchanged', 'd\n'),
 
930
                          ('conflicted-b', 'g\n'),
 
931
                          ('unchanged', 'h\n'),
 
932
                          ('conflicted-a', 'g\n'),
 
933
                          ('unchanged', 'e\n'),
 
934
                          ('unchanged', 'f\n'),
 
935
                         ], plan)
 
936
        pwm = versionedfile.PlanWeaveMerge(plan)
 
937
        self.assertEqualDiff('\n'.join('abcdhef') + '\n',
 
938
                             ''.join(pwm.base_from_plan()))
828
939
 
829
940
    def assertRemoveExternalReferences(self, filtered_parent_map,
830
941
                                       child_map, tails, parent_map):
1030
1141
                         ], list(plan))
1031
1142
 
1032
1143
 
1033
 
class TestMergeImplementation(object):
1034
 
 
1035
 
    def do_merge(self, target_tree, source_tree, **kwargs):
1036
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
1037
 
            target_tree, source_tree.last_revision(),
1038
 
            other_branch=source_tree.branch)
1039
 
        merger.merge_type=self.merge_type
1040
 
        for name, value in kwargs.items():
1041
 
            setattr(merger, name, value)
1042
 
        merger.do_merge()
1043
 
 
1044
 
    def test_merge_specific_file(self):
1045
 
        this_tree = self.make_branch_and_tree('this')
1046
 
        this_tree.lock_write()
1047
 
        self.addCleanup(this_tree.unlock)
1048
 
        self.build_tree_contents([
1049
 
            ('this/file1', 'a\nb\n'),
1050
 
            ('this/file2', 'a\nb\n')
1051
 
        ])
1052
 
        this_tree.add(['file1', 'file2'])
1053
 
        this_tree.commit('Added files')
1054
 
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
1055
 
        self.build_tree_contents([
1056
 
            ('other/file1', 'a\nb\nc\n'),
1057
 
            ('other/file2', 'a\nb\nc\n')
1058
 
        ])
1059
 
        other_tree.commit('modified both')
1060
 
        self.build_tree_contents([
1061
 
            ('this/file1', 'd\na\nb\n'),
1062
 
            ('this/file2', 'd\na\nb\n')
1063
 
        ])
1064
 
        this_tree.commit('modified both')
1065
 
        self.do_merge(this_tree, other_tree, interesting_files=['file1'])
1066
 
        self.assertFileEqual('d\na\nb\nc\n', 'this/file1')
1067
 
        self.assertFileEqual('d\na\nb\n', 'this/file2')
1068
 
 
1069
 
    def test_merge_move_and_change(self):
1070
 
        this_tree = self.make_branch_and_tree('this')
1071
 
        this_tree.lock_write()
1072
 
        self.addCleanup(this_tree.unlock)
1073
 
        self.build_tree_contents([
1074
 
            ('this/file1', 'line 1\nline 2\nline 3\nline 4\n'),
1075
 
        ])
1076
 
        this_tree.add('file1',)
1077
 
        this_tree.commit('Added file')
1078
 
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
1079
 
        self.build_tree_contents([
1080
 
            ('other/file1', 'line 1\nline 2 to 2.1\nline 3\nline 4\n'),
1081
 
        ])
1082
 
        other_tree.commit('Changed 2 to 2.1')
1083
 
        self.build_tree_contents([
1084
 
            ('this/file1', 'line 1\nline 3\nline 2\nline 4\n'),
1085
 
        ])
1086
 
        this_tree.commit('Swapped 2 & 3')
1087
 
        self.do_merge(this_tree, other_tree)
1088
 
        self.assertFileEqual('line 1\n'
1089
 
            '<<<<<<< TREE\n'
1090
 
            'line 3\n'
1091
 
            'line 2\n'
1092
 
            '=======\n'
1093
 
            'line 2 to 2.1\n'
1094
 
            'line 3\n'
1095
 
            '>>>>>>> MERGE-SOURCE\n'
1096
 
            'line 4\n', 'this/file1')
1097
 
 
1098
 
    def test_modify_conflicts_with_delete(self):
1099
 
        # If one side deletes a line, and the other modifies that line, then
1100
 
        # the modification should be considered a conflict
1101
 
        builder = self.make_branch_builder('test')
1102
 
        builder.start_series()
1103
 
        builder.build_snapshot('BASE-id', None,
1104
 
            [('add', ('', None, 'directory', None)),
1105
 
             ('add', ('foo', 'foo-id', 'file', 'a\nb\nc\nd\ne\n')),
1106
 
            ])
1107
 
        # Delete 'b\n'
1108
 
        builder.build_snapshot('OTHER-id', ['BASE-id'],
1109
 
            [('modify', ('foo-id', 'a\nc\nd\ne\n'))])
1110
 
        # Modify 'b\n', add 'X\n'
1111
 
        builder.build_snapshot('THIS-id', ['BASE-id'],
1112
 
            [('modify', ('foo-id', 'a\nb2\nc\nd\nX\ne\n'))])
1113
 
        builder.finish_series()
1114
 
        branch = builder.get_branch()
1115
 
        this_tree = branch.bzrdir.create_workingtree()
1116
 
        this_tree.lock_write()
1117
 
        self.addCleanup(this_tree.unlock)
1118
 
        other_tree = this_tree.bzrdir.sprout('other', 'OTHER-id').open_workingtree()
1119
 
        self.do_merge(this_tree, other_tree)
1120
 
        if self.merge_type is _mod_merge.LCAMerger:
1121
 
            self.expectFailure("lca merge doesn't track deleted lines",
1122
 
                self.assertFileEqual,
1123
 
                    'a\n'
1124
 
                    '<<<<<<< TREE\n'
1125
 
                    'b2\n'
1126
 
                    '=======\n'
1127
 
                    '>>>>>>> MERGE-SOURCE\n'
1128
 
                    'c\n'
1129
 
                    'd\n'
1130
 
                    'X\n'
1131
 
                    'e\n', 'test/foo')
1132
 
        else:
1133
 
            self.assertFileEqual(
1134
 
                'a\n'
1135
 
                '<<<<<<< TREE\n'
1136
 
                'b2\n'
1137
 
                '=======\n'
1138
 
                '>>>>>>> MERGE-SOURCE\n'
1139
 
                'c\n'
1140
 
                'd\n'
1141
 
                'X\n'
1142
 
                'e\n', 'test/foo')
1143
 
 
1144
 
 
1145
 
class TestMerge3Merge(TestCaseWithTransport, TestMergeImplementation):
1146
 
 
1147
 
    merge_type = _mod_merge.Merge3Merger
1148
 
 
1149
 
 
1150
 
class TestWeaveMerge(TestCaseWithTransport, TestMergeImplementation):
1151
 
 
1152
 
    merge_type = _mod_merge.WeaveMerger
1153
 
 
1154
 
 
1155
 
class TestLCAMerge(TestCaseWithTransport, TestMergeImplementation):
1156
 
 
1157
 
    merge_type = _mod_merge.LCAMerger
1158
 
 
1159
 
    def test_merge_move_and_change(self):
1160
 
        self.expectFailure("lca merge doesn't conflict for move and change",
1161
 
            super(TestLCAMerge, self).test_merge_move_and_change)
1162
 
 
1163
 
 
1164
1144
class LoggingMerger(object):
1165
1145
    # These seem to be the required attributes
1166
1146
    requires_base = False
1221
1201
        mem_tree = memorytree.MemoryTree.create_on_branch(builder.get_branch())
1222
1202
        mem_tree.lock_write()
1223
1203
        self.addCleanup(mem_tree.unlock)
1224
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
1204
        merger = _mod_merge.Merger.from_revision_ids(None,
1225
1205
            mem_tree, other_revision_id)
1226
1206
        merger.set_interesting_files(interesting_files)
1227
1207
        # It seems there is no matching function for set_interesting_ids
1232
1212
 
1233
1213
class TestMergerInMemory(TestMergerBase):
1234
1214
 
 
1215
    def test_cache_trees_with_revision_ids_None(self):
 
1216
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
 
1217
        original_cache = dict(merger._cached_trees)
 
1218
        merger.cache_trees_with_revision_ids([None])
 
1219
        self.assertEqual(original_cache, merger._cached_trees)
 
1220
 
 
1221
    def test_cache_trees_with_revision_ids_no_revision_id(self):
 
1222
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
 
1223
        original_cache = dict(merger._cached_trees)
 
1224
        tree = self.make_branch_and_memory_tree('tree')
 
1225
        merger.cache_trees_with_revision_ids([tree])
 
1226
        self.assertEqual(original_cache, merger._cached_trees)
 
1227
 
 
1228
    def test_cache_trees_with_revision_ids_having_revision_id(self):
 
1229
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
 
1230
        original_cache = dict(merger._cached_trees)
 
1231
        tree = merger.this_branch.repository.revision_tree('B-id')
 
1232
        original_cache['B-id'] = tree
 
1233
        merger.cache_trees_with_revision_ids([tree])
 
1234
        self.assertEqual(original_cache, merger._cached_trees)
 
1235
 
1235
1236
    def test_find_base(self):
1236
1237
        merger = self.make_Merger(self.setup_simple_graph(), 'C-id')
1237
1238
        self.assertEqual('A-id', merger.base_rev_id)
1980
1981
 
1981
1982
    def do_merge(self, builder, other_revision_id):
1982
1983
        wt = self.get_wt_from_builder(builder)
1983
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
1984
        merger = _mod_merge.Merger.from_revision_ids(None,
1984
1985
            wt, other_revision_id)
1985
1986
        merger.merge_type = _mod_merge.Merge3Merger
1986
1987
        return wt, merger.do_merge()
2246
2247
        wt.commit('D merges B & C', rev_id='D-id')
2247
2248
        self.assertEqual('barry', wt.id2path('foo-id'))
2248
2249
        # Check the output of the Merger object directly
2249
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2250
        merger = _mod_merge.Merger.from_revision_ids(None,
2250
2251
            wt, 'F-id')
2251
2252
        merger.merge_type = _mod_merge.Merge3Merger
2252
2253
        merge_obj = merger.make_merger()
2302
2303
        wt.commit('F foo => bing', rev_id='F-id')
2303
2304
 
2304
2305
        # Check the output of the Merger object directly
2305
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2306
        merger = _mod_merge.Merger.from_revision_ids(None,
2306
2307
            wt, 'E-id')
2307
2308
        merger.merge_type = _mod_merge.Merge3Merger
2308
2309
        merge_obj = merger.make_merger()
2353
2354
        list(wt.iter_changes(wt.basis_tree()))
2354
2355
        wt.commit('D merges B & C, makes it a file', rev_id='D-id')
2355
2356
 
2356
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2357
        merger = _mod_merge.Merger.from_revision_ids(None,
2357
2358
            wt, 'E-id')
2358
2359
        merger.merge_type = _mod_merge.Merge3Merger
2359
2360
        merge_obj = merger.make_merger()
2568
2569
        wt.branch.set_last_revision_info(2, 'B-id')
2569
2570
        wt.commit('D', rev_id='D-id', recursive=None)
2570
2571
 
2571
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2572
        merger = _mod_merge.Merger.from_revision_ids(None,
2572
2573
            wt, 'E-id')
2573
2574
        merger.merge_type = _mod_merge.Merge3Merger
2574
2575
        merge_obj = merger.make_merger()
2605
2606
        wt.branch.set_last_revision_info(2, 'B-id')
2606
2607
        wt.commit('D', rev_id='D-id', recursive=None)
2607
2608
 
2608
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2609
        merger = _mod_merge.Merger.from_revision_ids(None,
2609
2610
            wt, 'E-id')
2610
2611
        merger.merge_type = _mod_merge.Merge3Merger
2611
2612
        merge_obj = merger.make_merger()
2645
2646
        wt.branch.set_last_revision_info(2, 'B-id')
2646
2647
        wt.commit('D', rev_id='D-id', recursive=None)
2647
2648
 
2648
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2649
        merger = _mod_merge.Merger.from_revision_ids(None,
2649
2650
            wt, 'E-id')
2650
2651
        merger.merge_type = _mod_merge.Merge3Merger
2651
2652
        merge_obj = merger.make_merger()
2690
2691
        wt.branch.set_last_revision_info(2, 'B-id')
2691
2692
        wt.commit('D', rev_id='D-id', recursive=None)
2692
2693
 
2693
 
        merger = _mod_merge.Merger.from_revision_ids(progress.DummyProgress(),
 
2694
        merger = _mod_merge.Merger.from_revision_ids(None,
2694
2695
            wt, 'E-id')
2695
2696
        merger.merge_type = _mod_merge.Merge3Merger
2696
2697
        merge_obj = merger.make_merger()
2832
2833
            'bval', ['lca1val', 'lca2val', 'lca2val'], 'oval', 'tval')
2833
2834
        self.assertLCAMultiWay('conflict',
2834
2835
            'bval', ['lca1val', 'lca2val', 'lca3val'], 'oval', 'tval')
 
2836
 
 
2837
 
 
2838
class TestConfigurableFileMerger(tests.TestCaseWithTransport):
 
2839
 
 
2840
    def setUp(self):
 
2841
        super(TestConfigurableFileMerger, self).setUp()
 
2842
        self.calls = []
 
2843
 
 
2844
    def get_merger_factory(self):
 
2845
        # Allows  the inner methods to access the test attributes
 
2846
        test = self
 
2847
 
 
2848
        class FooMerger(_mod_merge.ConfigurableFileMerger):
 
2849
            name_prefix = "foo"
 
2850
            default_files = ['bar']
 
2851
 
 
2852
            def merge_text(self, params):
 
2853
                test.calls.append('merge_text')
 
2854
                return ('not_applicable', None)
 
2855
 
 
2856
        def factory(merger):
 
2857
            result = FooMerger(merger)
 
2858
            # Make sure we start with a clean slate
 
2859
            self.assertEqual(None, result.affected_files)
 
2860
            # Track the original merger
 
2861
            self.merger = result
 
2862
            return result
 
2863
 
 
2864
        return factory
 
2865
 
 
2866
    def _install_hook(self, factory):
 
2867
        _mod_merge.Merger.hooks.install_named_hook('merge_file_content',
 
2868
                                                   factory, 'test factory')
 
2869
 
 
2870
    def make_builder(self):
 
2871
        builder = test_merge_core.MergeBuilder(self.test_base_dir)
 
2872
        self.addCleanup(builder.cleanup)
 
2873
        return builder
 
2874
 
 
2875
    def make_text_conflict(self, file_name='bar'):
 
2876
        factory = self.get_merger_factory()
 
2877
        self._install_hook(factory)
 
2878
        builder = self.make_builder()
 
2879
        builder.add_file('bar-id', builder.tree_root, file_name, 'text1', True)
 
2880
        builder.change_contents('bar-id', other='text4', this='text3')
 
2881
        return builder
 
2882
 
 
2883
    def make_kind_change(self):
 
2884
        factory = self.get_merger_factory()
 
2885
        self._install_hook(factory)
 
2886
        builder = self.make_builder()
 
2887
        builder.add_file('bar-id', builder.tree_root, 'bar', 'text1', True,
 
2888
                         this=False)
 
2889
        builder.add_dir('bar-dir', builder.tree_root, 'bar-id',
 
2890
                        base=False, other=False)
 
2891
        return builder
 
2892
 
 
2893
    def test_uses_this_branch(self):
 
2894
        builder = self.make_text_conflict()
 
2895
        tt = builder.make_preview_transform()
 
2896
        self.addCleanup(tt.finalize)
 
2897
 
 
2898
    def test_affected_files_cached(self):
 
2899
        """Ensures that the config variable is cached"""
 
2900
        builder = self.make_text_conflict()
 
2901
        conflicts = builder.merge()
 
2902
        # The hook should set the variable
 
2903
        self.assertEqual(['bar'], self.merger.affected_files)
 
2904
        self.assertEqual(1, len(conflicts))
 
2905
 
 
2906
    def test_hook_called_for_text_conflicts(self):
 
2907
        builder = self.make_text_conflict()
 
2908
        conflicts = builder.merge()
 
2909
        # The hook should call the merge_text() method
 
2910
        self.assertEqual(['merge_text'], self.calls)
 
2911
 
 
2912
    def test_hook_not_called_for_kind_change(self):
 
2913
        builder = self.make_kind_change()
 
2914
        conflicts = builder.merge()
 
2915
        # The hook should not call the merge_text() method
 
2916
        self.assertEqual([], self.calls)
 
2917
 
 
2918
    def test_hook_not_called_for_other_files(self):
 
2919
        builder = self.make_text_conflict('foobar')
 
2920
        conflicts = builder.merge()
 
2921
        # The hook should not call the merge_text() method
 
2922
        self.assertEqual([], self.calls)
 
2923
 
 
2924
 
 
2925
class TestMergeIntoBase(tests.TestCaseWithTransport):
 
2926
 
 
2927
    def setup_simple_branch(self, relpath, shape=None, root_id=None):
 
2928
        """One commit, containing tree specified by optional shape.
 
2929
        
 
2930
        Default is empty tree (just root entry).
 
2931
        """
 
2932
        if root_id is None:
 
2933
            root_id = '%s-root-id' % (relpath,)
 
2934
        wt = self.make_branch_and_tree(relpath)
 
2935
        wt.set_root_id(root_id)
 
2936
        if shape is not None:
 
2937
            adjusted_shape = [relpath + '/' + elem for elem in shape]
 
2938
            self.build_tree(adjusted_shape)
 
2939
            ids = ['%s-%s-id' % (relpath, basename(elem.rstrip('/')))
 
2940
                   for elem in shape]
 
2941
            wt.add(shape, ids=ids)
 
2942
        rev_id = 'r1-%s' % (relpath,)
 
2943
        wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
 
2944
        self.assertEqual(root_id, wt.path2id(''))
 
2945
        return wt
 
2946
 
 
2947
    def setup_two_branches(self, custom_root_ids=True):
 
2948
        """Setup 2 branches, one will be a library, the other a project."""
 
2949
        if custom_root_ids:
 
2950
            root_id = None
 
2951
        else:
 
2952
            root_id = inventory.ROOT_ID
 
2953
        project_wt = self.setup_simple_branch(
 
2954
            'project', ['README', 'dir/', 'dir/file.c'],
 
2955
            root_id)
 
2956
        lib_wt = self.setup_simple_branch(
 
2957
            'lib1', ['README', 'Makefile', 'foo.c'], root_id)
 
2958
 
 
2959
        return project_wt, lib_wt
 
2960
 
 
2961
    def do_merge_into(self, location, merge_as):
 
2962
        """Helper for using MergeIntoMerger.
 
2963
        
 
2964
        :param location: location of directory to merge from, either the
 
2965
            location of a branch or of a path inside a branch.
 
2966
        :param merge_as: the path in a tree to add the new directory as.
 
2967
        :returns: the conflicts from 'do_merge'.
 
2968
        """
 
2969
        operation = cleanup.OperationWithCleanups(self._merge_into)
 
2970
        return operation.run(location, merge_as)
 
2971
 
 
2972
    def _merge_into(self, op, location, merge_as):
 
2973
        # Open and lock the various tree and branch objects
 
2974
        wt, subdir_relpath = WorkingTree.open_containing(merge_as)
 
2975
        op.add_cleanup(wt.lock_write().unlock)
 
2976
        branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
 
2977
            location)
 
2978
        op.add_cleanup(branch_to_merge.lock_read().unlock)
 
2979
        other_tree = branch_to_merge.basis_tree()
 
2980
        op.add_cleanup(other_tree.lock_read().unlock)
 
2981
        # Perform the merge
 
2982
        merger = _mod_merge.MergeIntoMerger(this_tree=wt, other_tree=other_tree,
 
2983
            other_branch=branch_to_merge, target_subdir=subdir_relpath,
 
2984
            source_subpath=subdir_to_merge)
 
2985
        merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
 
2986
        conflicts = merger.do_merge()
 
2987
        merger.set_pending()
 
2988
        return conflicts
 
2989
 
 
2990
    def assertTreeEntriesEqual(self, expected_entries, tree):
 
2991
        """Assert that 'tree' contains the expected inventory entries.
 
2992
 
 
2993
        :param expected_entries: sequence of (path, file-id) pairs.
 
2994
        """
 
2995
        files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
 
2996
        self.assertEqual(expected_entries, files)
 
2997
 
 
2998
 
 
2999
class TestMergeInto(TestMergeIntoBase):
 
3000
 
 
3001
    def test_newdir_with_unique_roots(self):
 
3002
        """Merge a branch with a unique root into a new directory."""
 
3003
        project_wt, lib_wt = self.setup_two_branches()
 
3004
        self.do_merge_into('lib1', 'project/lib1')
 
3005
        project_wt.lock_read()
 
3006
        self.addCleanup(project_wt.unlock)
 
3007
        # The r1-lib1 revision should be merged into this one
 
3008
        self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
 
3009
        self.assertTreeEntriesEqual(
 
3010
            [('', 'project-root-id'),
 
3011
             ('README', 'project-README-id'),
 
3012
             ('dir', 'project-dir-id'),
 
3013
             ('lib1', 'lib1-root-id'),
 
3014
             ('dir/file.c', 'project-file.c-id'),
 
3015
             ('lib1/Makefile', 'lib1-Makefile-id'),
 
3016
             ('lib1/README', 'lib1-README-id'),
 
3017
             ('lib1/foo.c', 'lib1-foo.c-id'),
 
3018
            ], project_wt)
 
3019
 
 
3020
    def test_subdir(self):
 
3021
        """Merge a branch into a subdirectory of an existing directory."""
 
3022
        project_wt, lib_wt = self.setup_two_branches()
 
3023
        self.do_merge_into('lib1', 'project/dir/lib1')
 
3024
        project_wt.lock_read()
 
3025
        self.addCleanup(project_wt.unlock)
 
3026
        # The r1-lib1 revision should be merged into this one
 
3027
        self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
 
3028
        self.assertTreeEntriesEqual(
 
3029
            [('', 'project-root-id'),
 
3030
             ('README', 'project-README-id'),
 
3031
             ('dir', 'project-dir-id'),
 
3032
             ('dir/file.c', 'project-file.c-id'),
 
3033
             ('dir/lib1', 'lib1-root-id'),
 
3034
             ('dir/lib1/Makefile', 'lib1-Makefile-id'),
 
3035
             ('dir/lib1/README', 'lib1-README-id'),
 
3036
             ('dir/lib1/foo.c', 'lib1-foo.c-id'),
 
3037
            ], project_wt)
 
3038
 
 
3039
    def test_newdir_with_repeat_roots(self):
 
3040
        """If the file-id of the dir to be merged already exists a new ID will
 
3041
        be allocated to let the merge happen.
 
3042
        """
 
3043
        project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
 
3044
        root_id = project_wt.path2id('')
 
3045
        self.do_merge_into('lib1', 'project/lib1')
 
3046
        project_wt.lock_read()
 
3047
        self.addCleanup(project_wt.unlock)
 
3048
        # The r1-lib1 revision should be merged into this one
 
3049
        self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
 
3050
        new_lib1_id = project_wt.path2id('lib1')
 
3051
        self.assertNotEqual(None, new_lib1_id)
 
3052
        self.assertTreeEntriesEqual(
 
3053
            [('', root_id),
 
3054
             ('README', 'project-README-id'),
 
3055
             ('dir', 'project-dir-id'),
 
3056
             ('lib1', new_lib1_id),
 
3057
             ('dir/file.c', 'project-file.c-id'),
 
3058
             ('lib1/Makefile', 'lib1-Makefile-id'),
 
3059
             ('lib1/README', 'lib1-README-id'),
 
3060
             ('lib1/foo.c', 'lib1-foo.c-id'),
 
3061
            ], project_wt)
 
3062
 
 
3063
    def test_name_conflict(self):
 
3064
        """When the target directory name already exists a conflict is
 
3065
        generated and the original directory is renamed to foo.moved.
 
3066
        """
 
3067
        dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
 
3068
        src_wt = self.setup_simple_branch('src', ['README'])
 
3069
        conflicts = self.do_merge_into('src', 'dest/dir')
 
3070
        self.assertEqual(1, conflicts)
 
3071
        dest_wt.lock_read()
 
3072
        self.addCleanup(dest_wt.unlock)
 
3073
        # The r1-lib1 revision should be merged into this one
 
3074
        self.assertEqual(['r1-dest', 'r1-src'], dest_wt.get_parent_ids())
 
3075
        self.assertTreeEntriesEqual(
 
3076
            [('', 'dest-root-id'),
 
3077
             ('dir', 'src-root-id'),
 
3078
             ('dir.moved', 'dest-dir-id'),
 
3079
             ('dir/README', 'src-README-id'),
 
3080
             ('dir.moved/file.txt', 'dest-file.txt-id'),
 
3081
            ], dest_wt)
 
3082
 
 
3083
    def test_file_id_conflict(self):
 
3084
        """A conflict is generated if the merge-into adds a file (or other
 
3085
        inventory entry) with a file-id that already exists in the target tree.
 
3086
        """
 
3087
        dest_wt = self.setup_simple_branch('dest', ['file.txt'])
 
3088
        # Make a second tree with a file-id that will clash with file.txt in
 
3089
        # dest.
 
3090
        src_wt = self.make_branch_and_tree('src')
 
3091
        self.build_tree(['src/README'])
 
3092
        src_wt.add(['README'], ids=['dest-file.txt-id'])
 
3093
        src_wt.commit("Rev 1 of src.", rev_id='r1-src')
 
3094
        conflicts = self.do_merge_into('src', 'dest/dir')
 
3095
        # This is an edge case that shouldn't happen to users very often.  So
 
3096
        # we don't care really about the exact presentation of the conflict,
 
3097
        # just that there is one.
 
3098
        self.assertEqual(1, conflicts)
 
3099
 
 
3100
    def test_only_subdir(self):
 
3101
        """When the location points to just part of a tree, merge just that
 
3102
        subtree.
 
3103
        """
 
3104
        dest_wt = self.setup_simple_branch('dest')
 
3105
        src_wt = self.setup_simple_branch(
 
3106
            'src', ['hello.txt', 'dir/', 'dir/foo.c'])
 
3107
        conflicts = self.do_merge_into('src/dir', 'dest/dir')
 
3108
        dest_wt.lock_read()
 
3109
        self.addCleanup(dest_wt.unlock)
 
3110
        # The r1-lib1 revision should NOT be merged into this one (this is a
 
3111
        # partial merge).
 
3112
        self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
 
3113
        self.assertTreeEntriesEqual(
 
3114
            [('', 'dest-root-id'),
 
3115
             ('dir', 'src-dir-id'),
 
3116
             ('dir/foo.c', 'src-foo.c-id'),
 
3117
            ], dest_wt)
 
3118
 
 
3119
    def test_only_file(self):
 
3120
        """An edge case: merge just one file, not a whole dir."""
 
3121
        dest_wt = self.setup_simple_branch('dest')
 
3122
        two_file_wt = self.setup_simple_branch(
 
3123
            'two-file', ['file1.txt', 'file2.txt'])
 
3124
        conflicts = self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
 
3125
        dest_wt.lock_read()
 
3126
        self.addCleanup(dest_wt.unlock)
 
3127
        # The r1-lib1 revision should NOT be merged into this one
 
3128
        self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
 
3129
        self.assertTreeEntriesEqual(
 
3130
            [('', 'dest-root-id'), ('file1.txt', 'two-file-file1.txt-id')],
 
3131
            dest_wt)
 
3132
 
 
3133
    def test_no_such_source_path(self):
 
3134
        """PathNotInTree is raised if the specified path in the source tree
 
3135
        does not exist.
 
3136
        """
 
3137
        dest_wt = self.setup_simple_branch('dest')
 
3138
        two_file_wt = self.setup_simple_branch('src', ['dir/'])
 
3139
        self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
 
3140
            'src/no-such-dir', 'dest/foo')
 
3141
        dest_wt.lock_read()
 
3142
        self.addCleanup(dest_wt.unlock)
 
3143
        # The dest tree is unmodified.
 
3144
        self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
 
3145
        self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)
 
3146
 
 
3147
    def test_no_such_target_path(self):
 
3148
        """PathNotInTree is also raised if the specified path in the target
 
3149
        tree does not exist.
 
3150
        """
 
3151
        dest_wt = self.setup_simple_branch('dest')
 
3152
        two_file_wt = self.setup_simple_branch('src', ['file.txt'])
 
3153
        self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
 
3154
            'src', 'dest/no-such-dir/foo')
 
3155
        dest_wt.lock_read()
 
3156
        self.addCleanup(dest_wt.unlock)
 
3157
        # The dest tree is unmodified.
 
3158
        self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
 
3159
        self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)