2917
2974
conflicts = builder.merge()
2918
2975
# The hook should not call the merge_text() method
2919
2976
self.assertEqual([], self.calls)
2979
class TestMergeIntoBase(tests.TestCaseWithTransport):
2981
def setup_simple_branch(self, relpath, shape=None, root_id=None):
2982
"""One commit, containing tree specified by optional shape.
2984
Default is empty tree (just root entry).
2987
root_id = '%s-root-id' % (relpath,)
2988
wt = self.make_branch_and_tree(relpath)
2989
wt.set_root_id(root_id)
2990
if shape is not None:
2991
adjusted_shape = [relpath + '/' + elem for elem in shape]
2992
self.build_tree(adjusted_shape)
2993
ids = ['%s-%s-id' % (relpath, basename(elem.rstrip('/')))
2995
wt.add(shape, ids=ids)
2996
rev_id = 'r1-%s' % (relpath,)
2997
wt.commit("Initial commit of %s" % (relpath,), rev_id=rev_id)
2998
self.assertEqual(root_id, wt.path2id(''))
3001
def setup_two_branches(self, custom_root_ids=True):
3002
"""Setup 2 branches, one will be a library, the other a project."""
3006
root_id = inventory.ROOT_ID
3007
project_wt = self.setup_simple_branch(
3008
'project', ['README', 'dir/', 'dir/file.c'],
3010
lib_wt = self.setup_simple_branch(
3011
'lib1', ['README', 'Makefile', 'foo.c'], root_id)
3013
return project_wt, lib_wt
3015
def do_merge_into(self, location, merge_as):
3016
"""Helper for using MergeIntoMerger.
3018
:param location: location of directory to merge from, either the
3019
location of a branch or of a path inside a branch.
3020
:param merge_as: the path in a tree to add the new directory as.
3021
:returns: the conflicts from 'do_merge'.
3023
operation = cleanup.OperationWithCleanups(self._merge_into)
3024
return operation.run(location, merge_as)
3026
def _merge_into(self, op, location, merge_as):
3027
# Open and lock the various tree and branch objects
3028
wt, subdir_relpath = WorkingTree.open_containing(merge_as)
3029
op.add_cleanup(wt.lock_write().unlock)
3030
branch_to_merge, subdir_to_merge = _mod_branch.Branch.open_containing(
3032
op.add_cleanup(branch_to_merge.lock_read().unlock)
3033
other_tree = branch_to_merge.basis_tree()
3034
op.add_cleanup(other_tree.lock_read().unlock)
3036
merger = _mod_merge.MergeIntoMerger(this_tree=wt, other_tree=other_tree,
3037
other_branch=branch_to_merge, target_subdir=subdir_relpath,
3038
source_subpath=subdir_to_merge)
3039
merger.set_base_revision(_mod_revision.NULL_REVISION, branch_to_merge)
3040
conflicts = merger.do_merge()
3041
merger.set_pending()
3044
def assertTreeEntriesEqual(self, expected_entries, tree):
3045
"""Assert that 'tree' contains the expected inventory entries.
3047
:param expected_entries: sequence of (path, file-id) pairs.
3049
files = [(path, ie.file_id) for path, ie in tree.iter_entries_by_dir()]
3050
self.assertEqual(expected_entries, files)
3053
class TestMergeInto(TestMergeIntoBase):
3055
def test_newdir_with_unique_roots(self):
3056
"""Merge a branch with a unique root into a new directory."""
3057
project_wt, lib_wt = self.setup_two_branches()
3058
self.do_merge_into('lib1', 'project/lib1')
3059
project_wt.lock_read()
3060
self.addCleanup(project_wt.unlock)
3061
# The r1-lib1 revision should be merged into this one
3062
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3063
self.assertTreeEntriesEqual(
3064
[('', 'project-root-id'),
3065
('README', 'project-README-id'),
3066
('dir', 'project-dir-id'),
3067
('lib1', 'lib1-root-id'),
3068
('dir/file.c', 'project-file.c-id'),
3069
('lib1/Makefile', 'lib1-Makefile-id'),
3070
('lib1/README', 'lib1-README-id'),
3071
('lib1/foo.c', 'lib1-foo.c-id'),
3074
def test_subdir(self):
3075
"""Merge a branch into a subdirectory of an existing directory."""
3076
project_wt, lib_wt = self.setup_two_branches()
3077
self.do_merge_into('lib1', 'project/dir/lib1')
3078
project_wt.lock_read()
3079
self.addCleanup(project_wt.unlock)
3080
# The r1-lib1 revision should be merged into this one
3081
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3082
self.assertTreeEntriesEqual(
3083
[('', 'project-root-id'),
3084
('README', 'project-README-id'),
3085
('dir', 'project-dir-id'),
3086
('dir/file.c', 'project-file.c-id'),
3087
('dir/lib1', 'lib1-root-id'),
3088
('dir/lib1/Makefile', 'lib1-Makefile-id'),
3089
('dir/lib1/README', 'lib1-README-id'),
3090
('dir/lib1/foo.c', 'lib1-foo.c-id'),
3093
def test_newdir_with_repeat_roots(self):
3094
"""If the file-id of the dir to be merged already exists a new ID will
3095
be allocated to let the merge happen.
3097
project_wt, lib_wt = self.setup_two_branches(custom_root_ids=False)
3098
root_id = project_wt.path2id('')
3099
self.do_merge_into('lib1', 'project/lib1')
3100
project_wt.lock_read()
3101
self.addCleanup(project_wt.unlock)
3102
# The r1-lib1 revision should be merged into this one
3103
self.assertEqual(['r1-project', 'r1-lib1'], project_wt.get_parent_ids())
3104
new_lib1_id = project_wt.path2id('lib1')
3105
self.assertNotEqual(None, new_lib1_id)
3106
self.assertTreeEntriesEqual(
3108
('README', 'project-README-id'),
3109
('dir', 'project-dir-id'),
3110
('lib1', new_lib1_id),
3111
('dir/file.c', 'project-file.c-id'),
3112
('lib1/Makefile', 'lib1-Makefile-id'),
3113
('lib1/README', 'lib1-README-id'),
3114
('lib1/foo.c', 'lib1-foo.c-id'),
3117
def test_name_conflict(self):
3118
"""When the target directory name already exists a conflict is
3119
generated and the original directory is renamed to foo.moved.
3121
dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
3122
src_wt = self.setup_simple_branch('src', ['README'])
3123
conflicts = self.do_merge_into('src', 'dest/dir')
3124
self.assertEqual(1, conflicts)
3126
self.addCleanup(dest_wt.unlock)
3127
# The r1-lib1 revision should be merged into this one
3128
self.assertEqual(['r1-dest', 'r1-src'], dest_wt.get_parent_ids())
3129
self.assertTreeEntriesEqual(
3130
[('', 'dest-root-id'),
3131
('dir', 'src-root-id'),
3132
('dir.moved', 'dest-dir-id'),
3133
('dir/README', 'src-README-id'),
3134
('dir.moved/file.txt', 'dest-file.txt-id'),
3137
def test_file_id_conflict(self):
3138
"""A conflict is generated if the merge-into adds a file (or other
3139
inventory entry) with a file-id that already exists in the target tree.
3141
dest_wt = self.setup_simple_branch('dest', ['file.txt'])
3142
# Make a second tree with a file-id that will clash with file.txt in
3144
src_wt = self.make_branch_and_tree('src')
3145
self.build_tree(['src/README'])
3146
src_wt.add(['README'], ids=['dest-file.txt-id'])
3147
src_wt.commit("Rev 1 of src.", rev_id='r1-src')
3148
conflicts = self.do_merge_into('src', 'dest/dir')
3149
# This is an edge case that shouldn't happen to users very often. So
3150
# we don't care really about the exact presentation of the conflict,
3151
# just that there is one.
3152
self.assertEqual(1, conflicts)
3154
def test_only_subdir(self):
3155
"""When the location points to just part of a tree, merge just that
3158
dest_wt = self.setup_simple_branch('dest')
3159
src_wt = self.setup_simple_branch(
3160
'src', ['hello.txt', 'dir/', 'dir/foo.c'])
3161
conflicts = self.do_merge_into('src/dir', 'dest/dir')
3163
self.addCleanup(dest_wt.unlock)
3164
# The r1-lib1 revision should NOT be merged into this one (this is a
3166
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3167
self.assertTreeEntriesEqual(
3168
[('', 'dest-root-id'),
3169
('dir', 'src-dir-id'),
3170
('dir/foo.c', 'src-foo.c-id'),
3173
def test_only_file(self):
3174
"""An edge case: merge just one file, not a whole dir."""
3175
dest_wt = self.setup_simple_branch('dest')
3176
two_file_wt = self.setup_simple_branch(
3177
'two-file', ['file1.txt', 'file2.txt'])
3178
conflicts = self.do_merge_into('two-file/file1.txt', 'dest/file1.txt')
3180
self.addCleanup(dest_wt.unlock)
3181
# The r1-lib1 revision should NOT be merged into this one
3182
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3183
self.assertTreeEntriesEqual(
3184
[('', 'dest-root-id'), ('file1.txt', 'two-file-file1.txt-id')],
3187
def test_no_such_source_path(self):
3188
"""PathNotInTree is raised if the specified path in the source tree
3191
dest_wt = self.setup_simple_branch('dest')
3192
two_file_wt = self.setup_simple_branch('src', ['dir/'])
3193
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3194
'src/no-such-dir', 'dest/foo')
3196
self.addCleanup(dest_wt.unlock)
3197
# The dest tree is unmodified.
3198
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3199
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)
3201
def test_no_such_target_path(self):
3202
"""PathNotInTree is also raised if the specified path in the target
3203
tree does not exist.
3205
dest_wt = self.setup_simple_branch('dest')
3206
two_file_wt = self.setup_simple_branch('src', ['file.txt'])
3207
self.assertRaises(_mod_merge.PathNotInTree, self.do_merge_into,
3208
'src', 'dest/no-such-dir/foo')
3210
self.addCleanup(dest_wt.unlock)
3211
# The dest tree is unmodified.
3212
self.assertEqual(['r1-dest'], dest_wt.get_parent_ids())
3213
self.assertTreeEntriesEqual([('', 'dest-root-id')], dest_wt)