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
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)
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
216
71
class Merger(object):
220
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
221
73
this_tree=None, pb=None, change_reporter=None,
222
74
recurse='down', revision_graph=None):
417
257
if self.this_rev_id is None:
418
258
if self.this_basis_tree.get_file_sha1(file_id) != \
419
259
self.this_tree.get_file_sha1(file_id):
420
raise errors.WorkingTreeNotRevision(self.this_tree)
260
raise WorkingTreeNotRevision(self.this_tree)
422
262
trees = (self.this_basis_tree, self.other_tree)
423
263
return [get_id(tree, file_id) for tree in trees]
425
@deprecated_method(deprecated_in((2, 1, 0)))
426
265
def check_basis(self, check_clean, require_commits=True):
427
266
if self.this_basis is None and require_commits is True:
428
raise errors.BzrCommandError(
429
"This branch has no commits."
430
" (perhaps you would prefer 'bzr pull')")
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
432
270
self.compare_basis()
433
271
if self.this_basis != self.this_rev_id:
434
272
raise errors.UncommittedChanges(self.this_tree)
436
@deprecated_method(deprecated_in((2, 1, 0)))
437
274
def compare_basis(self):
439
276
basis_tree = self.revision_tree(self.this_tree.last_revision())
440
277
except errors.NoSuchRevision:
441
278
basis_tree = self.this_tree.basis_tree()
442
if not self.this_tree.has_changes(basis_tree):
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
443
281
self.this_rev_id = self.this_basis
445
283
def set_interesting_files(self, file_list):
446
284
self.interesting_files = file_list
448
286
def set_pending(self):
449
if (not self.base_is_ancestor or not self.base_is_other_ancestor
450
or self.other_rev_id is None):
287
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
452
289
self._add_parent()
454
291
def _add_parent(self):
455
292
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
456
293
new_parent_trees = []
457
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
458
294
for revision_id in new_parents:
460
296
tree = self.revision_tree(revision_id)
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=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 = 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)
603
self.tt = TreeTransform(self.this_tree, self.pb)
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
606
self._compute_transform()
608
results = self.tt.apply(no_conflicts=True)
609
self.write_modified(results)
611
self.this_tree.add_conflicts(self.cooked_conflicts)
612
except UnsupportedOperation:
616
self.other_tree.unlock()
617
self.base_tree.unlock()
618
self.this_tree.unlock()
777
621
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
779
622
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
781
623
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()
624
self.tt = TransformPreview(self.this_tree)
627
self._compute_transform()
630
self.other_tree.unlock()
631
self.base_tree.unlock()
790
635
def _compute_transform(self):
1283
1138
if winner == 'this':
1284
1139
# No interesting changes introduced by OTHER
1285
1140
return "unmodified"
1286
# We have a hypothetical conflict, but if we have files, then we
1287
# can try to merge the content
1288
1141
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':
1142
if winner == 'other':
1377
1143
# 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
1144
file_in_this = file_id in self.this_tree
1146
# Remove any existing contents
1147
self.tt.delete_contents(trans_id)
1148
if file_id in self.other_tree:
1149
# OTHER changed the file
1150
create_from_tree(self.tt, trans_id,
1151
self.other_tree, file_id)
1152
if not file_in_this:
1153
self.tt.version_file(file_id, trans_id)
1156
# OTHER deleted the file
1157
self.tt.unversion_file(trans_id)
1390
return 'not_applicable', None
1160
# We have a hypothetical conflict, but if we have files, then we
1161
# can try to merge the content
1162
if this_pair[0] == 'file' and other_pair[0] == 'file':
1163
# THIS and OTHER are both files, so text merge. Either
1164
# BASE is a file, or both converted to files, so at least we
1165
# have agreement that output should be a file.
1167
self.text_merge(file_id, trans_id)
1169
return contents_conflict()
1170
if file_id not in self.this_tree:
1171
self.tt.version_file(file_id, trans_id)
1173
self.tt.tree_kind(trans_id)
1174
self.tt.delete_contents(trans_id)
1179
return contents_conflict()
1392
1181
def get_lines(self, tree, file_id):
1393
1182
"""Return the lines in a file, or an empty list."""
1394
if tree.has_id(file_id):
1395
1184
return tree.get_file(file_id).readlines()
1601
1372
supports_reverse_cherrypick = False
1602
1373
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,
1375
def _merged_lines(self, file_id):
1376
"""Generate the merged lines.
1377
There is no distinction between lines that are meant to contain <<<<<<<
1381
base = self.base_tree
1384
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
1386
if 'merge' in debug.debug_flags:
1619
1387
plan = list(plan)
1620
1388
trans_id = self.tt.trans_id_file_id(file_id)
1621
1389
name = self.tt.final_name(trans_id) + '.plan'
1622
contents = ('%11s|%s' % l for l in plan)
1390
contents = ('%10s|%s' % l for l in plan)
1623
1391
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
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1394
return textmerge.merge_lines(self.reprocess)
1633
1396
def text_merge(self, file_id, trans_id):
1634
1397
"""Perform a (weave) text merge for a given file and file-id.
1635
1398
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1636
1399
and a conflict will be noted.
1638
lines, base_lines = self._merged_lines(file_id)
1401
lines, conflicts = self._merged_lines(file_id)
1639
1402
lines = list(lines)
1640
1403
# Note we're checking whether the OUTPUT is binary in this case,
1641
1404
# because we don't want to get into weave merge guts.
1642
textfile.check_text_lines(lines)
1405
check_text_lines(lines)
1643
1406
self.tt.create_file(lines, trans_id)
1644
if base_lines is not None:
1646
1408
self._raw_conflicts.append(('text conflict', trans_id))
1647
1409
name = self.tt.final_name(trans_id)
1648
1410
parent_id = self.tt.final_parent(trans_id)
1649
1411
file_group = self._dump_conflicts(name, parent_id, file_id,
1651
base_lines=base_lines)
1652
1413
file_group.append(trans_id)
1655
1416
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,
1418
def _merged_lines(self, file_id):
1419
"""Generate the merged lines.
1420
There is no distinction between lines that are meant to contain <<<<<<<
1424
base = self.base_tree
1427
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1429
if 'merge' in debug.debug_flags:
1431
trans_id = self.tt.trans_id_file_id(file_id)
1432
name = self.tt.final_name(trans_id) + '.plan'
1433
contents = ('%10s|%s' % l for l in plan)
1434
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1435
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1436
'>>>>>>> MERGE-SOURCE\n')
1437
return textmerge.merge_lines(self.reprocess)
1661
1440
class Diff3Merger(Merge3Merger):
1662
1441
"""Three-way merger using external diff3 for text merging"""
1664
1443
def dump_file(self, temp_dir, name, tree, file_id):
1665
out_path = osutils.pathjoin(temp_dir, name)
1444
out_path = pathjoin(temp_dir, name)
1666
1445
out_file = open(out_path, "wb")
1668
1447
in_file = tree.get_file(file_id)