44
47
def transform_tree(from_tree, to_tree, interesting_ids=None):
45
48
from_tree.lock_tree_write()
47
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
48
interesting_ids=interesting_ids, this_tree=from_tree)
49
operation = OperationWithCleanups(merge_inner)
50
operation.add_cleanup(from_tree.unlock)
51
operation.run_simple(from_tree.branch, to_tree, from_tree,
52
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
55
class MergeHooks(hooks.Hooks):
58
hooks.Hooks.__init__(self)
59
self.create_hook(hooks.HookPoint('merge_file_content',
60
"Called with a bzrlib.merge.Merger object to create a per file "
61
"merge object when starting a merge. "
62
"Should return either None or a subclass of "
63
"``bzrlib.merge.AbstractPerFileMerger``. "
64
"Such objects will then be called per file "
65
"that needs to be merged (including when one "
66
"side has deleted the file and the other has changed it). "
67
"See the AbstractPerFileMerger API docs for details on how it is "
72
class AbstractPerFileMerger(object):
73
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
75
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
77
:ivar merger: The Merge3Merger performing the merge.
80
def __init__(self, merger):
81
"""Create a PerFileMerger for use with merger."""
84
def merge_contents(self, merge_params):
85
"""Attempt to merge the contents of a single file.
87
:param merge_params: A bzrlib.merge.MergeHookParams
88
:return : A tuple of (status, chunks), where status is one of
89
'not_applicable', 'success', 'conflicted', or 'delete'. If status
90
is 'success' or 'conflicted', then chunks should be an iterable of
91
strings for the new file contents.
93
return ('not applicable', None)
96
class ConfigurableFileMerger(AbstractPerFileMerger):
97
"""Merge individual files when configured via a .conf file.
99
This is a base class for concrete custom file merging logic. Concrete
100
classes should implement ``merge_text``.
102
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
104
:ivar affected_files: The configured file paths to merge.
106
:cvar name_prefix: The prefix to use when looking up configuration
107
details. <name_prefix>_merge_files describes the files targeted by the
110
:cvar default_files: The default file paths to merge when no configuration
117
def __init__(self, merger):
118
super(ConfigurableFileMerger, self).__init__(merger)
119
self.affected_files = None
120
self.default_files = self.__class__.default_files or []
121
self.name_prefix = self.__class__.name_prefix
122
if self.name_prefix is None:
123
raise ValueError("name_prefix must be set.")
125
def filename_matches_config(self, params):
126
"""Check whether the file should call the merge hook.
128
<name_prefix>_merge_files configuration variable is a list of files
129
that should use the hook.
131
affected_files = self.affected_files
132
if affected_files is None:
133
config = self.merger.this_branch.get_config()
134
# Until bzr provides a better policy for caching the config, we
135
# just add the part we're interested in to the params to avoid
136
# reading the config files repeatedly (bazaar.conf, location.conf,
138
config_key = self.name_prefix + '_merge_files'
139
affected_files = config.get_user_option_as_list(config_key)
140
if affected_files is None:
141
# If nothing was specified in the config, use the default.
142
affected_files = self.default_files
143
self.affected_files = affected_files
145
filename = self.merger.this_tree.id2path(params.file_id)
146
if filename in affected_files:
150
def merge_contents(self, params):
151
"""Merge the contents of a single file."""
152
# First, check whether this custom merge logic should be used. We
153
# expect most files should not be merged by this handler.
155
# OTHER is a straight winner, rely on default merge.
156
params.winner == 'other' or
157
# THIS and OTHER aren't both files.
158
not params.is_file_merge() or
159
# The filename isn't listed in the 'NAME_merge_files' config
161
not self.filename_matches_config(params)):
162
return 'not_applicable', None
163
return self.merge_text(params)
165
def merge_text(self, params):
166
"""Merge the byte contents of a single file.
168
This is called after checking that the merge should be performed in
169
merge_contents, and it should behave as per
170
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
172
raise NotImplementedError(self.merge_text)
175
class MergeHookParams(object):
176
"""Object holding parameters passed to merge_file_content hooks.
178
There are some fields hooks can access:
180
:ivar file_id: the file ID of the file being merged
181
:ivar trans_id: the transform ID for the merge of this file
182
:ivar this_kind: kind of file_id in 'this' tree
183
:ivar other_kind: kind of file_id in 'other' tree
184
:ivar winner: one of 'this', 'other', 'conflict'
187
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
189
self._merger = merger
190
self.file_id = file_id
191
self.trans_id = trans_id
192
self.this_kind = this_kind
193
self.other_kind = other_kind
196
def is_file_merge(self):
197
"""True if this_kind and other_kind are both 'file'."""
198
return self.this_kind == 'file' and self.other_kind == 'file'
200
@decorators.cachedproperty
201
def base_lines(self):
202
"""The lines of the 'base' version of the file."""
203
return self._merger.get_lines(self._merger.base_tree, self.file_id)
205
@decorators.cachedproperty
206
def this_lines(self):
207
"""The lines of the 'this' version of the file."""
208
return self._merger.get_lines(self._merger.this_tree, self.file_id)
210
@decorators.cachedproperty
211
def other_lines(self):
212
"""The lines of the 'other' version of the file."""
213
return self._merger.get_lines(self._merger.other_tree, self.file_id)
53
216
class Merger(object):
54
220
def __init__(self, this_branch, other_tree=None, base_tree=None,
55
221
this_tree=None, pb=None, change_reporter=None,
56
222
recurse='down', revision_graph=None):
541
696
def __init__(self, working_tree, this_tree, base_tree, other_tree,
542
697
interesting_ids=None, reprocess=False, show_base=False,
543
pb=progress.DummyProgress(), pp=None, change_reporter=None,
698
pb=None, pp=None, change_reporter=None,
544
699
interesting_files=None, do_merge=True,
545
cherrypick=False, lca_trees=None):
700
cherrypick=False, lca_trees=None, this_branch=None):
546
701
"""Initialize the merger object and perform the merge.
548
703
:param working_tree: The working tree to apply the merge to
549
704
:param this_tree: The local tree in the merge operation
550
705
:param base_tree: The common tree in the merge operation
551
706
:param other_tree: The other tree to merge changes from
707
:param this_branch: The branch associated with this_tree
552
708
:param interesting_ids: The file_ids of files that should be
553
709
participate in the merge. May not be combined with
554
710
interesting_files.
555
711
:param: reprocess If True, perform conflict-reduction processing.
556
712
:param show_base: If True, show the base revision in text conflicts.
557
713
(incompatible with reprocess)
558
:param pb: A Progress bar
559
715
:param pp: A ProgressPhase object
560
716
:param change_reporter: An object that should report changes made
561
717
:param interesting_files: The tree-relative paths of files that should
587
744
# making sure we haven't missed any corner cases.
588
745
# if lca_trees is None:
589
746
# self._lca_trees = [self.base_tree]
592
747
self.change_reporter = change_reporter
593
748
self.cherrypick = cherrypick
595
self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
752
warnings.warn("pp argument to Merge3Merger is deprecated")
754
warnings.warn("pb argument to Merge3Merger is deprecated")
599
756
def do_merge(self):
757
operation = OperationWithCleanups(self._do_merge)
600
758
self.this_tree.lock_tree_write()
759
operation.add_cleanup(self.this_tree.unlock)
601
760
self.base_tree.lock_read()
761
operation.add_cleanup(self.base_tree.unlock)
602
762
self.other_tree.lock_read()
763
operation.add_cleanup(self.other_tree.unlock)
766
def _do_merge(self, operation):
767
self.tt = transform.TreeTransform(self.this_tree, None)
768
operation.add_cleanup(self.tt.finalize)
769
self._compute_transform()
770
results = self.tt.apply(no_conflicts=True)
771
self.write_modified(results)
604
self.tt = transform.TreeTransform(self.this_tree, self.pb)
607
self._compute_transform()
609
results = self.tt.apply(no_conflicts=True)
610
self.write_modified(results)
612
self.this_tree.add_conflicts(self.cooked_conflicts)
613
except errors.UnsupportedOperation:
618
self.other_tree.unlock()
619
self.base_tree.unlock()
620
self.this_tree.unlock()
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
623
777
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
624
779
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
625
781
self.other_tree.lock_read()
782
operation.add_cleanup(self.other_tree.unlock)
783
return operation.run_simple()
785
def _make_preview_transform(self):
626
786
self.tt = transform.TransformPreview(self.this_tree)
629
self._compute_transform()
632
self.other_tree.unlock()
633
self.base_tree.unlock()
787
self._compute_transform()
637
790
def _compute_transform(self):
1140
1284
if winner == 'this':
1141
1285
# No interesting changes introduced by OTHER
1142
1286
return "unmodified"
1287
# We have a hypothetical conflict, but if we have files, then we
1288
# can try to merge the content
1143
1289
trans_id = self.tt.trans_id_file_id(file_id)
1144
if winner == 'other':
1290
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1291
other_pair[0], winner)
1292
hooks = self.active_hooks
1293
hook_status = 'not_applicable'
1295
hook_status, lines = hook.merge_contents(params)
1296
if hook_status != 'not_applicable':
1297
# Don't try any more hooks, this one applies.
1300
if hook_status == 'not_applicable':
1301
# This is a contents conflict, because none of the available
1302
# functions could merge it.
1304
name = self.tt.final_name(trans_id)
1305
parent_id = self.tt.final_parent(trans_id)
1306
if self.this_tree.has_id(file_id):
1307
self.tt.unversion_file(trans_id)
1308
file_group = self._dump_conflicts(name, parent_id, file_id,
1310
self._raw_conflicts.append(('contents conflict', file_group))
1311
elif hook_status == 'success':
1312
self.tt.create_file(lines, trans_id)
1313
elif hook_status == 'conflicted':
1314
# XXX: perhaps the hook should be able to provide
1315
# the BASE/THIS/OTHER files?
1316
self.tt.create_file(lines, trans_id)
1317
self._raw_conflicts.append(('text conflict', trans_id))
1318
name = self.tt.final_name(trans_id)
1319
parent_id = self.tt.final_parent(trans_id)
1320
self._dump_conflicts(name, parent_id, file_id)
1321
elif hook_status == 'delete':
1322
self.tt.unversion_file(trans_id)
1324
elif hook_status == 'done':
1325
# The hook function did whatever it needs to do directly, no
1326
# further action needed here.
1329
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1330
if not self.this_tree.has_id(file_id) and result == "modified":
1331
self.tt.version_file(file_id, trans_id)
1332
# The merge has been performed, so the old contents should not be
1335
self.tt.delete_contents(trans_id)
1336
except errors.NoSuchFile:
1340
def _default_other_winner_merge(self, merge_hook_params):
1341
"""Replace this contents with other."""
1342
file_id = merge_hook_params.file_id
1343
trans_id = merge_hook_params.trans_id
1344
file_in_this = self.this_tree.has_id(file_id)
1345
if self.other_tree.has_id(file_id):
1346
# OTHER changed the file
1348
if wt.supports_content_filtering():
1349
# We get the path from the working tree if it exists.
1350
# That fails though when OTHER is adding a file, so
1351
# we fall back to the other tree to find the path if
1352
# it doesn't exist locally.
1354
filter_tree_path = wt.id2path(file_id)
1355
except errors.NoSuchId:
1356
filter_tree_path = self.other_tree.id2path(file_id)
1358
# Skip the id2path lookup for older formats
1359
filter_tree_path = None
1360
transform.create_from_tree(self.tt, trans_id,
1361
self.other_tree, file_id,
1362
filter_tree_path=filter_tree_path)
1365
# OTHER deleted the file
1366
return 'delete', None
1368
raise AssertionError(
1369
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1372
def merge_contents(self, merge_hook_params):
1373
"""Fallback merge logic after user installed hooks."""
1374
# This function is used in merge hooks as the fallback instance.
1375
# Perhaps making this function and the functions it calls be a
1376
# a separate class would be better.
1377
if merge_hook_params.winner == 'other':
1145
1378
# OTHER is a straight winner, so replace this contents with other
1146
file_in_this = file_id in self.this_tree
1148
# Remove any existing contents
1149
self.tt.delete_contents(trans_id)
1150
if file_id in self.other_tree:
1151
# OTHER changed the file
1153
if wt.supports_content_filtering():
1154
# We get the path from the working tree if it exists.
1155
# That fails though when OTHER is adding a file, so
1156
# we fall back to the other tree to find the path if
1157
# it doesn't exist locally.
1159
filter_tree_path = wt.id2path(file_id)
1160
except errors.NoSuchId:
1161
filter_tree_path = self.other_tree.id2path(file_id)
1163
# Skip the id2path lookup for older formats
1164
filter_tree_path = None
1165
transform.create_from_tree(self.tt, trans_id,
1166
self.other_tree, file_id,
1167
filter_tree_path=filter_tree_path)
1168
if not file_in_this:
1169
self.tt.version_file(file_id, trans_id)
1172
# OTHER deleted the file
1173
self.tt.unversion_file(trans_id)
1379
return self._default_other_winner_merge(merge_hook_params)
1380
elif merge_hook_params.is_file_merge():
1381
# THIS and OTHER are both files, so text merge. Either
1382
# BASE is a file, or both converted to files, so at least we
1383
# have agreement that output should be a file.
1385
self.text_merge(merge_hook_params.file_id,
1386
merge_hook_params.trans_id)
1387
except errors.BinaryFile:
1388
return 'not_applicable', None
1176
# We have a hypothetical conflict, but if we have files, then we
1177
# can try to merge the content
1178
if this_pair[0] == 'file' and other_pair[0] == 'file':
1179
# THIS and OTHER are both files, so text merge. Either
1180
# BASE is a file, or both converted to files, so at least we
1181
# have agreement that output should be a file.
1183
self.text_merge(file_id, trans_id)
1184
except errors.BinaryFile:
1185
return contents_conflict()
1186
if file_id not in self.this_tree:
1187
self.tt.version_file(file_id, trans_id)
1189
self.tt.tree_kind(trans_id)
1190
self.tt.delete_contents(trans_id)
1191
except errors.NoSuchFile:
1195
return contents_conflict()
1391
return 'not_applicable', None
1197
1393
def get_lines(self, tree, file_id):
1198
1394
"""Return the lines in a file, or an empty list."""
1395
if tree.has_id(file_id):
1200
1396
return tree.get_file(file_id).readlines()