15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from itertools import chain
18
23
from bzrlib import (
19
branch as _mod_branch,
20
conflicts as _mod_conflicts,
24
26
graph as _mod_graph,
30
30
revision as _mod_revision,
39
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
43
63
# TODO: Report back as changes are merged in
46
66
def transform_tree(from_tree, to_tree, interesting_ids=None):
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 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
:ivar affected_files: The configured file paths to merge.
103
:cvar name_prefix: The prefix to use when looking up configuration
105
:cvar default_files: The default file paths to merge when no configuration
112
def __init__(self, merger):
113
super(ConfigurableFileMerger, self).__init__(merger)
114
self.affected_files = None
115
self.default_files = self.__class__.default_files or []
116
self.name_prefix = self.__class__.name_prefix
117
if self.name_prefix is None:
118
raise ValueError("name_prefix must be set.")
120
def filename_matches_config(self, params):
121
affected_files = self.affected_files
122
if affected_files is None:
123
config = self.merger.this_tree.branch.get_config()
124
# Until bzr provides a better policy for caching the config, we
125
# just add the part we're interested in to the params to avoid
126
# reading the config files repeatedly (bazaar.conf, location.conf,
128
config_key = self.name_prefix + '_merge_files'
129
affected_files = config.get_user_option_as_list(config_key)
130
if affected_files is None:
131
# If nothing was specified in the config, use the default.
132
affected_files = self.default_files
133
self.affected_files = affected_files
135
filename = self.merger.this_tree.id2path(params.file_id)
136
if filename in affected_files:
140
def merge_contents(self, params):
141
"""Merge the contents of a single file."""
142
# First, check whether this custom merge logic should be used. We
143
# expect most files should not be merged by this handler.
145
# OTHER is a straight winner, rely on default merge.
146
params.winner == 'other' or
147
# THIS and OTHER aren't both files.
148
not params.is_file_merge() or
149
# The filename isn't listed in the 'NAME_merge_files' config
151
not self.filename_matches_config(params)):
152
return 'not_applicable', None
153
return self.merge_text(self, params)
155
def merge_text(self, params):
156
"""Merge the byte contents of a single file.
158
This is called after checking that the merge should be performed in
159
merge_contents, and it should behave as per
160
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
162
raise NotImplementedError(self.merge_text)
165
class MergeHookParams(object):
166
"""Object holding parameters passed to merge_file_content hooks.
168
There are some fields hooks can access:
170
:ivar file_id: the file ID of the file being merged
171
:ivar trans_id: the transform ID for the merge of this file
172
:ivar this_kind: kind of file_id in 'this' tree
173
:ivar other_kind: kind of file_id in 'other' tree
174
:ivar winner: one of 'this', 'other', 'conflict'
177
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
179
self._merger = merger
180
self.file_id = file_id
181
self.trans_id = trans_id
182
self.this_kind = this_kind
183
self.other_kind = other_kind
186
def is_file_merge(self):
187
"""True if this_kind and other_kind are both 'file'."""
188
return self.this_kind == 'file' and self.other_kind == 'file'
190
@decorators.cachedproperty
191
def base_lines(self):
192
"""The lines of the 'base' version of the file."""
193
return self._merger.get_lines(self._merger.base_tree, self.file_id)
195
@decorators.cachedproperty
196
def this_lines(self):
197
"""The lines of the 'this' version of the file."""
198
return self._merger.get_lines(self._merger.this_tree, self.file_id)
200
@decorators.cachedproperty
201
def other_lines(self):
202
"""The lines of the 'other' version of the file."""
203
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)
206
71
class Merger(object):
210
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
211
73
this_tree=None, pb=None, change_reporter=None,
212
74
recurse='down', revision_graph=None):
408
257
if self.this_rev_id is None:
409
258
if self.this_basis_tree.get_file_sha1(file_id) != \
410
259
self.this_tree.get_file_sha1(file_id):
411
raise errors.WorkingTreeNotRevision(self.this_tree)
260
raise WorkingTreeNotRevision(self.this_tree)
413
262
trees = (self.this_basis_tree, self.other_tree)
414
263
return [get_id(tree, file_id) for tree in trees]
416
@deprecated_method(deprecated_in((2, 1, 0)))
417
265
def check_basis(self, check_clean, require_commits=True):
418
266
if self.this_basis is None and require_commits is True:
419
raise errors.BzrCommandError(
420
"This branch has no commits."
421
" (perhaps you would prefer 'bzr pull')")
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
423
270
self.compare_basis()
424
271
if self.this_basis != self.this_rev_id:
425
272
raise errors.UncommittedChanges(self.this_tree)
427
@deprecated_method(deprecated_in((2, 1, 0)))
428
274
def compare_basis(self):
430
276
basis_tree = self.revision_tree(self.this_tree.last_revision())
431
277
except errors.NoSuchRevision:
432
278
basis_tree = self.this_tree.basis_tree()
433
if not self.this_tree.has_changes(basis_tree):
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
434
281
self.this_rev_id = self.this_basis
436
283
def set_interesting_files(self, file_list):
437
284
self.interesting_files = file_list
439
286
def set_pending(self):
440
if (not self.base_is_ancestor or not self.base_is_other_ancestor
441
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:
443
289
self._add_parent()
1289
1138
if winner == 'this':
1290
1139
# No interesting changes introduced by OTHER
1291
1140
return "unmodified"
1292
# We have a hypothetical conflict, but if we have files, then we
1293
# can try to merge the content
1294
1141
trans_id = self.tt.trans_id_file_id(file_id)
1295
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1296
other_pair[0], winner)
1297
hooks = self.active_hooks
1298
hook_status = 'not_applicable'
1300
hook_status, lines = hook.merge_contents(params)
1301
if hook_status != 'not_applicable':
1302
# Don't try any more hooks, this one applies.
1305
if hook_status == 'not_applicable':
1306
# This is a contents conflict, because none of the available
1307
# functions could merge it.
1309
name = self.tt.final_name(trans_id)
1310
parent_id = self.tt.final_parent(trans_id)
1311
if self.this_tree.has_id(file_id):
1312
self.tt.unversion_file(trans_id)
1313
file_group = self._dump_conflicts(name, parent_id, file_id,
1315
self._raw_conflicts.append(('contents conflict', file_group))
1316
elif hook_status == 'success':
1317
self.tt.create_file(lines, trans_id)
1318
elif hook_status == 'conflicted':
1319
# XXX: perhaps the hook should be able to provide
1320
# the BASE/THIS/OTHER files?
1321
self.tt.create_file(lines, trans_id)
1322
self._raw_conflicts.append(('text conflict', trans_id))
1323
name = self.tt.final_name(trans_id)
1324
parent_id = self.tt.final_parent(trans_id)
1325
self._dump_conflicts(name, parent_id, file_id)
1326
elif hook_status == 'delete':
1327
self.tt.unversion_file(trans_id)
1329
elif hook_status == 'done':
1330
# The hook function did whatever it needs to do directly, no
1331
# further action needed here.
1334
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1335
if not self.this_tree.has_id(file_id) and result == "modified":
1336
self.tt.version_file(file_id, trans_id)
1337
# The merge has been performed, so the old contents should not be
1340
self.tt.delete_contents(trans_id)
1341
except errors.NoSuchFile:
1345
def _default_other_winner_merge(self, merge_hook_params):
1346
"""Replace this contents with other."""
1347
file_id = merge_hook_params.file_id
1348
trans_id = merge_hook_params.trans_id
1349
file_in_this = self.this_tree.has_id(file_id)
1350
if self.other_tree.has_id(file_id):
1351
# OTHER changed the file
1353
if wt.supports_content_filtering():
1354
# We get the path from the working tree if it exists.
1355
# That fails though when OTHER is adding a file, so
1356
# we fall back to the other tree to find the path if
1357
# it doesn't exist locally.
1359
filter_tree_path = wt.id2path(file_id)
1360
except errors.NoSuchId:
1361
filter_tree_path = self.other_tree.id2path(file_id)
1363
# Skip the id2path lookup for older formats
1364
filter_tree_path = None
1365
transform.create_from_tree(self.tt, trans_id,
1366
self.other_tree, file_id,
1367
filter_tree_path=filter_tree_path)
1370
# OTHER deleted the file
1371
return 'delete', None
1373
raise AssertionError(
1374
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1377
def merge_contents(self, merge_hook_params):
1378
"""Fallback merge logic after user installed hooks."""
1379
# This function is used in merge hooks as the fallback instance.
1380
# Perhaps making this function and the functions it calls be a
1381
# a separate class would be better.
1382
if merge_hook_params.winner == 'other':
1142
if winner == 'other':
1383
1143
# OTHER is a straight winner, so replace this contents with other
1384
return self._default_other_winner_merge(merge_hook_params)
1385
elif merge_hook_params.is_file_merge():
1386
# THIS and OTHER are both files, so text merge. Either
1387
# BASE is a file, or both converted to files, so at least we
1388
# have agreement that output should be a file.
1390
self.text_merge(merge_hook_params.file_id,
1391
merge_hook_params.trans_id)
1392
except errors.BinaryFile:
1393
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)
1396
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()
1398
1181
def get_lines(self, tree, file_id):
1399
1182
"""Return the lines in a file, or an empty list."""
1400
if tree.has_id(file_id):
1401
1184
return tree.get_file(file_id).readlines()
1607
1372
supports_reverse_cherrypick = False
1608
1373
history_based = True
1610
def _generate_merge_plan(self, file_id, base):
1611
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,
1614
def _merged_lines(self, file_id):
1615
"""Generate the merged lines.
1616
There is no distinction between lines that are meant to contain <<<<<<<
1620
base = self.base_tree
1623
plan = self._generate_merge_plan(file_id, base)
1624
1386
if 'merge' in debug.debug_flags:
1625
1387
plan = list(plan)
1626
1388
trans_id = self.tt.trans_id_file_id(file_id)
1627
1389
name = self.tt.final_name(trans_id) + '.plan'
1628
contents = ('%11s|%s' % l for l in plan)
1390
contents = ('%10s|%s' % l for l in plan)
1629
1391
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1630
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1631
'>>>>>>> MERGE-SOURCE\n')
1632
lines, conflicts = textmerge.merge_lines(self.reprocess)
1634
base_lines = textmerge.base_from_plan()
1637
return lines, base_lines
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1394
return textmerge.merge_lines(self.reprocess)
1639
1396
def text_merge(self, file_id, trans_id):
1640
1397
"""Perform a (weave) text merge for a given file and file-id.
1641
1398
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1642
1399
and a conflict will be noted.
1644
lines, base_lines = self._merged_lines(file_id)
1401
lines, conflicts = self._merged_lines(file_id)
1645
1402
lines = list(lines)
1646
1403
# Note we're checking whether the OUTPUT is binary in this case,
1647
1404
# because we don't want to get into weave merge guts.
1648
textfile.check_text_lines(lines)
1405
check_text_lines(lines)
1649
1406
self.tt.create_file(lines, trans_id)
1650
if base_lines is not None:
1652
1408
self._raw_conflicts.append(('text conflict', trans_id))
1653
1409
name = self.tt.final_name(trans_id)
1654
1410
parent_id = self.tt.final_parent(trans_id)
1655
1411
file_group = self._dump_conflicts(name, parent_id, file_id,
1657
base_lines=base_lines)
1658
1413
file_group.append(trans_id)
1661
1416
class LCAMerger(WeaveMerger):
1663
def _generate_merge_plan(self, file_id, base):
1664
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)
1667
1440
class Diff3Merger(Merge3Merger):
1668
1441
"""Three-way merger using external diff3 for text merging"""
1670
1443
def dump_file(self, temp_dir, name, tree, file_id):
1671
out_path = osutils.pathjoin(temp_dir, name)
1444
out_path = pathjoin(temp_dir, name)
1672
1445
out_file = open(out_path, "wb")
1674
1447
in_file = tree.get_file(file_id)