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 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)
71
206
class Merger(object):
72
210
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
211
this_tree=None, pb=None, change_reporter=None,
74
212
recurse='down', revision_graph=None):
257
408
if self.this_rev_id is None:
258
409
if self.this_basis_tree.get_file_sha1(file_id) != \
259
410
self.this_tree.get_file_sha1(file_id):
260
raise WorkingTreeNotRevision(self.this_tree)
411
raise errors.WorkingTreeNotRevision(self.this_tree)
262
413
trees = (self.this_basis_tree, self.other_tree)
263
414
return [get_id(tree, file_id) for tree in trees]
416
@deprecated_method(deprecated_in((2, 1, 0)))
265
417
def check_basis(self, check_clean, require_commits=True):
266
418
if self.this_basis is None and require_commits is True:
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
419
raise errors.BzrCommandError(
420
"This branch has no commits."
421
" (perhaps you would prefer 'bzr pull')")
270
423
self.compare_basis()
271
424
if self.this_basis != self.this_rev_id:
272
425
raise errors.UncommittedChanges(self.this_tree)
427
@deprecated_method(deprecated_in((2, 1, 0)))
274
428
def compare_basis(self):
276
430
basis_tree = self.revision_tree(self.this_tree.last_revision())
277
431
except errors.NoSuchRevision:
278
432
basis_tree = self.this_tree.basis_tree()
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
433
if not self.this_tree.has_changes(basis_tree):
281
434
self.this_rev_id = self.this_basis
283
436
def set_interesting_files(self, file_list):
284
437
self.interesting_files = file_list
286
439
def set_pending(self):
287
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
440
if (not self.base_is_ancestor or not self.base_is_other_ancestor
441
or self.other_rev_id is None):
289
443
self._add_parent()
1136
1289
if winner == 'this':
1137
1290
# No interesting changes introduced by OTHER
1138
1291
return "unmodified"
1292
# We have a hypothetical conflict, but if we have files, then we
1293
# can try to merge the content
1139
1294
trans_id = self.tt.trans_id_file_id(file_id)
1140
if winner == 'other':
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':
1141
1383
# 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)
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
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()
1396
return 'not_applicable', None
1179
1398
def get_lines(self, tree, file_id):
1180
1399
"""Return the lines in a file, or an empty list."""
1400
if tree.has_id(file_id):
1182
1401
return tree.get_file(file_id).readlines()
1370
1607
supports_reverse_cherrypick = False
1371
1608
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,
1610
def _generate_merge_plan(self, file_id, base):
1611
return 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)
1384
1624
if 'merge' in debug.debug_flags:
1385
1625
plan = list(plan)
1386
1626
trans_id = self.tt.trans_id_file_id(file_id)
1387
1627
name = self.tt.final_name(trans_id) + '.plan'
1388
contents = ('%10s|%s' % l for l in plan)
1628
contents = ('%11s|%s' % l for l in plan)
1389
1629
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)
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
1394
1639
def text_merge(self, file_id, trans_id):
1395
1640
"""Perform a (weave) text merge for a given file and file-id.
1396
1641
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1397
1642
and a conflict will be noted.
1399
lines, conflicts = self._merged_lines(file_id)
1644
lines, base_lines = self._merged_lines(file_id)
1400
1645
lines = list(lines)
1401
1646
# Note we're checking whether the OUTPUT is binary in this case,
1402
1647
# because we don't want to get into weave merge guts.
1403
check_text_lines(lines)
1648
textfile.check_text_lines(lines)
1404
1649
self.tt.create_file(lines, trans_id)
1650
if base_lines is not None:
1406
1652
self._raw_conflicts.append(('text conflict', trans_id))
1407
1653
name = self.tt.final_name(trans_id)
1408
1654
parent_id = self.tt.final_parent(trans_id)
1409
1655
file_group = self._dump_conflicts(name, parent_id, file_id,
1657
base_lines=base_lines)
1411
1658
file_group.append(trans_id)
1414
1661
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,
1663
def _generate_merge_plan(self, file_id, base):
1664
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
1667
class Diff3Merger(Merge3Merger):
1439
1668
"""Three-way merger using external diff3 for text merging"""
1441
1670
def dump_file(self, temp_dir, name, tree, file_id):
1442
out_path = pathjoin(temp_dir, name)
1671
out_path = osutils.pathjoin(temp_dir, name)
1443
1672
out_file = open(out_path, "wb")
1445
1674
in_file = tree.get_file(file_id)