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.symbol_versioning import (
63
43
# TODO: Report back as changes are merged in
66
46
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)
47
from_tree.lock_tree_write()
49
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
50
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 when file content needs to be merged (including when one "
61
"side has deleted the file and the other has changed it)."
62
"merge_file_content is called with a "
63
"bzrlib.merge.MergeHookParams. The function should return a tuple "
64
"of (status, lines), where status is one of 'not_applicable', "
65
"'success', 'conflicted', or 'delete'. If status is success or "
66
"conflicted, then lines should be an iterable of strings of the "
71
class MergeHookParams(object):
72
"""Object holding parameters passed to merge_file_content hooks.
74
There are 3 fields hooks can access:
76
:ivar merger: the Merger object
77
:ivar file_id: the file ID of the file being merged
78
:ivar trans_id: the transform ID for the merge of this file
79
:ivar this_kind: kind of file_id in 'this' tree
80
:ivar other_kind: kind of file_id in 'other' tree
81
:ivar winner: one of 'this', 'other', 'conflict'
84
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
87
self.file_id = file_id
88
self.trans_id = trans_id
89
self.this_kind = this_kind
90
self.other_kind = other_kind
93
def is_file_merge(self):
94
"""True if this_kind and other_kind are both 'file'."""
95
return self.this_kind == 'file' and self.other_kind == 'file'
97
@decorators.cachedproperty
99
"""The lines of the 'base' version of the file."""
100
return self.merger.get_lines(self.merger.base_tree, self.file_id)
102
@decorators.cachedproperty
103
def this_lines(self):
104
"""The lines of the 'this' version of the file."""
105
return self.merger.get_lines(self.merger.this_tree, self.file_id)
107
@decorators.cachedproperty
108
def other_lines(self):
109
"""The lines of the 'other' version of the file."""
110
return self.merger.get_lines(self.merger.other_tree, self.file_id)
71
113
class Merger(object):
72
117
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
118
this_tree=None, pb=None, change_reporter=None,
74
119
recurse='down', revision_graph=None):
256
315
if self.this_rev_id is None:
257
316
if self.this_basis_tree.get_file_sha1(file_id) != \
258
317
self.this_tree.get_file_sha1(file_id):
259
raise WorkingTreeNotRevision(self.this_tree)
318
raise errors.WorkingTreeNotRevision(self.this_tree)
261
320
trees = (self.this_basis_tree, self.other_tree)
262
321
return [get_id(tree, file_id) for tree in trees]
323
@deprecated_method(deprecated_in((2, 1, 0)))
264
324
def check_basis(self, check_clean, require_commits=True):
265
325
if self.this_basis is None and require_commits is True:
266
raise BzrCommandError("This branch has no commits."
267
" (perhaps you would prefer 'bzr pull')")
326
raise errors.BzrCommandError(
327
"This branch has no commits."
328
" (perhaps you would prefer 'bzr pull')")
269
330
self.compare_basis()
270
331
if self.this_basis != self.this_rev_id:
271
332
raise errors.UncommittedChanges(self.this_tree)
334
@deprecated_method(deprecated_in((2, 1, 0)))
273
335
def compare_basis(self):
275
337
basis_tree = self.revision_tree(self.this_tree.last_revision())
432
495
'other_tree': self.other_tree,
433
496
'interesting_ids': self.interesting_ids,
434
497
'interesting_files': self.interesting_files,
498
'pp': self.pp, 'this_branch': self.this_branch,
436
499
'do_merge': False}
437
500
if self.merge_type.requires_base:
438
501
kwargs['base_tree'] = self.base_tree
439
502
if self.merge_type.supports_reprocess:
440
503
kwargs['reprocess'] = self.reprocess
441
504
elif self.reprocess:
442
raise BzrError("Conflict reduction is not supported for merge"
443
" type %s." % self.merge_type)
505
raise errors.BzrError(
506
"Conflict reduction is not supported for merge"
507
" type %s." % self.merge_type)
444
508
if self.merge_type.supports_show_base:
445
509
kwargs['show_base'] = self.show_base
446
510
elif self.show_base:
447
raise BzrError("Showing base is not supported for this"
448
" merge type. %s" % self.merge_type)
511
raise errors.BzrError("Showing base is not supported for this"
512
" merge type. %s" % self.merge_type)
449
513
if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
450
514
and not self.base_is_other_ancestor):
451
515
raise errors.CannotReverseCherrypick()
539
604
def __init__(self, working_tree, this_tree, base_tree, other_tree,
540
605
interesting_ids=None, reprocess=False, show_base=False,
541
pb=DummyProgress(), pp=None, change_reporter=None,
606
pb=progress.DummyProgress(), pp=None, change_reporter=None,
542
607
interesting_files=None, do_merge=True,
543
cherrypick=False, lca_trees=None):
608
cherrypick=False, lca_trees=None, this_branch=None):
544
609
"""Initialize the merger object and perform the merge.
546
611
:param working_tree: The working tree to apply the merge to
547
612
:param this_tree: The local tree in the merge operation
548
613
:param base_tree: The common tree in the merge operation
549
614
:param other_tree: The other tree to merge changes from
615
:param this_branch: The branch associated with this_tree
550
616
:param interesting_ids: The file_ids of files that should be
551
617
participate in the merge. May not be combined with
552
618
interesting_files.
1136
1193
if winner == 'this':
1137
1194
# No interesting changes introduced by OTHER
1138
1195
return "unmodified"
1196
# We have a hypothetical conflict, but if we have files, then we
1197
# can try to merge the content
1139
1198
trans_id = self.tt.trans_id_file_id(file_id)
1140
if winner == 'other':
1199
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1200
other_pair[0], winner)
1201
hooks = Merger.hooks['merge_file_content']
1202
hooks = list(hooks) + [self.default_text_merge]
1203
hook_status = 'not_applicable'
1205
hook_status, lines = hook(params)
1206
if hook_status != 'not_applicable':
1207
# Don't try any more hooks, this one applies.
1210
if hook_status == 'not_applicable':
1211
# This is a contents conflict, because none of the available
1212
# functions could merge it.
1214
name = self.tt.final_name(trans_id)
1215
parent_id = self.tt.final_parent(trans_id)
1216
if self.this_tree.has_id(file_id):
1217
self.tt.unversion_file(trans_id)
1218
file_group = self._dump_conflicts(name, parent_id, file_id,
1220
self._raw_conflicts.append(('contents conflict', file_group))
1221
elif hook_status == 'success':
1222
self.tt.create_file(lines, trans_id)
1223
elif hook_status == 'conflicted':
1224
# XXX: perhaps the hook should be able to provide
1225
# the BASE/THIS/OTHER files?
1226
self.tt.create_file(lines, trans_id)
1227
self._raw_conflicts.append(('text conflict', trans_id))
1228
name = self.tt.final_name(trans_id)
1229
parent_id = self.tt.final_parent(trans_id)
1230
self._dump_conflicts(name, parent_id, file_id)
1231
elif hook_status == 'delete':
1232
self.tt.unversion_file(trans_id)
1234
elif hook_status == 'done':
1235
# The hook function did whatever it needs to do directly, no
1236
# further action needed here.
1239
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1240
if not self.this_tree.has_id(file_id) and result == "modified":
1241
self.tt.version_file(file_id, trans_id)
1242
# The merge has been performed, so the old contents should not be
1245
self.tt.delete_contents(trans_id)
1246
except errors.NoSuchFile:
1250
def _default_other_winner_merge(self, merge_hook_params):
1251
"""Replace this contents with other."""
1252
file_id = merge_hook_params.file_id
1253
trans_id = merge_hook_params.trans_id
1254
file_in_this = self.this_tree.has_id(file_id)
1255
if self.other_tree.has_id(file_id):
1256
# OTHER changed the file
1258
if wt.supports_content_filtering():
1259
# We get the path from the working tree if it exists.
1260
# That fails though when OTHER is adding a file, so
1261
# we fall back to the other tree to find the path if
1262
# it doesn't exist locally.
1264
filter_tree_path = wt.id2path(file_id)
1265
except errors.NoSuchId:
1266
filter_tree_path = self.other_tree.id2path(file_id)
1268
# Skip the id2path lookup for older formats
1269
filter_tree_path = None
1270
transform.create_from_tree(self.tt, trans_id,
1271
self.other_tree, file_id,
1272
filter_tree_path=filter_tree_path)
1275
# OTHER deleted the file
1276
return 'delete', None
1278
raise AssertionError(
1279
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1282
def default_text_merge(self, merge_hook_params):
1283
if merge_hook_params.winner == 'other':
1141
1284
# 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)
1285
return self._default_other_winner_merge(merge_hook_params)
1286
elif merge_hook_params.is_file_merge():
1287
# THIS and OTHER are both files, so text merge. Either
1288
# BASE is a file, or both converted to files, so at least we
1289
# have agreement that output should be a file.
1291
self.text_merge(merge_hook_params.file_id,
1292
merge_hook_params.trans_id)
1293
except errors.BinaryFile:
1294
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()
1297
return 'not_applicable', None
1179
1299
def get_lines(self, tree, file_id):
1180
1300
"""Return the lines in a file, or an empty list."""
1301
if tree.has_id(file_id):
1182
1302
return tree.get_file(file_id).readlines()
1370
1508
supports_reverse_cherrypick = False
1371
1509
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,
1511
def _generate_merge_plan(self, file_id, base):
1512
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1515
def _merged_lines(self, file_id):
1516
"""Generate the merged lines.
1517
There is no distinction between lines that are meant to contain <<<<<<<
1521
base = self.base_tree
1524
plan = self._generate_merge_plan(file_id, base)
1384
1525
if 'merge' in debug.debug_flags:
1385
1526
plan = list(plan)
1386
1527
trans_id = self.tt.trans_id_file_id(file_id)
1387
1528
name = self.tt.final_name(trans_id) + '.plan'
1388
contents = ('%10s|%s' % l for l in plan)
1529
contents = ('%11s|%s' % l for l in plan)
1389
1530
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)
1531
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1532
'>>>>>>> MERGE-SOURCE\n')
1533
lines, conflicts = textmerge.merge_lines(self.reprocess)
1535
base_lines = textmerge.base_from_plan()
1538
return lines, base_lines
1394
1540
def text_merge(self, file_id, trans_id):
1395
1541
"""Perform a (weave) text merge for a given file and file-id.
1396
1542
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1397
1543
and a conflict will be noted.
1399
lines, conflicts = self._merged_lines(file_id)
1545
lines, base_lines = self._merged_lines(file_id)
1400
1546
lines = list(lines)
1401
1547
# Note we're checking whether the OUTPUT is binary in this case,
1402
1548
# because we don't want to get into weave merge guts.
1403
check_text_lines(lines)
1549
textfile.check_text_lines(lines)
1404
1550
self.tt.create_file(lines, trans_id)
1551
if base_lines is not None:
1406
1553
self._raw_conflicts.append(('text conflict', trans_id))
1407
1554
name = self.tt.final_name(trans_id)
1408
1555
parent_id = self.tt.final_parent(trans_id)
1409
1556
file_group = self._dump_conflicts(name, parent_id, file_id,
1558
base_lines=base_lines)
1411
1559
file_group.append(trans_id)
1414
1562
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,
1564
def _generate_merge_plan(self, file_id, base):
1565
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
1568
class Diff3Merger(Merge3Merger):
1439
1569
"""Three-way merger using external diff3 for text merging"""
1441
1571
def dump_file(self, temp_dir, name, tree, file_id):
1442
out_path = pathjoin(temp_dir, name)
1572
out_path = osutils.pathjoin(temp_dir, name)
1443
1573
out_file = open(out_path, "wb")
1445
1575
in_file = tree.get_file(file_id)