384
417
'>>>>>>> MERGE-SOURCE\n',
420
def test_merge_reverse_revision_range(self):
421
tree = self.make_branch_and_tree(".")
423
self.addCleanup(tree.unlock)
424
self.build_tree(['a'])
426
first_rev = tree.commit("added a")
427
merger = _mod_merge.Merger.from_revision_ids(tree,
428
_mod_revision.NULL_REVISION,
430
merger.merge_type = _mod_merge.Merge3Merger
431
merger.interesting_files = 'a'
432
conflict_count = merger.do_merge()
433
self.assertEqual(0, conflict_count)
435
self.assertPathDoesNotExist("a")
437
self.assertPathExists("a")
387
439
def test_make_merger(self):
388
440
this_tree = self.make_branch_and_tree('this')
389
441
this_tree.commit('rev1', rev_id='rev1')
390
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
442
other_tree = this_tree.controldir.sprout('other').open_workingtree()
391
443
this_tree.commit('rev2', rev_id='rev2a')
392
444
other_tree.commit('rev2', rev_id='rev2b')
393
445
this_tree.lock_write()
394
446
self.addCleanup(this_tree.unlock)
395
merger = _mod_merge.Merger.from_revision_ids(None,
447
merger = _mod_merge.Merger.from_revision_ids(
396
448
this_tree, 'rev2b', other_branch=other_tree.branch)
397
449
merger.merge_type = _mod_merge.Merge3Merger
398
450
tree_merger = merger.make_merger()
399
451
self.assertIs(_mod_merge.Merge3Merger, tree_merger.__class__)
400
self.assertEqual('rev2b', tree_merger.other_tree.get_revision_id())
401
self.assertEqual('rev1', tree_merger.base_tree.get_revision_id())
452
self.assertEqual('rev2b',
453
tree_merger.other_tree.get_revision_id())
454
self.assertEqual('rev1',
455
tree_merger.base_tree.get_revision_id())
456
self.assertEqual(other_tree.branch, tree_merger.other_branch)
403
458
def test_make_preview_transform(self):
404
459
this_tree = self.make_branch_and_tree('this')
405
460
self.build_tree_contents([('this/file', '1\n')])
406
461
this_tree.add('file', 'file-id')
407
462
this_tree.commit('rev1', rev_id='rev1')
408
other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
463
other_tree = this_tree.controldir.sprout('other').open_workingtree()
409
464
self.build_tree_contents([('this/file', '1\n2a\n')])
410
465
this_tree.commit('rev2', rev_id='rev2a')
411
466
self.build_tree_contents([('other/file', '2b\n1\n')])
412
467
other_tree.commit('rev2', rev_id='rev2b')
413
468
this_tree.lock_write()
414
469
self.addCleanup(this_tree.unlock)
415
merger = _mod_merge.Merger.from_revision_ids(None,
470
merger = _mod_merge.Merger.from_revision_ids(
416
471
this_tree, 'rev2b', other_branch=other_tree.branch)
417
472
merger.merge_type = _mod_merge.Merge3Merger
418
473
tree_merger = merger.make_merger()
2917
3004
conflicts = builder.merge()
2918
3005
# The hook should not call the merge_text() method
2919
3006
self.assertEqual([], self.calls)
3009
class TestMergeIntoBase(tests.TestCaseWithTransport):
3011
def setup_simple_branch(self, relpath, shape=None, root_id=None):
3012
"""One commit, containing tree specified by optional shape.
3014
Default is empty tree (just root entry).
3017
root_id = '%s-root-id' % (relpath,)
3018
wt = self.make_branch_and_tree(relpath)
3019
wt.set_root_id(root_id)
3020
if shape is not None:
3021
adjusted_shape = [relpath + '/' + elem for elem in shape]
3022
self.build_tree(adjusted_shape)
3023
ids = ['%s-%s-id' % (relpath, basename(elem.rstrip('/')))
3025
wt.add(shape, ids=ids)
3026
rev_id = 'r1-%s' % (relpath,)
3027
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
3028
self.assertEqual(root_id, wt.path2id(''))
3031
def setup_two_branches(self, custom_root_ids=True):
3032
"""Setup 2 branches, one will be a library, the other a project."""
3036
root_id = inventory.ROOT_ID
3037
project_wt = self.setup_simple_branch(
3038
'project', ['README', 'dir/', 'dir/file.c'],
3040
lib_wt = self.setup_simple_branch(
3041
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3043
return project_wt, lib_wt
3045
def do_merge_into(self, location, merge_as):
3046
"""Helper for using MergeIntoMerger.
3048
:param location: location of directory to merge from, either the
3049
location of a branch or of a path inside a branch.
3050
:param merge_as: the path in a tree to add the new directory as.
3051
:returns: the conflicts from 'do_merge'.
3053
operation = cleanup.OperationWithCleanups(self._merge_into)
3054
return operation.run(location, merge_as)
3056
def _merge_into(self, op, location, merge_as):
3057
# Open and lock the various tree and branch objects
3058
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3059
op.add_cleanup(wt.lock_write().unlock)
3060
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3062
op.add_cleanup(branch_to_merge.lock_read().unlock)
3063
other_tree = branch_to_merge.basis_tree()
3064
op.add_cleanup(other_tree.lock_read().unlock)
3066
merger = _mod_merge.MergeIntoMerger(this_tree=wt, other_tree=other_tree,
3067
other_branch=branch_to_merge, target_subdir=subdir_relpath,
3068
source_subpath=subdir_to_merge)
3069
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3070
conflicts = merger.do_merge()
3071
merger.set_pending()
3074
def assertTreeEntriesEqual(self, expected_entries, tree):
3075
"""Assert that 'tree' contains the expected inventory entries.
3077
:param expected_entries: sequence of (path, file-id) pairs.
3079
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3080
self.assertEqual(expected_entries, files)
3083
class TestMergeInto(TestMergeIntoBase):
3085
def test_newdir_with_unique_roots(self):
3086
"""Merge a branch with a unique root into a new directory."""
3087
project_wt, lib_wt = self.setup_two_branches()
3088
self.do_merge_into('lib1', 'project/lib1')
3089
project_wt.lock_read()
3090
self.addCleanup(project_wt.unlock)
3091
# The r1-lib1 revision should be merged into this one
3092
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3093
self.assertTreeEntriesEqual(
3094
[('', 'project-root-id'),
3095
('README', 'project-README-id'),
3096
('dir', 'project-dir-id'),
3097
('lib1', 'lib1-root-id'),
3098
('dir/file.c', 'project-file.c-id'),
3099
('lib1/Makefile', 'lib1-Makefile-id'),
3100
('lib1/README', 'lib1-README-id'),
3101
('lib1/foo.c', 'lib1-foo.c-id'),
3104
def test_subdir(self):
3105
"""Merge a branch into a subdirectory of an existing directory."""
3106
project_wt, lib_wt = self.setup_two_branches()
3107
self.do_merge_into('lib1', 'project/dir/lib1')
3108
project_wt.lock_read()
3109
self.addCleanup(project_wt.unlock)
3110
# The r1-lib1 revision should be merged into this one
3111
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3112
self.assertTreeEntriesEqual(
3113
[('', 'project-root-id'),
3114
('README', 'project-README-id'),
3115
('dir', 'project-dir-id'),
3116
('dir/file.c', 'project-file.c-id'),
3117
('dir/lib1', 'lib1-root-id'),
3118
('dir/lib1/Makefile', 'lib1-Makefile-id'),
3119
('dir/lib1/README', 'lib1-README-id'),
3120
('dir/lib1/foo.c', 'lib1-foo.c-id'),
3123
def test_newdir_with_repeat_roots(self):
3124
"""If the file-id of the dir to be merged already exists a new ID will
3125
be allocated to let the merge happen.
3127
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3128
root_id = project_wt.path2id('')
3129
self.do_merge_into('lib1', 'project/lib1')
3130
project_wt.lock_read()
3131
self.addCleanup(project_wt.unlock)
3132
# The r1-lib1 revision should be merged into this one
3133
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3134
new_lib1_id = project_wt.path2id('lib1')
3135
self.assertNotEqual(None, new_lib1_id)
3136
self.assertTreeEntriesEqual(
3138
('README', 'project-README-id'),
3139
('dir', 'project-dir-id'),
3140
('lib1', new_lib1_id),
3141
('dir/file.c', 'project-file.c-id'),
3142
('lib1/Makefile', 'lib1-Makefile-id'),
3143
('lib1/README', 'lib1-README-id'),
3144
('lib1/foo.c', 'lib1-foo.c-id'),
3147
def test_name_conflict(self):
3148
"""When the target directory name already exists a conflict is
3149
generated and the original directory is renamed to foo.moved.
3151
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3152
src_wt = self.setup_simple_branch('src', ['README'])
3153
conflicts = self.do_merge_into('src', 'dest/dir')
3154
self.assertEqual(1, conflicts)
3156
self.addCleanup(dest_wt.unlock)
3157
# The r1-lib1 revision should be merged into this one
3158
self.assertEqual(['r1-dest', 'r1-src'], dest_wt.get_parent_ids())
3159
self.assertTreeEntriesEqual(
3160
[('', 'dest-root-id'),
3161
('dir', 'src-root-id'),
3162
('dir.moved', 'dest-dir-id'),
3163
('dir/README', 'src-README-id'),
3164
('dir.moved/file.txt', 'dest-file.txt-id'),
3167
def test_file_id_conflict(self):
3168
"""A conflict is generated if the merge-into adds a file (or other
3169
inventory entry) with a file-id that already exists in the target tree.
3171
dest_wt = self.setup_simple_branch('dest', ['file.txt'])
3172
# Make a second tree with a file-id that will clash with file.txt in
3174
src_wt = self.make_branch_and_tree('src')
3175
self.build_tree(['src/README'])
3176
src_wt.add(['README'], ids=['dest-file.txt-id'])
3177
src_wt.commit("Rev 1 of src.", rev_id='r1-src')
3178
conflicts = self.do_merge_into('src', 'dest/dir')
3179
# This is an edge case that shouldn't happen to users very often. So
3180
# we don't care really about the exact presentation of the conflict,
3181
# just that there is one.
3182
self.assertEqual(1, conflicts)
3184
def test_only_subdir(self):
3185
"""When the location points to just part of a tree, merge just that
3188
dest_wt = self.setup_simple_branch('dest')
3189
src_wt = self.setup_simple_branch(
3190
'src', ['hello.txt', 'dir/', 'dir/foo.c'])
3191
conflicts = self.do_merge_into('src/dir', 'dest/dir')
3193
self.addCleanup(dest_wt.unlock)
3194
# The r1-lib1 revision should NOT be merged into this one (this is a
3196
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3197
self.assertTreeEntriesEqual(
3198
[('', 'dest-root-id'),
3199
('dir', 'src-dir-id'),
3200
('dir/foo.c', 'src-foo.c-id'),
3203
def test_only_file(self):
3204
"""An edge case: merge just one file, not a whole dir."""
3205
dest_wt = self.setup_simple_branch('dest')
3206
two_file_wt = self.setup_simple_branch(
3207
'two-file', ['file1.txt', 'file2.txt'])
3208
conflicts = self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3210
self.addCleanup(dest_wt.unlock)
3211
# The r1-lib1 revision should NOT be merged into this one
3212
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3213
self.assertTreeEntriesEqual(
3214
[('', 'dest-root-id'), ('file1.txt', 'two-file-file1.txt-id')],
3217
def test_no_such_source_path(self):
3218
"""PathNotInTree is raised if the specified path in the source tree
3221
dest_wt = self.setup_simple_branch('dest')
3222
two_file_wt = self.setup_simple_branch('src', ['dir/'])
3223
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3224
'src/no-such-dir', 'dest/foo')
3226
self.addCleanup(dest_wt.unlock)
3227
# The dest tree is unmodified.
3228
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3229
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)
3231
def test_no_such_target_path(self):
3232
"""PathNotInTree is also raised if the specified path in the target
3233
tree does not exist.
3235
dest_wt = self.setup_simple_branch('dest')
3236
two_file_wt = self.setup_simple_branch('src', ['file.txt'])
3237
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3238
'src', 'dest/no-such-dir/foo')
3240
self.addCleanup(dest_wt.unlock)
3241
# The dest tree is unmodified.
3242
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3243
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)
3246
class TestMergeHooks(TestCaseWithTransport):
3249
super(TestMergeHooks, self).setUp()
3250
self.tree_a = self.make_branch_and_tree('tree_a')
3251
self.build_tree_contents([('tree_a/file', 'content_1')])
3252
self.tree_a.add('file', 'file-id')
3253
self.tree_a.commit('added file')
3255
self.tree_b = self.tree_a.controldir.sprout('tree_b').open_workingtree()
3256
self.build_tree_contents([('tree_b/file', 'content_2')])
3257
self.tree_b.commit('modify file')
3259
def test_pre_merge_hook_inject_different_tree(self):
3260
tree_c = self.tree_b.controldir.sprout('tree_c').open_workingtree()
3261
self.build_tree_contents([('tree_c/file', 'content_3')])
3262
tree_c.commit("more content")
3264
def factory(merger):
3265
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3266
merger.other_tree = tree_c
3267
calls.append(merger)
3268
_mod_merge.Merger.hooks.install_named_hook('pre_merge',
3269
factory, 'test factory')
3270
self.tree_a.merge_from_branch(self.tree_b.branch)
3272
self.assertFileEqual("content_3", 'tree_a/file')
3273
self.assertLength(1, calls)
3275
def test_post_merge_hook_called(self):
3277
def factory(merger):
3278
self.assertIsInstance(merger, _mod_merge.Merge3Merger)
3279
calls.append(merger)
3280
_mod_merge.Merger.hooks.install_named_hook('post_merge',
3281
factory, 'test factory')
3283
self.tree_a.merge_from_branch(self.tree_b.branch)
3285
self.assertFileEqual("content_2", 'tree_a/file')
3286
self.assertLength(1, calls)