47
44
def transform_tree(from_tree, to_tree, interesting_ids=None):
48
45
from_tree.lock_tree_write()
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)
47
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
48
interesting_ids=interesting_ids, this_tree=from_tree)
216
53
class Merger(object):
220
54
def __init__(self, this_branch, other_tree=None, base_tree=None,
221
55
this_tree=None, pb=None, change_reporter=None,
222
56
recurse='down', revision_graph=None):
696
541
def __init__(self, working_tree, this_tree, base_tree, other_tree,
697
542
interesting_ids=None, reprocess=False, show_base=False,
698
pb=None, pp=None, change_reporter=None,
543
pb=progress.DummyProgress(), pp=None, change_reporter=None,
699
544
interesting_files=None, do_merge=True,
700
cherrypick=False, lca_trees=None, this_branch=None):
545
cherrypick=False, lca_trees=None):
701
546
"""Initialize the merger object and perform the merge.
703
548
:param working_tree: The working tree to apply the merge to
704
549
:param this_tree: The local tree in the merge operation
705
550
:param base_tree: The common tree in the merge operation
706
551
:param other_tree: The other tree to merge changes from
707
:param this_branch: The branch associated with this_tree
708
552
:param interesting_ids: The file_ids of files that should be
709
553
participate in the merge. May not be combined with
710
554
interesting_files.
711
555
:param: reprocess If True, perform conflict-reduction processing.
712
556
:param show_base: If True, show the base revision in text conflicts.
713
557
(incompatible with reprocess)
558
:param pb: A Progress bar
715
559
:param pp: A ProgressPhase object
716
560
:param change_reporter: An object that should report changes made
717
561
:param interesting_files: The tree-relative paths of files that should
744
587
# making sure we haven't missed any corner cases.
745
588
# if lca_trees is None:
746
589
# self._lca_trees = [self.base_tree]
747
592
self.change_reporter = change_reporter
748
593
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")
756
599
def do_merge(self):
757
operation = OperationWithCleanups(self._do_merge)
758
600
self.this_tree.lock_tree_write()
759
operation.add_cleanup(self.this_tree.unlock)
760
601
self.base_tree.lock_read()
761
operation.add_cleanup(self.base_tree.unlock)
762
602
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)
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
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()
777
623
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
779
624
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
781
625
self.other_tree.lock_read()
782
operation.add_cleanup(self.other_tree.unlock)
783
return operation.run_simple()
785
def _make_preview_transform(self):
786
626
self.tt = transform.TransformPreview(self.this_tree)
787
self._compute_transform()
629
self._compute_transform()
632
self.other_tree.unlock()
633
self.base_tree.unlock()
790
637
def _compute_transform(self):
1283
1140
if winner == 'this':
1284
1141
# No interesting changes introduced by OTHER
1285
1142
return "unmodified"
1286
# We have a hypothetical conflict, but if we have files, then we
1287
# can try to merge the content
1288
1143
trans_id = self.tt.trans_id_file_id(file_id)
1289
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1290
other_pair[0], winner)
1291
hooks = self.active_hooks
1292
hook_status = 'not_applicable'
1294
hook_status, lines = hook.merge_contents(params)
1295
if hook_status != 'not_applicable':
1296
# Don't try any more hooks, this one applies.
1299
if hook_status == 'not_applicable':
1300
# This is a contents conflict, because none of the available
1301
# functions could merge it.
1303
name = self.tt.final_name(trans_id)
1304
parent_id = self.tt.final_parent(trans_id)
1305
if self.this_tree.has_id(file_id):
1306
self.tt.unversion_file(trans_id)
1307
file_group = self._dump_conflicts(name, parent_id, file_id,
1309
self._raw_conflicts.append(('contents conflict', file_group))
1310
elif hook_status == 'success':
1311
self.tt.create_file(lines, trans_id)
1312
elif hook_status == 'conflicted':
1313
# XXX: perhaps the hook should be able to provide
1314
# the BASE/THIS/OTHER files?
1315
self.tt.create_file(lines, trans_id)
1316
self._raw_conflicts.append(('text conflict', trans_id))
1317
name = self.tt.final_name(trans_id)
1318
parent_id = self.tt.final_parent(trans_id)
1319
self._dump_conflicts(name, parent_id, file_id)
1320
elif hook_status == 'delete':
1321
self.tt.unversion_file(trans_id)
1323
elif hook_status == 'done':
1324
# The hook function did whatever it needs to do directly, no
1325
# further action needed here.
1328
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1329
if not self.this_tree.has_id(file_id) and result == "modified":
1330
self.tt.version_file(file_id, trans_id)
1331
# The merge has been performed, so the old contents should not be
1334
self.tt.delete_contents(trans_id)
1335
except errors.NoSuchFile:
1339
def _default_other_winner_merge(self, merge_hook_params):
1340
"""Replace this contents with other."""
1341
file_id = merge_hook_params.file_id
1342
trans_id = merge_hook_params.trans_id
1343
file_in_this = self.this_tree.has_id(file_id)
1344
if self.other_tree.has_id(file_id):
1345
# OTHER changed the file
1347
if wt.supports_content_filtering():
1348
# We get the path from the working tree if it exists.
1349
# That fails though when OTHER is adding a file, so
1350
# we fall back to the other tree to find the path if
1351
# it doesn't exist locally.
1353
filter_tree_path = wt.id2path(file_id)
1354
except errors.NoSuchId:
1355
filter_tree_path = self.other_tree.id2path(file_id)
1357
# Skip the id2path lookup for older formats
1358
filter_tree_path = None
1359
transform.create_from_tree(self.tt, trans_id,
1360
self.other_tree, file_id,
1361
filter_tree_path=filter_tree_path)
1364
# OTHER deleted the file
1365
return 'delete', None
1367
raise AssertionError(
1368
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1371
def merge_contents(self, merge_hook_params):
1372
"""Fallback merge logic after user installed hooks."""
1373
# This function is used in merge hooks as the fallback instance.
1374
# Perhaps making this function and the functions it calls be a
1375
# a separate class would be better.
1376
if merge_hook_params.winner == 'other':
1144
if winner == 'other':
1377
1145
# OTHER is a straight winner, so replace this contents with other
1378
return self._default_other_winner_merge(merge_hook_params)
1379
elif merge_hook_params.is_file_merge():
1380
# THIS and OTHER are both files, so text merge. Either
1381
# BASE is a file, or both converted to files, so at least we
1382
# have agreement that output should be a file.
1384
self.text_merge(merge_hook_params.file_id,
1385
merge_hook_params.trans_id)
1386
except errors.BinaryFile:
1387
return 'not_applicable', None
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)
1390
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()
1392
1197
def get_lines(self, tree, file_id):
1393
1198
"""Return the lines in a file, or an empty list."""
1394
if tree.has_id(file_id):
1395
1200
return tree.get_file(file_id).readlines()