15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from itertools import chain
23
18
from bzrlib import (
19
branch as _mod_branch,
20
conflicts as _mod_conflicts,
26
24
graph as _mod_graph,
30
30
revision as _mod_revision,
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_from_tree,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
39
from bzrlib.cleanup import OperationWithCleanups
40
from bzrlib.symbol_versioning import (
63
44
# TODO: Report back as changes are merged in
66
47
def transform_tree(from_tree, to_tree, interesting_ids=None):
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
48
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)
71
216
class Merger(object):
72
220
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
221
this_tree=None, pb=None, change_reporter=None,
74
222
recurse='down', revision_graph=None):
590
750
self.change_reporter = change_reporter
591
751
self.cherrypick = cherrypick
592
752
if self.pp is None:
593
self.pp = ProgressPhase("Merge phase", 3, self.pb)
753
self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
597
757
def do_merge(self):
758
operation = OperationWithCleanups(self._do_merge)
759
operation.add_cleanup(self.pb.clear)
598
760
self.this_tree.lock_tree_write()
761
operation.add_cleanup(self.this_tree.unlock)
599
762
self.base_tree.lock_read()
763
operation.add_cleanup(self.base_tree.unlock)
600
764
self.other_tree.lock_read()
601
self.tt = TreeTransform(self.this_tree, self.pb)
765
operation.add_cleanup(self.other_tree.unlock)
768
def _do_merge(self, operation):
769
self.tt = transform.TreeTransform(self.this_tree, self.pb)
770
operation.add_cleanup(self.tt.finalize)
772
self._compute_transform()
774
results = self.tt.apply(no_conflicts=True)
775
self.write_modified(results)
604
self._compute_transform()
606
results = self.tt.apply(no_conflicts=True)
607
self.write_modified(results)
609
self.this_tree.add_conflicts(self.cooked_conflicts)
610
except UnsupportedOperation:
614
self.other_tree.unlock()
615
self.base_tree.unlock()
616
self.this_tree.unlock()
777
self.this_tree.add_conflicts(self.cooked_conflicts)
778
except errors.UnsupportedOperation:
619
781
def make_preview_transform(self):
782
operation = OperationWithCleanups(self._make_preview_transform)
783
operation.add_cleanup(self.pb.clear)
620
784
self.base_tree.lock_read()
785
operation.add_cleanup(self.base_tree.unlock)
621
786
self.other_tree.lock_read()
622
self.tt = TransformPreview(self.this_tree)
625
self._compute_transform()
628
self.other_tree.unlock()
629
self.base_tree.unlock()
787
operation.add_cleanup(self.other_tree.unlock)
788
return operation.run_simple()
790
def _make_preview_transform(self):
791
self.tt = transform.TransformPreview(self.this_tree)
793
self._compute_transform()
633
797
def _compute_transform(self):
1136
1291
if winner == 'this':
1137
1292
# No interesting changes introduced by OTHER
1138
1293
return "unmodified"
1294
# We have a hypothetical conflict, but if we have files, then we
1295
# can try to merge the content
1139
1296
trans_id = self.tt.trans_id_file_id(file_id)
1140
if winner == 'other':
1297
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1298
other_pair[0], winner)
1299
hooks = self.active_hooks
1300
hook_status = 'not_applicable'
1302
hook_status, lines = hook.merge_contents(params)
1303
if hook_status != 'not_applicable':
1304
# Don't try any more hooks, this one applies.
1307
if hook_status == 'not_applicable':
1308
# This is a contents conflict, because none of the available
1309
# functions could merge it.
1311
name = self.tt.final_name(trans_id)
1312
parent_id = self.tt.final_parent(trans_id)
1313
if self.this_tree.has_id(file_id):
1314
self.tt.unversion_file(trans_id)
1315
file_group = self._dump_conflicts(name, parent_id, file_id,
1317
self._raw_conflicts.append(('contents conflict', file_group))
1318
elif hook_status == 'success':
1319
self.tt.create_file(lines, trans_id)
1320
elif hook_status == 'conflicted':
1321
# XXX: perhaps the hook should be able to provide
1322
# the BASE/THIS/OTHER files?
1323
self.tt.create_file(lines, trans_id)
1324
self._raw_conflicts.append(('text conflict', trans_id))
1325
name = self.tt.final_name(trans_id)
1326
parent_id = self.tt.final_parent(trans_id)
1327
self._dump_conflicts(name, parent_id, file_id)
1328
elif hook_status == 'delete':
1329
self.tt.unversion_file(trans_id)
1331
elif hook_status == 'done':
1332
# The hook function did whatever it needs to do directly, no
1333
# further action needed here.
1336
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1337
if not self.this_tree.has_id(file_id) and result == "modified":
1338
self.tt.version_file(file_id, trans_id)
1339
# The merge has been performed, so the old contents should not be
1342
self.tt.delete_contents(trans_id)
1343
except errors.NoSuchFile:
1347
def _default_other_winner_merge(self, merge_hook_params):
1348
"""Replace this contents with other."""
1349
file_id = merge_hook_params.file_id
1350
trans_id = merge_hook_params.trans_id
1351
file_in_this = self.this_tree.has_id(file_id)
1352
if self.other_tree.has_id(file_id):
1353
# OTHER changed the file
1355
if wt.supports_content_filtering():
1356
# We get the path from the working tree if it exists.
1357
# That fails though when OTHER is adding a file, so
1358
# we fall back to the other tree to find the path if
1359
# it doesn't exist locally.
1361
filter_tree_path = wt.id2path(file_id)
1362
except errors.NoSuchId:
1363
filter_tree_path = self.other_tree.id2path(file_id)
1365
# Skip the id2path lookup for older formats
1366
filter_tree_path = None
1367
transform.create_from_tree(self.tt, trans_id,
1368
self.other_tree, file_id,
1369
filter_tree_path=filter_tree_path)
1372
# OTHER deleted the file
1373
return 'delete', None
1375
raise AssertionError(
1376
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1379
def merge_contents(self, merge_hook_params):
1380
"""Fallback merge logic after user installed hooks."""
1381
# This function is used in merge hooks as the fallback instance.
1382
# Perhaps making this function and the functions it calls be a
1383
# a separate class would be better.
1384
if merge_hook_params.winner == 'other':
1141
1385
# OTHER is a straight winner, so replace this contents with other
1142
file_in_this = file_id in self.this_tree
1144
# Remove any existing contents
1145
self.tt.delete_contents(trans_id)
1146
if file_id in self.other_tree:
1147
# OTHER changed the file
1148
create_from_tree(self.tt, trans_id,
1149
self.other_tree, file_id)
1150
if not file_in_this:
1151
self.tt.version_file(file_id, trans_id)
1154
# OTHER deleted the file
1155
self.tt.unversion_file(trans_id)
1386
return self._default_other_winner_merge(merge_hook_params)
1387
elif merge_hook_params.is_file_merge():
1388
# THIS and OTHER are both files, so text merge. Either
1389
# BASE is a file, or both converted to files, so at least we
1390
# have agreement that output should be a file.
1392
self.text_merge(merge_hook_params.file_id,
1393
merge_hook_params.trans_id)
1394
except errors.BinaryFile:
1395
return 'not_applicable', None
1158
# We have a hypothetical conflict, but if we have files, then we
1159
# can try to merge the content
1160
if this_pair[0] == 'file' and other_pair[0] == 'file':
1161
# THIS and OTHER are both files, so text merge. Either
1162
# BASE is a file, or both converted to files, so at least we
1163
# have agreement that output should be a file.
1165
self.text_merge(file_id, trans_id)
1167
return contents_conflict()
1168
if file_id not in self.this_tree:
1169
self.tt.version_file(file_id, trans_id)
1171
self.tt.tree_kind(trans_id)
1172
self.tt.delete_contents(trans_id)
1177
return contents_conflict()
1398
return 'not_applicable', None
1179
1400
def get_lines(self, tree, file_id):
1180
1401
"""Return the lines in a file, or an empty list."""
1402
if tree.has_id(file_id):
1182
1403
return tree.get_file(file_id).readlines()
1370
1609
supports_reverse_cherrypick = False
1371
1610
history_based = True
1373
def _merged_lines(self, file_id):
1374
"""Generate the merged lines.
1375
There is no distinction between lines that are meant to contain <<<<<<<
1379
base = self.base_tree
1382
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1612
def _generate_merge_plan(self, file_id, base):
1613
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1616
def _merged_lines(self, file_id):
1617
"""Generate the merged lines.
1618
There is no distinction between lines that are meant to contain <<<<<<<
1622
base = self.base_tree
1625
plan = self._generate_merge_plan(file_id, base)
1384
1626
if 'merge' in debug.debug_flags:
1385
1627
plan = list(plan)
1386
1628
trans_id = self.tt.trans_id_file_id(file_id)
1387
1629
name = self.tt.final_name(trans_id) + '.plan'
1388
contents = ('%10s|%s' % l for l in plan)
1630
contents = ('%11s|%s' % l for l in plan)
1389
1631
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1390
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1391
'>>>>>>> MERGE-SOURCE\n')
1392
return textmerge.merge_lines(self.reprocess)
1632
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1633
'>>>>>>> MERGE-SOURCE\n')
1634
lines, conflicts = textmerge.merge_lines(self.reprocess)
1636
base_lines = textmerge.base_from_plan()
1639
return lines, base_lines
1394
1641
def text_merge(self, file_id, trans_id):
1395
1642
"""Perform a (weave) text merge for a given file and file-id.
1396
1643
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1397
1644
and a conflict will be noted.
1399
lines, conflicts = self._merged_lines(file_id)
1646
lines, base_lines = self._merged_lines(file_id)
1400
1647
lines = list(lines)
1401
1648
# Note we're checking whether the OUTPUT is binary in this case,
1402
1649
# because we don't want to get into weave merge guts.
1403
check_text_lines(lines)
1650
textfile.check_text_lines(lines)
1404
1651
self.tt.create_file(lines, trans_id)
1652
if base_lines is not None:
1406
1654
self._raw_conflicts.append(('text conflict', trans_id))
1407
1655
name = self.tt.final_name(trans_id)
1408
1656
parent_id = self.tt.final_parent(trans_id)
1409
1657
file_group = self._dump_conflicts(name, parent_id, file_id,
1659
base_lines=base_lines)
1411
1660
file_group.append(trans_id)
1414
1663
class LCAMerger(WeaveMerger):
1416
def _merged_lines(self, file_id):
1417
"""Generate the merged lines.
1418
There is no distinction between lines that are meant to contain <<<<<<<
1422
base = self.base_tree
1425
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1665
def _generate_merge_plan(self, file_id, base):
1666
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1427
if 'merge' in debug.debug_flags:
1429
trans_id = self.tt.trans_id_file_id(file_id)
1430
name = self.tt.final_name(trans_id) + '.plan'
1431
contents = ('%10s|%s' % l for l in plan)
1432
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1433
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1434
'>>>>>>> MERGE-SOURCE\n')
1435
return textmerge.merge_lines(self.reprocess)
1438
1669
class Diff3Merger(Merge3Merger):
1439
1670
"""Three-way merger using external diff3 for text merging"""
1441
1672
def dump_file(self, temp_dir, name, tree, file_id):
1442
out_path = pathjoin(temp_dir, name)
1673
out_path = osutils.pathjoin(temp_dir, name)
1443
1674
out_file = open(out_path, "wb")
1445
1676
in_file = tree.get_file(file_id)