14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from itertools import chain
19
23
from bzrlib import (
20
branch as _mod_branch,
21
conflicts as _mod_conflicts,
25
26
graph as _mod_graph,
30
30
revision as _mod_revision,
39
from bzrlib.cleanup import OperationWithCleanups
40
from bzrlib.symbol_versioning import (
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
44
63
# TODO: Report back as changes are merged in
47
66
def transform_tree(from_tree, to_tree, interesting_ids=None):
48
67
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_tree.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)
69
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
70
interesting_ids=interesting_ids, this_tree=from_tree)
216
75
class Merger(object):
220
76
def __init__(self, this_branch, other_tree=None, base_tree=None,
221
77
this_tree=None, pb=None, change_reporter=None,
222
78
recurse='down', revision_graph=None):
696
554
def __init__(self, working_tree, this_tree, base_tree, other_tree,
697
555
interesting_ids=None, reprocess=False, show_base=False,
698
pb=None, pp=None, change_reporter=None,
556
pb=DummyProgress(), pp=None, change_reporter=None,
699
557
interesting_files=None, do_merge=True,
700
cherrypick=False, lca_trees=None, this_branch=None):
558
cherrypick=False, lca_trees=None):
701
559
"""Initialize the merger object and perform the merge.
703
561
:param working_tree: The working tree to apply the merge to
704
562
:param this_tree: The local tree in the merge operation
705
563
:param base_tree: The common tree in the merge operation
706
564
:param other_tree: The other tree to merge changes from
707
:param this_branch: The branch associated with this_tree
708
565
:param interesting_ids: The file_ids of files that should be
709
566
participate in the merge. May not be combined with
710
567
interesting_files.
711
568
:param: reprocess If True, perform conflict-reduction processing.
712
569
:param show_base: If True, show the base revision in text conflicts.
713
570
(incompatible with reprocess)
571
:param pb: A Progress bar
715
572
:param pp: A ProgressPhase object
716
573
:param change_reporter: An object that should report changes made
717
574
:param interesting_files: The tree-relative paths of files that should
744
600
# making sure we haven't missed any corner cases.
745
601
# if lca_trees is None:
746
602
# self._lca_trees = [self.base_tree]
747
605
self.change_reporter = change_reporter
748
606
self.cherrypick = cherrypick
608
self.pp = 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
612
def do_merge(self):
757
operation = OperationWithCleanups(self._do_merge)
758
613
self.this_tree.lock_tree_write()
759
operation.add_cleanup(self.this_tree.unlock)
760
614
self.base_tree.lock_read()
761
operation.add_cleanup(self.base_tree.unlock)
762
615
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)
616
self.tt = TreeTransform(self.this_tree, self.pb)
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
619
self._compute_transform()
621
results = self.tt.apply(no_conflicts=True)
622
self.write_modified(results)
624
self.this_tree.add_conflicts(self.cooked_conflicts)
625
except UnsupportedOperation:
629
self.other_tree.unlock()
630
self.base_tree.unlock()
631
self.this_tree.unlock()
777
634
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
779
635
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
781
636
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
self.tt = transform.TransformPreview(self.this_tree)
787
self._compute_transform()
637
self.tt = TransformPreview(self.this_tree)
640
self._compute_transform()
643
self.other_tree.unlock()
644
self.base_tree.unlock()
790
648
def _compute_transform(self):
1283
1151
if winner == 'this':
1284
1152
# No interesting changes introduced by OTHER
1285
1153
return "unmodified"
1286
# We have a hypothetical conflict, but if we have files, then we
1287
# can try to merge the content
1288
1154
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':
1155
if winner == 'other':
1377
1156
# 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
1157
file_in_this = file_id in self.this_tree
1159
# Remove any existing contents
1160
self.tt.delete_contents(trans_id)
1161
if file_id in self.other_tree:
1162
# OTHER changed the file
1163
create_from_tree(self.tt, trans_id,
1164
self.other_tree, file_id)
1165
if not file_in_this:
1166
self.tt.version_file(file_id, trans_id)
1169
# OTHER deleted the file
1170
self.tt.unversion_file(trans_id)
1390
return 'not_applicable', None
1173
# We have a hypothetical conflict, but if we have files, then we
1174
# can try to merge the content
1175
if this_pair[0] == 'file' and other_pair[0] == 'file':
1176
# THIS and OTHER are both files, so text merge. Either
1177
# BASE is a file, or both converted to files, so at least we
1178
# have agreement that output should be a file.
1180
self.text_merge(file_id, trans_id)
1182
return contents_conflict()
1183
if file_id not in self.this_tree:
1184
self.tt.version_file(file_id, trans_id)
1186
self.tt.tree_kind(trans_id)
1187
self.tt.delete_contents(trans_id)
1192
return contents_conflict()
1392
1194
def get_lines(self, tree, file_id):
1393
1195
"""Return the lines in a file, or an empty list."""
1394
if tree.has_id(file_id):
1395
1197
return tree.get_file(file_id).readlines()
1601
1385
supports_reverse_cherrypick = False
1602
1386
history_based = True
1604
def _generate_merge_plan(self, file_id, base):
1605
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1388
def _merged_lines(self, file_id):
1389
"""Generate the merged lines.
1390
There is no distinction between lines that are meant to contain <<<<<<<
1394
base = self.base_tree
1397
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1608
def _merged_lines(self, file_id):
1609
"""Generate the merged lines.
1610
There is no distinction between lines that are meant to contain <<<<<<<
1614
base = self.base_tree
1617
plan = self._generate_merge_plan(file_id, base)
1618
1399
if 'merge' in debug.debug_flags:
1619
1400
plan = list(plan)
1620
1401
trans_id = self.tt.trans_id_file_id(file_id)
1621
1402
name = self.tt.final_name(trans_id) + '.plan'
1622
contents = ('%11s|%s' % l for l in plan)
1403
contents = ('%10s|%s' % l for l in plan)
1623
1404
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1624
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1625
'>>>>>>> MERGE-SOURCE\n')
1626
lines, conflicts = textmerge.merge_lines(self.reprocess)
1628
base_lines = textmerge.base_from_plan()
1631
return lines, base_lines
1405
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1406
'>>>>>>> MERGE-SOURCE\n')
1407
return textmerge.merge_lines(self.reprocess)
1633
1409
def text_merge(self, file_id, trans_id):
1634
1410
"""Perform a (weave) text merge for a given file and file-id.
1635
1411
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1636
1412
and a conflict will be noted.
1638
lines, base_lines = self._merged_lines(file_id)
1414
lines, conflicts = self._merged_lines(file_id)
1639
1415
lines = list(lines)
1640
1416
# Note we're checking whether the OUTPUT is binary in this case,
1641
1417
# because we don't want to get into weave merge guts.
1642
textfile.check_text_lines(lines)
1418
check_text_lines(lines)
1643
1419
self.tt.create_file(lines, trans_id)
1644
if base_lines is not None:
1646
1421
self._raw_conflicts.append(('text conflict', trans_id))
1647
1422
name = self.tt.final_name(trans_id)
1648
1423
parent_id = self.tt.final_parent(trans_id)
1649
1424
file_group = self._dump_conflicts(name, parent_id, file_id,
1651
base_lines=base_lines)
1652
1426
file_group.append(trans_id)
1655
1429
class LCAMerger(WeaveMerger):
1657
def _generate_merge_plan(self, file_id, base):
1658
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1431
def _merged_lines(self, file_id):
1432
"""Generate the merged lines.
1433
There is no distinction between lines that are meant to contain <<<<<<<
1437
base = self.base_tree
1440
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1442
if 'merge' in debug.debug_flags:
1444
trans_id = self.tt.trans_id_file_id(file_id)
1445
name = self.tt.final_name(trans_id) + '.plan'
1446
contents = ('%10s|%s' % l for l in plan)
1447
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1448
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1449
'>>>>>>> MERGE-SOURCE\n')
1450
return textmerge.merge_lines(self.reprocess)
1661
1453
class Diff3Merger(Merge3Merger):
1662
1454
"""Three-way merger using external diff3 for text merging"""
1664
1456
def dump_file(self, temp_dir, name, tree, file_id):
1665
out_path = osutils.pathjoin(temp_dir, name)
1457
out_path = pathjoin(temp_dir, name)
1666
1458
out_file = open(out_path, "wb")
1668
1460
in_file = tree.get_file(file_id)