13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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,
31
30
revision as _mod_revision,
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()
50
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
51
interesting_ids=interesting_ids, this_tree=from_tree)
56
class MergeHooks(hooks.Hooks):
59
hooks.Hooks.__init__(self)
60
self.create_hook(hooks.HookPoint('merge_file_content',
61
"Called with a bzrlib.merge.Merger object to create a per file "
62
"merge object when starting a merge. "
63
"Should return either None or a subclass of "
64
"``bzrlib.merge.AbstractPerFileMerger``. "
65
"Such objects will then be called per file "
66
"that needs to be merged (including when one "
67
"side has deleted the file and the other has changed it). "
68
"See the AbstractPerFileMerger API docs for details on how it is "
73
class AbstractPerFileMerger(object):
74
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
76
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
78
:ivar merger: The Merge3Merger performing the merge.
81
def __init__(self, merger):
82
"""Create a PerFileMerger for use with merger."""
85
def merge_contents(self, merge_params):
86
"""Attempt to merge the contents of a single file.
88
:param merge_params: A bzrlib.merge.MergeHookParams
89
:return : A tuple of (status, chunks), where status is one of
90
'not_applicable', 'success', 'conflicted', or 'delete'. If status
91
is 'success' or 'conflicted', then chunks should be an iterable of
92
strings for the new file contents.
94
return ('not applicable', None)
97
class ConfigurableFileMerger(AbstractPerFileMerger):
98
"""Merge individual files when configured via a .conf file.
100
This is a base class for concrete custom file merging logic. Concrete
101
classes should implement ``merge_text``.
103
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
105
:ivar affected_files: The configured file paths to merge.
107
:cvar name_prefix: The prefix to use when looking up configuration
108
details. <name_prefix>_merge_files describes the files targeted by the
111
:cvar default_files: The default file paths to merge when no configuration
118
def __init__(self, merger):
119
super(ConfigurableFileMerger, self).__init__(merger)
120
self.affected_files = None
121
self.default_files = self.__class__.default_files or []
122
self.name_prefix = self.__class__.name_prefix
123
if self.name_prefix is None:
124
raise ValueError("name_prefix must be set.")
126
def filename_matches_config(self, params):
127
"""Check whether the file should call the merge hook.
129
<name_prefix>_merge_files configuration variable is a list of files
130
that should use the hook.
132
affected_files = self.affected_files
133
if affected_files is None:
134
config = self.merger.this_tree.branch.get_config()
135
# Until bzr provides a better policy for caching the config, we
136
# just add the part we're interested in to the params to avoid
137
# reading the config files repeatedly (bazaar.conf, location.conf,
139
config_key = self.name_prefix + '_merge_files'
140
affected_files = config.get_user_option_as_list(config_key)
141
if affected_files is None:
142
# If nothing was specified in the config, use the default.
143
affected_files = self.default_files
144
self.affected_files = affected_files
146
filename = self.merger.this_tree.id2path(params.file_id)
147
if filename in affected_files:
151
def merge_contents(self, params):
152
"""Merge the contents of a single file."""
153
# First, check whether this custom merge logic should be used. We
154
# expect most files should not be merged by this handler.
156
# OTHER is a straight winner, rely on default merge.
157
params.winner == 'other' or
158
# THIS and OTHER aren't both files.
159
not params.is_file_merge() or
160
# The filename isn't listed in the 'NAME_merge_files' config
162
not self.filename_matches_config(params)):
163
return 'not_applicable', None
164
return self.merge_text(params)
166
def merge_text(self, params):
167
"""Merge the byte contents of a single file.
169
This is called after checking that the merge should be performed in
170
merge_contents, and it should behave as per
171
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
173
raise NotImplementedError(self.merge_text)
176
class MergeHookParams(object):
177
"""Object holding parameters passed to merge_file_content hooks.
179
There are some fields hooks can access:
181
:ivar file_id: the file ID of the file being merged
182
:ivar trans_id: the transform ID for the merge of this file
183
:ivar this_kind: kind of file_id in 'this' tree
184
:ivar other_kind: kind of file_id in 'other' tree
185
:ivar winner: one of 'this', 'other', 'conflict'
188
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
190
self._merger = merger
191
self.file_id = file_id
192
self.trans_id = trans_id
193
self.this_kind = this_kind
194
self.other_kind = other_kind
197
def is_file_merge(self):
198
"""True if this_kind and other_kind are both 'file'."""
199
return self.this_kind == 'file' and self.other_kind == 'file'
201
@decorators.cachedproperty
202
def base_lines(self):
203
"""The lines of the 'base' version of the file."""
204
return self._merger.get_lines(self._merger.base_tree, self.file_id)
206
@decorators.cachedproperty
207
def this_lines(self):
208
"""The lines of the 'this' version of the file."""
209
return self._merger.get_lines(self._merger.this_tree, self.file_id)
211
@decorators.cachedproperty
212
def other_lines(self):
213
"""The lines of the 'other' version of the file."""
214
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)
217
71
class Merger(object):
221
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
222
this_tree=None, pb=None, change_reporter=None,
73
this_tree=None, pb=DummyProgress(), change_reporter=None,
223
74
recurse='down', revision_graph=None):
224
75
object.__init__(self)
225
76
self.this_branch = this_branch
418
255
if self.this_rev_id is None:
419
256
if self.this_basis_tree.get_file_sha1(file_id) != \
420
257
self.this_tree.get_file_sha1(file_id):
421
raise errors.WorkingTreeNotRevision(self.this_tree)
258
raise WorkingTreeNotRevision(self.this_tree)
423
260
trees = (self.this_basis_tree, self.other_tree)
424
261
return [get_id(tree, file_id) for tree in trees]
426
@deprecated_method(deprecated_in((2, 1, 0)))
427
263
def check_basis(self, check_clean, require_commits=True):
428
264
if self.this_basis is None and require_commits is True:
429
raise errors.BzrCommandError(
430
"This branch has no commits."
431
" (perhaps you would prefer 'bzr pull')")
265
raise BzrCommandError("This branch has no commits."
266
" (perhaps you would prefer 'bzr pull')")
433
268
self.compare_basis()
434
269
if self.this_basis != self.this_rev_id:
435
270
raise errors.UncommittedChanges(self.this_tree)
437
@deprecated_method(deprecated_in((2, 1, 0)))
438
272
def compare_basis(self):
440
274
basis_tree = self.revision_tree(self.this_tree.last_revision())
441
275
except errors.NoSuchRevision:
442
276
basis_tree = self.this_tree.basis_tree()
443
if not self.this_tree.has_changes(basis_tree):
277
changes = self.this_tree.changes_from(basis_tree)
278
if not changes.has_changed():
444
279
self.this_rev_id = self.this_basis
446
281
def set_interesting_files(self, file_list):
447
282
self.interesting_files = file_list
449
284
def set_pending(self):
450
if (not self.base_is_ancestor or not self.base_is_other_ancestor
451
or self.other_rev_id is None):
285
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
453
287
self._add_parent()
707
537
def __init__(self, working_tree, this_tree, base_tree, other_tree,
708
538
interesting_ids=None, reprocess=False, show_base=False,
709
pb=None, pp=None, change_reporter=None,
539
pb=DummyProgress(), pp=None, change_reporter=None,
710
540
interesting_files=None, do_merge=True,
711
cherrypick=False, lca_trees=None, this_branch=None):
541
cherrypick=False, lca_trees=None):
712
542
"""Initialize the merger object and perform the merge.
714
544
:param working_tree: The working tree to apply the merge to
715
545
:param this_tree: The local tree in the merge operation
716
546
:param base_tree: The common tree in the merge operation
717
:param other_tree: The other tree to merge changes from
718
:param this_branch: The branch associated with this_tree
547
:param other_tree: The other other tree to merge changes from
719
548
:param interesting_ids: The file_ids of files that should be
720
549
participate in the merge. May not be combined with
721
550
interesting_files.
722
551
:param: reprocess If True, perform conflict-reduction processing.
723
552
:param show_base: If True, show the base revision in text conflicts.
724
553
(incompatible with reprocess)
554
:param pb: A Progress bar
726
555
:param pp: A ProgressPhase object
727
556
:param change_reporter: An object that should report changes made
728
557
:param interesting_files: The tree-relative paths of files that should
1292
1134
if winner == 'this':
1293
1135
# No interesting changes introduced by OTHER
1294
1136
return "unmodified"
1295
# We have a hypothetical conflict, but if we have files, then we
1296
# can try to merge the content
1297
1137
trans_id = self.tt.trans_id_file_id(file_id)
1298
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1299
other_pair[0], winner)
1300
hooks = self.active_hooks
1301
hook_status = 'not_applicable'
1303
hook_status, lines = hook.merge_contents(params)
1304
if hook_status != 'not_applicable':
1305
# Don't try any more hooks, this one applies.
1308
if hook_status == 'not_applicable':
1309
# This is a contents conflict, because none of the available
1310
# functions could merge it.
1312
name = self.tt.final_name(trans_id)
1313
parent_id = self.tt.final_parent(trans_id)
1314
if self.this_tree.has_id(file_id):
1315
self.tt.unversion_file(trans_id)
1316
file_group = self._dump_conflicts(name, parent_id, file_id,
1318
self._raw_conflicts.append(('contents conflict', file_group))
1319
elif hook_status == 'success':
1320
self.tt.create_file(lines, trans_id)
1321
elif hook_status == 'conflicted':
1322
# XXX: perhaps the hook should be able to provide
1323
# the BASE/THIS/OTHER files?
1324
self.tt.create_file(lines, trans_id)
1325
self._raw_conflicts.append(('text conflict', trans_id))
1326
name = self.tt.final_name(trans_id)
1327
parent_id = self.tt.final_parent(trans_id)
1328
self._dump_conflicts(name, parent_id, file_id)
1329
elif hook_status == 'delete':
1330
self.tt.unversion_file(trans_id)
1332
elif hook_status == 'done':
1333
# The hook function did whatever it needs to do directly, no
1334
# further action needed here.
1337
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1338
if not self.this_tree.has_id(file_id) and result == "modified":
1339
self.tt.version_file(file_id, trans_id)
1340
# The merge has been performed, so the old contents should not be
1343
self.tt.delete_contents(trans_id)
1344
except errors.NoSuchFile:
1348
def _default_other_winner_merge(self, merge_hook_params):
1349
"""Replace this contents with other."""
1350
file_id = merge_hook_params.file_id
1351
trans_id = merge_hook_params.trans_id
1352
file_in_this = self.this_tree.has_id(file_id)
1353
if self.other_tree.has_id(file_id):
1354
# OTHER changed the file
1356
if wt.supports_content_filtering():
1357
# We get the path from the working tree if it exists.
1358
# That fails though when OTHER is adding a file, so
1359
# we fall back to the other tree to find the path if
1360
# it doesn't exist locally.
1362
filter_tree_path = wt.id2path(file_id)
1363
except errors.NoSuchId:
1364
filter_tree_path = self.other_tree.id2path(file_id)
1366
# Skip the id2path lookup for older formats
1367
filter_tree_path = None
1368
transform.create_from_tree(self.tt, trans_id,
1369
self.other_tree, file_id,
1370
filter_tree_path=filter_tree_path)
1373
# OTHER deleted the file
1374
return 'delete', None
1376
raise AssertionError(
1377
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1380
def merge_contents(self, merge_hook_params):
1381
"""Fallback merge logic after user installed hooks."""
1382
# This function is used in merge hooks as the fallback instance.
1383
# Perhaps making this function and the functions it calls be a
1384
# a separate class would be better.
1385
if merge_hook_params.winner == 'other':
1138
if winner == 'other':
1386
1139
# OTHER is a straight winner, so replace this contents with other
1387
return self._default_other_winner_merge(merge_hook_params)
1388
elif merge_hook_params.is_file_merge():
1389
# THIS and OTHER are both files, so text merge. Either
1390
# BASE is a file, or both converted to files, so at least we
1391
# have agreement that output should be a file.
1393
self.text_merge(merge_hook_params.file_id,
1394
merge_hook_params.trans_id)
1395
except errors.BinaryFile:
1396
return 'not_applicable', None
1140
file_in_this = file_id in self.this_tree
1142
# Remove any existing contents
1143
self.tt.delete_contents(trans_id)
1144
if file_id in self.other_tree:
1145
# OTHER changed the file
1146
create_from_tree(self.tt, trans_id,
1147
self.other_tree, file_id)
1148
if not file_in_this:
1149
self.tt.version_file(file_id, trans_id)
1152
# OTHER deleted the file
1153
self.tt.unversion_file(trans_id)
1399
return 'not_applicable', None
1156
# We have a hypothetical conflict, but if we have files, then we
1157
# can try to merge the content
1158
if this_pair[0] == 'file' and other_pair[0] == 'file':
1159
# THIS and OTHER are both files, so text merge. Either
1160
# BASE is a file, or both converted to files, so at least we
1161
# have agreement that output should be a file.
1163
self.text_merge(file_id, trans_id)
1165
return contents_conflict()
1166
if file_id not in self.this_tree:
1167
self.tt.version_file(file_id, trans_id)
1169
self.tt.tree_kind(trans_id)
1170
self.tt.delete_contents(trans_id)
1175
return contents_conflict()
1401
1177
def get_lines(self, tree, file_id):
1402
1178
"""Return the lines in a file, or an empty list."""
1403
if tree.has_id(file_id):
1404
1180
return tree.get_file(file_id).readlines()
1610
1368
supports_reverse_cherrypick = False
1611
1369
history_based = True
1613
def _generate_merge_plan(self, file_id, base):
1614
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1371
def _merged_lines(self, file_id):
1372
"""Generate the merged lines.
1373
There is no distinction between lines that are meant to contain <<<<<<<
1377
base = self.base_tree
1380
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1617
def _merged_lines(self, file_id):
1618
"""Generate the merged lines.
1619
There is no distinction between lines that are meant to contain <<<<<<<
1623
base = self.base_tree
1626
plan = self._generate_merge_plan(file_id, base)
1627
1382
if 'merge' in debug.debug_flags:
1628
1383
plan = list(plan)
1629
1384
trans_id = self.tt.trans_id_file_id(file_id)
1630
1385
name = self.tt.final_name(trans_id) + '.plan'
1631
contents = ('%11s|%s' % l for l in plan)
1386
contents = ('%10s|%s' % l for l in plan)
1632
1387
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1633
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1634
'>>>>>>> MERGE-SOURCE\n')
1635
lines, conflicts = textmerge.merge_lines(self.reprocess)
1637
base_lines = textmerge.base_from_plan()
1640
return lines, base_lines
1388
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1389
'>>>>>>> MERGE-SOURCE\n')
1390
return textmerge.merge_lines(self.reprocess)
1642
1392
def text_merge(self, file_id, trans_id):
1643
1393
"""Perform a (weave) text merge for a given file and file-id.
1644
1394
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1645
1395
and a conflict will be noted.
1647
lines, base_lines = self._merged_lines(file_id)
1397
lines, conflicts = self._merged_lines(file_id)
1648
1398
lines = list(lines)
1649
1399
# Note we're checking whether the OUTPUT is binary in this case,
1650
1400
# because we don't want to get into weave merge guts.
1651
textfile.check_text_lines(lines)
1401
check_text_lines(lines)
1652
1402
self.tt.create_file(lines, trans_id)
1653
if base_lines is not None:
1655
1404
self._raw_conflicts.append(('text conflict', trans_id))
1656
1405
name = self.tt.final_name(trans_id)
1657
1406
parent_id = self.tt.final_parent(trans_id)
1658
1407
file_group = self._dump_conflicts(name, parent_id, file_id,
1660
base_lines=base_lines)
1661
1409
file_group.append(trans_id)
1664
1412
class LCAMerger(WeaveMerger):
1666
def _generate_merge_plan(self, file_id, base):
1667
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1414
def _merged_lines(self, file_id):
1415
"""Generate the merged lines.
1416
There is no distinction between lines that are meant to contain <<<<<<<
1420
base = self.base_tree
1423
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1425
if 'merge' in debug.debug_flags:
1427
trans_id = self.tt.trans_id_file_id(file_id)
1428
name = self.tt.final_name(trans_id) + '.plan'
1429
contents = ('%10s|%s' % l for l in plan)
1430
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1431
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1432
'>>>>>>> MERGE-SOURCE\n')
1433
return textmerge.merge_lines(self.reprocess)
1670
1436
class Diff3Merger(Merge3Merger):
1671
1437
"""Three-way merger using external diff3 for text merging"""
1673
1439
def dump_file(self, temp_dir, name, tree, file_id):
1674
out_path = osutils.pathjoin(temp_dir, name)
1440
out_path = pathjoin(temp_dir, name)
1675
1441
out_file = open(out_path, "wb")
1677
1443
in_file = tree.get_file(file_id)