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
from bzrlib.lazy_import import lazy_import
20
lazy_import(globals(), """
21
23
from bzrlib import (
22
branch as _mod_branch,
24
conflicts as _mod_conflicts,
27
26
graph as _mod_graph,
31
30
revision as _mod_revision,
47
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
51
63
# TODO: Report back as changes are merged in
54
66
def transform_tree(from_tree, to_tree, interesting_ids=None):
55
from_tree.lock_tree_write()
56
operation = cleanup.OperationWithCleanups(merge_inner)
57
operation.add_cleanup(from_tree.unlock)
58
operation.run_simple(from_tree.branch, to_tree, from_tree,
59
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
62
class MergeHooks(hooks.Hooks):
65
hooks.Hooks.__init__(self, "bzrlib.merge", "Merger.hooks")
66
self.add_hook('merge_file_content',
67
"Called with a bzrlib.merge.Merger object to create a per file "
68
"merge object when starting a merge. "
69
"Should return either None or a subclass of "
70
"``bzrlib.merge.AbstractPerFileMerger``. "
71
"Such objects will then be called per file "
72
"that needs to be merged (including when one "
73
"side has deleted the file and the other has changed it). "
74
"See the AbstractPerFileMerger API docs for details on how it is "
79
class AbstractPerFileMerger(object):
80
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
82
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
84
:ivar merger: The Merge3Merger performing the merge.
87
def __init__(self, merger):
88
"""Create a PerFileMerger for use with merger."""
91
def merge_contents(self, merge_params):
92
"""Attempt to merge the contents of a single file.
94
:param merge_params: A bzrlib.merge.MergeHookParams
95
:return: A tuple of (status, chunks), where status is one of
96
'not_applicable', 'success', 'conflicted', or 'delete'. If status
97
is 'success' or 'conflicted', then chunks should be an iterable of
98
strings for the new file contents.
100
return ('not applicable', None)
103
class PerFileMerger(AbstractPerFileMerger):
104
"""Merge individual files when self.file_matches returns True.
106
This class is intended to be subclassed. The file_matches and
107
merge_matching methods should be overridden with concrete implementations.
110
def file_matches(self, params):
111
"""Return True if merge_matching should be called on this file.
113
Only called with merges of plain files with no clear winner.
115
Subclasses must override this.
117
raise NotImplementedError(self.file_matches)
119
def get_filename(self, params, tree):
120
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
121
self.merger.this_tree) and a MergeHookParams.
123
return osutils.basename(tree.id2path(params.file_id))
125
def get_filepath(self, params, tree):
126
"""Calculate the path to the file in a tree.
128
:param params: A MergeHookParams describing the file to merge
129
:param tree: a Tree, e.g. self.merger.this_tree.
131
return tree.id2path(params.file_id)
133
def merge_contents(self, params):
134
"""Merge the contents of a single file."""
135
# Check whether this custom merge logic should be used.
137
# OTHER is a straight winner, rely on default merge.
138
params.winner == 'other' or
139
# THIS and OTHER aren't both files.
140
not params.is_file_merge() or
141
# The filename doesn't match *.xml
142
not self.file_matches(params)):
143
return 'not_applicable', None
144
return self.merge_matching(params)
146
def merge_matching(self, params):
147
"""Merge the contents of a single file that has matched the criteria
148
in PerFileMerger.merge_contents (is a conflict, is a file,
149
self.file_matches is True).
151
Subclasses must override this.
153
raise NotImplementedError(self.merge_matching)
156
class ConfigurableFileMerger(PerFileMerger):
157
"""Merge individual files when configured via a .conf file.
159
This is a base class for concrete custom file merging logic. Concrete
160
classes should implement ``merge_text``.
162
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
164
:ivar affected_files: The configured file paths to merge.
166
:cvar name_prefix: The prefix to use when looking up configuration
167
details. <name_prefix>_merge_files describes the files targeted by the
170
:cvar default_files: The default file paths to merge when no configuration
177
def __init__(self, merger):
178
super(ConfigurableFileMerger, self).__init__(merger)
179
self.affected_files = None
180
self.default_files = self.__class__.default_files or []
181
self.name_prefix = self.__class__.name_prefix
182
if self.name_prefix is None:
183
raise ValueError("name_prefix must be set.")
185
def file_matches(self, params):
186
"""Check whether the file should call the merge hook.
188
<name_prefix>_merge_files configuration variable is a list of files
189
that should use the hook.
191
affected_files = self.affected_files
192
if affected_files is None:
193
config = self.merger.this_branch.get_config()
194
# Until bzr provides a better policy for caching the config, we
195
# just add the part we're interested in to the params to avoid
196
# reading the config files repeatedly (bazaar.conf, location.conf,
198
config_key = self.name_prefix + '_merge_files'
199
affected_files = config.get_user_option_as_list(config_key)
200
if affected_files is None:
201
# If nothing was specified in the config, use the default.
202
affected_files = self.default_files
203
self.affected_files = affected_files
205
filepath = self.get_filepath(params, self.merger.this_tree)
206
if filepath in affected_files:
210
def merge_matching(self, params):
211
return self.merge_text(params)
213
def merge_text(self, params):
214
"""Merge the byte contents of a single file.
216
This is called after checking that the merge should be performed in
217
merge_contents, and it should behave as per
218
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
220
raise NotImplementedError(self.merge_text)
223
class MergeHookParams(object):
224
"""Object holding parameters passed to merge_file_content hooks.
226
There are some fields hooks can access:
228
:ivar file_id: the file ID of the file being merged
229
:ivar trans_id: the transform ID for the merge of this file
230
:ivar this_kind: kind of file_id in 'this' tree
231
:ivar other_kind: kind of file_id in 'other' tree
232
:ivar winner: one of 'this', 'other', 'conflict'
235
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
237
self._merger = merger
238
self.file_id = file_id
239
self.trans_id = trans_id
240
self.this_kind = this_kind
241
self.other_kind = other_kind
244
def is_file_merge(self):
245
"""True if this_kind and other_kind are both 'file'."""
246
return self.this_kind == 'file' and self.other_kind == 'file'
248
@decorators.cachedproperty
249
def base_lines(self):
250
"""The lines of the 'base' version of the file."""
251
return self._merger.get_lines(self._merger.base_tree, self.file_id)
253
@decorators.cachedproperty
254
def this_lines(self):
255
"""The lines of the 'this' version of the file."""
256
return self._merger.get_lines(self._merger.this_tree, self.file_id)
258
@decorators.cachedproperty
259
def other_lines(self):
260
"""The lines of the 'other' version of the file."""
261
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)
264
71
class Merger(object):
268
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
269
73
this_tree=None, pb=None, change_reporter=None,
270
74
recurse='down', revision_graph=None):
451
244
if self.other_rev_id is None:
452
245
other_basis_tree = self.revision_tree(self.other_basis)
453
if other_basis_tree.has_changes(self.other_tree):
454
raise errors.WorkingTreeNotRevision(self.this_tree)
246
changes = other_basis_tree.changes_from(self.other_tree)
247
if changes.has_changed():
248
raise WorkingTreeNotRevision(self.this_tree)
455
249
other_rev_id = self.other_basis
456
250
self.other_tree = other_basis_tree
458
@deprecated_method(deprecated_in((2, 1, 0)))
459
252
def file_revisions(self, file_id):
460
253
self.ensure_revision_trees()
254
def get_id(tree, file_id):
255
revision_id = tree.inventory[file_id].revision
461
257
if self.this_rev_id is None:
462
258
if self.this_basis_tree.get_file_sha1(file_id) != \
463
259
self.this_tree.get_file_sha1(file_id):
464
raise errors.WorkingTreeNotRevision(self.this_tree)
260
raise WorkingTreeNotRevision(self.this_tree)
466
262
trees = (self.this_basis_tree, self.other_tree)
467
return [tree.get_file_revision(file_id) for tree in trees]
263
return [get_id(tree, file_id) for tree in trees]
469
@deprecated_method(deprecated_in((2, 1, 0)))
470
265
def check_basis(self, check_clean, require_commits=True):
471
266
if self.this_basis is None and require_commits is True:
472
raise errors.BzrCommandError(
473
"This branch has no commits."
474
" (perhaps you would prefer 'bzr pull')")
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
476
270
self.compare_basis()
477
271
if self.this_basis != self.this_rev_id:
478
272
raise errors.UncommittedChanges(self.this_tree)
480
@deprecated_method(deprecated_in((2, 1, 0)))
481
274
def compare_basis(self):
483
276
basis_tree = self.revision_tree(self.this_tree.last_revision())
484
277
except errors.NoSuchRevision:
485
278
basis_tree = self.this_tree.basis_tree()
486
if not self.this_tree.has_changes(basis_tree):
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
487
281
self.this_rev_id = self.this_basis
489
283
def set_interesting_files(self, file_list):
490
284
self.interesting_files = file_list
492
286
def set_pending(self):
493
if (not self.base_is_ancestor or not self.base_is_other_ancestor
494
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:
496
289
self._add_parent()
498
291
def _add_parent(self):
499
292
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
500
293
new_parent_trees = []
501
operation = cleanup.OperationWithCleanups(
502
self.this_tree.set_parent_trees)
503
294
for revision_id in new_parents:
505
296
tree = self.revision_tree(revision_id)
592
387
self.base_rev_id = self.revision_graph.find_unique_lca(
594
sorted_lca_keys = self.revision_graph.find_merge_order(
596
if self.base_rev_id == _mod_revision.NULL_REVISION:
597
self.base_rev_id = sorted_lca_keys[0]
599
if self.base_rev_id == _mod_revision.NULL_REVISION:
600
raise errors.UnrelatedBranches()
389
self._is_criss_cross = True
390
if self.base_rev_id == NULL_REVISION:
391
raise UnrelatedBranches()
601
392
if self._is_criss_cross:
602
trace.warning('Warning: criss-cross merge encountered. See bzr'
603
' help criss-cross.')
604
trace.mutter('Criss-cross lcas: %r' % lcas)
605
if self.base_rev_id in lcas:
606
trace.mutter('Unable to find unique lca. '
607
'Fallback %r as best option.' % self.base_rev_id)
608
interesting_revision_ids = set(lcas)
609
interesting_revision_ids.add(self.base_rev_id)
393
warning('Warning: criss-cross merge encountered. See bzr'
394
' help criss-cross.')
395
mutter('Criss-cross lcas: %r' % lcas)
396
interesting_revision_ids = [self.base_rev_id]
397
interesting_revision_ids.extend(lcas)
610
398
interesting_trees = dict((t.get_revision_id(), t)
611
399
for t in self.this_branch.repository.revision_trees(
612
400
interesting_revision_ids))
613
401
self._cached_trees.update(interesting_trees)
614
if self.base_rev_id in lcas:
615
self.base_tree = interesting_trees[self.base_rev_id]
617
self.base_tree = interesting_trees.pop(self.base_rev_id)
402
self.base_tree = interesting_trees.pop(self.base_rev_id)
403
sorted_lca_keys = self.revision_graph.find_merge_order(
618
405
self._lca_trees = [interesting_trees[key]
619
406
for key in sorted_lca_keys]
621
408
self.base_tree = self.revision_tree(self.base_rev_id)
622
409
self.base_is_ancestor = True
623
410
self.base_is_other_ancestor = True
624
trace.mutter('Base revid: %r' % self.base_rev_id)
411
mutter('Base revid: %r' % self.base_rev_id)
626
413
def set_base(self, base_revision):
627
414
"""Set the base revision to use for the merge.
629
416
:param base_revision: A 2-list containing a path and revision number.
631
trace.mutter("doing merge() with no base_revision specified")
418
mutter("doing merge() with no base_revision specified")
632
419
if base_revision == [None, None]:
801
587
# making sure we haven't missed any corner cases.
802
588
# if lca_trees is None:
803
589
# self._lca_trees = [self.base_tree]
804
592
self.change_reporter = change_reporter
805
593
self.cherrypick = cherrypick
595
self.pp = ProgressPhase("Merge phase", 3, self.pb)
809
warnings.warn("pp argument to Merge3Merger is deprecated")
811
warnings.warn("pb argument to Merge3Merger is deprecated")
813
599
def do_merge(self):
814
operation = cleanup.OperationWithCleanups(self._do_merge)
815
600
self.this_tree.lock_tree_write()
816
operation.add_cleanup(self.this_tree.unlock)
817
601
self.base_tree.lock_read()
818
operation.add_cleanup(self.base_tree.unlock)
819
602
self.other_tree.lock_read()
820
operation.add_cleanup(self.other_tree.unlock)
823
def _do_merge(self, operation):
824
self.tt = transform.TreeTransform(self.this_tree, None)
825
operation.add_cleanup(self.tt.finalize)
826
self._compute_transform()
827
results = self.tt.apply(no_conflicts=True)
828
self.write_modified(results)
603
self.tt = TreeTransform(self.this_tree, self.pb)
830
self.this_tree.add_conflicts(self.cooked_conflicts)
831
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()
834
621
def make_preview_transform(self):
835
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
836
622
self.base_tree.lock_read()
837
operation.add_cleanup(self.base_tree.unlock)
838
623
self.other_tree.lock_read()
839
operation.add_cleanup(self.other_tree.unlock)
840
return operation.run_simple()
842
def _make_preview_transform(self):
843
self.tt = transform.TransformPreview(self.this_tree)
844
self._compute_transform()
624
self.tt = TransformPreview(self.this_tree)
627
self._compute_transform()
630
self.other_tree.unlock()
631
self.base_tree.unlock()
847
635
def _compute_transform(self):
1106
886
other_root = self.tt.trans_id_file_id(other_root_file_id)
1107
887
if other_root == self.tt.root:
1109
if self.this_tree.inventory.has_id(
1110
self.other_tree.inventory.root.file_id):
1111
# the other tree's root is a non-root in the current tree (as
1112
# when a previously unrelated branch is merged into another)
1114
if self.tt.final_kind(other_root) is not None:
1115
other_root_is_present = True
1117
# other_root doesn't have a physical representation. We still need
1118
# to move any references to the actual root of the tree.
1119
other_root_is_present = False
1120
# 'other_tree.inventory.root' is not present in this tree. We are
1121
# calling adjust_path for children which *want* to be present with a
1122
# correct place to go.
1123
for _, child in self.other_tree.inventory.root.children.iteritems():
890
self.tt.final_kind(other_root)
893
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
894
# the other tree's root is a non-root in the current tree
896
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
897
self.tt.cancel_creation(other_root)
898
self.tt.cancel_versioning(other_root)
900
def reparent_children(self, ie, target):
901
for thing, child in ie.children.iteritems():
1124
902
trans_id = self.tt.trans_id_file_id(child.file_id)
1125
if not other_root_is_present:
1126
if self.tt.final_kind(trans_id) is not None:
1127
# The item exist in the final tree and has a defined place
1130
# Move the item into the root
1132
final_name = self.tt.final_name(trans_id)
1133
except errors.NoFinalPath:
1134
# This file is not present anymore, ignore it.
1136
self.tt.adjust_path(final_name, self.tt.root, trans_id)
1137
if other_root_is_present:
1138
self.tt.cancel_creation(other_root)
1139
self.tt.cancel_versioning(other_root)
903
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1141
905
def write_modified(self, results):
1142
906
modified_hashes = {}
1310
1071
parent_id_winner = "other"
1311
1072
if name_winner == "this" and parent_id_winner == "this":
1313
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1314
# Creating helpers (.OTHER or .THIS) here cause problems down the
1315
# road if a ContentConflict needs to be created so we should not do
1317
trans_id = self.tt.trans_id_file_id(file_id)
1318
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1319
this_parent, this_name,
1320
other_parent, other_name))
1321
if not self.other_tree.has_id(file_id):
1074
if name_winner == "conflict":
1075
trans_id = self.tt.trans_id_file_id(file_id)
1076
self._raw_conflicts.append(('name conflict', trans_id,
1077
this_name, other_name))
1078
if parent_id_winner == "conflict":
1079
trans_id = self.tt.trans_id_file_id(file_id)
1080
self._raw_conflicts.append(('parent conflict', trans_id,
1081
this_parent, other_parent))
1082
if other_name is None:
1322
1083
# it doesn't matter whether the result was 'other' or
1323
# 'conflict'-- if it has no file id, we leave it alone.
1084
# 'conflict'-- if there's no 'other', we leave it alone.
1086
# if we get here, name_winner and parent_winner are set to safe values.
1087
trans_id = self.tt.trans_id_file_id(file_id)
1325
1088
parent_id = parents[self.winner_idx[parent_id_winner]]
1326
name = names[self.winner_idx[name_winner]]
1327
if parent_id is not None or name is not None:
1328
# if we get here, name_winner and parent_winner are set to safe
1330
if parent_id is None and name is not None:
1331
# if parent_id is None and name is non-None, current file is
1333
if names[self.winner_idx[parent_id_winner]] != '':
1334
raise AssertionError(
1335
'File looks like a root, but named %s' %
1336
names[self.winner_idx[parent_id_winner]])
1337
parent_trans_id = transform.ROOT_PARENT
1339
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1340
self.tt.adjust_path(name, parent_trans_id,
1341
self.tt.trans_id_file_id(file_id))
1089
if parent_id is not None:
1090
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1091
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1092
parent_trans_id, trans_id)
1343
def _do_merge_contents(self, file_id):
1094
def merge_contents(self, file_id):
1344
1095
"""Performs a merge on file_id contents."""
1345
1096
def contents_pair(tree):
1346
if not tree.has_id(file_id):
1097
if file_id not in tree:
1347
1098
return (None, None)
1348
1099
kind = tree.kind(file_id)
1349
1100
if kind == "file":
1375
1138
if winner == 'this':
1376
1139
# No interesting changes introduced by OTHER
1377
1140
return "unmodified"
1378
# We have a hypothetical conflict, but if we have files, then we
1379
# can try to merge the content
1380
1141
trans_id = self.tt.trans_id_file_id(file_id)
1381
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1382
other_pair[0], winner)
1383
hooks = self.active_hooks
1384
hook_status = 'not_applicable'
1386
hook_status, lines = hook.merge_contents(params)
1387
if hook_status != 'not_applicable':
1388
# Don't try any more hooks, this one applies.
1391
if hook_status == 'not_applicable':
1392
# This is a contents conflict, because none of the available
1393
# functions could merge it.
1395
name = self.tt.final_name(trans_id)
1396
parent_id = self.tt.final_parent(trans_id)
1397
if self.this_tree.has_id(file_id):
1398
self.tt.unversion_file(trans_id)
1399
file_group = self._dump_conflicts(name, parent_id, file_id,
1401
self._raw_conflicts.append(('contents conflict', file_group))
1402
elif hook_status == 'success':
1403
self.tt.create_file(lines, trans_id)
1404
elif hook_status == 'conflicted':
1405
# XXX: perhaps the hook should be able to provide
1406
# the BASE/THIS/OTHER files?
1407
self.tt.create_file(lines, trans_id)
1408
self._raw_conflicts.append(('text conflict', trans_id))
1409
name = self.tt.final_name(trans_id)
1410
parent_id = self.tt.final_parent(trans_id)
1411
self._dump_conflicts(name, parent_id, file_id)
1412
elif hook_status == 'delete':
1413
self.tt.unversion_file(trans_id)
1415
elif hook_status == 'done':
1416
# The hook function did whatever it needs to do directly, no
1417
# further action needed here.
1420
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1421
if not self.this_tree.has_id(file_id) and result == "modified":
1422
self.tt.version_file(file_id, trans_id)
1423
# The merge has been performed, so the old contents should not be
1425
self.tt.delete_contents(trans_id)
1428
def _default_other_winner_merge(self, merge_hook_params):
1429
"""Replace this contents with other."""
1430
file_id = merge_hook_params.file_id
1431
trans_id = merge_hook_params.trans_id
1432
file_in_this = self.this_tree.has_id(file_id)
1433
if self.other_tree.has_id(file_id):
1434
# OTHER changed the file
1436
if wt.supports_content_filtering():
1437
# We get the path from the working tree if it exists.
1438
# That fails though when OTHER is adding a file, so
1439
# we fall back to the other tree to find the path if
1440
# it doesn't exist locally.
1442
filter_tree_path = wt.id2path(file_id)
1443
except errors.NoSuchId:
1444
filter_tree_path = self.other_tree.id2path(file_id)
1446
# Skip the id2path lookup for older formats
1447
filter_tree_path = None
1448
transform.create_from_tree(self.tt, trans_id,
1449
self.other_tree, file_id,
1450
filter_tree_path=filter_tree_path)
1453
# OTHER deleted the file
1454
return 'delete', None
1456
raise AssertionError(
1457
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1460
def merge_contents(self, merge_hook_params):
1461
"""Fallback merge logic after user installed hooks."""
1462
# This function is used in merge hooks as the fallback instance.
1463
# Perhaps making this function and the functions it calls be a
1464
# a separate class would be better.
1465
if merge_hook_params.winner == 'other':
1142
if winner == 'other':
1466
1143
# OTHER is a straight winner, so replace this contents with other
1467
return self._default_other_winner_merge(merge_hook_params)
1468
elif merge_hook_params.is_file_merge():
1469
# THIS and OTHER are both files, so text merge. Either
1470
# BASE is a file, or both converted to files, so at least we
1471
# have agreement that output should be a file.
1473
self.text_merge(merge_hook_params.file_id,
1474
merge_hook_params.trans_id)
1475
except errors.BinaryFile:
1476
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)
1479
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()
1481
1181
def get_lines(self, tree, file_id):
1482
1182
"""Return the lines in a file, or an empty list."""
1483
if tree.has_id(file_id):
1484
return tree.get_file_lines(file_id)
1184
return tree.get_file(file_id).readlines()
1618
1306
def cook_conflicts(self, fs_conflicts):
1619
1307
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1620
content_conflict_file_ids = set()
1621
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1622
fp = transform.FinalPaths(self.tt)
1308
from conflicts import Conflict
1310
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1311
fp = FinalPaths(self.tt)
1623
1312
for conflict in self._raw_conflicts:
1624
1313
conflict_type = conflict[0]
1625
if conflict_type == 'path conflict':
1627
this_parent, this_name,
1628
other_parent, other_name) = conflict[1:]
1629
if this_parent is None or this_name is None:
1630
this_path = '<deleted>'
1632
parent_path = fp.get_path(
1633
self.tt.trans_id_file_id(this_parent))
1634
this_path = osutils.pathjoin(parent_path, this_name)
1635
if other_parent is None or other_name is None:
1636
other_path = '<deleted>'
1638
if other_parent == self.other_tree.get_root_id():
1639
# The tree transform doesn't know about the other root,
1640
# so we special case here to avoid a NoFinalPath
1644
parent_path = fp.get_path(
1645
self.tt.trans_id_file_id(other_parent))
1646
other_path = osutils.pathjoin(parent_path, other_name)
1647
c = _mod_conflicts.Conflict.factory(
1648
'path conflict', path=this_path,
1649
conflict_path=other_path,
1651
elif conflict_type == 'contents conflict':
1314
if conflict_type in ('name conflict', 'parent conflict'):
1315
trans_id = conflict[1]
1316
conflict_args = conflict[2:]
1317
if trans_id not in name_conflicts:
1318
name_conflicts[trans_id] = {}
1319
unique_add(name_conflicts[trans_id], conflict_type,
1321
if conflict_type == 'contents conflict':
1652
1322
for trans_id in conflict[1]:
1653
1323
file_id = self.tt.final_file_id(trans_id)
1654
1324
if file_id is not None:
1658
1328
if path.endswith(suffix):
1659
1329
path = path[:-len(suffix)]
1661
c = _mod_conflicts.Conflict.factory(conflict_type,
1662
path=path, file_id=file_id)
1663
content_conflict_file_ids.add(file_id)
1664
elif conflict_type == 'text conflict':
1331
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1332
self.cooked_conflicts.append(c)
1333
if conflict_type == 'text conflict':
1665
1334
trans_id = conflict[1]
1666
1335
path = fp.get_path(trans_id)
1667
1336
file_id = self.tt.final_file_id(trans_id)
1668
c = _mod_conflicts.Conflict.factory(conflict_type,
1669
path=path, file_id=file_id)
1337
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1338
self.cooked_conflicts.append(c)
1340
for trans_id, conflicts in name_conflicts.iteritems():
1342
this_parent, other_parent = conflicts['parent conflict']
1343
if this_parent == other_parent:
1344
raise AssertionError()
1346
this_parent = other_parent = \
1347
self.tt.final_file_id(self.tt.final_parent(trans_id))
1349
this_name, other_name = conflicts['name conflict']
1350
if this_name == other_name:
1351
raise AssertionError()
1353
this_name = other_name = self.tt.final_name(trans_id)
1354
other_path = fp.get_path(trans_id)
1355
if this_parent is not None and this_name is not None:
1356
this_parent_path = \
1357
fp.get_path(self.tt.trans_id_file_id(this_parent))
1358
this_path = pathjoin(this_parent_path, this_name)
1671
raise AssertionError('bad conflict type: %r' % (conflict,))
1672
cooked_conflicts.append(c)
1674
self.cooked_conflicts = []
1675
# We want to get rid of path conflicts when a corresponding contents
1676
# conflict exists. This can occur when one branch deletes a file while
1677
# the other renames *and* modifies it. In this case, the content
1678
# conflict is enough.
1679
for c in cooked_conflicts:
1680
if (c.typestring == 'path conflict'
1681
and c.file_id in content_conflict_file_ids):
1360
this_path = "<deleted>"
1361
file_id = self.tt.final_file_id(trans_id)
1362
c = Conflict.factory('path conflict', path=this_path,
1363
conflict_path=other_path, file_id=file_id)
1683
1364
self.cooked_conflicts.append(c)
1684
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1365
self.cooked_conflicts.sort(key=Conflict.sort_key)
1687
1368
class WeaveMerger(Merge3Merger):
1691
1372
supports_reverse_cherrypick = False
1692
1373
history_based = True
1694
def _generate_merge_plan(self, file_id, base):
1695
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,
1698
def _merged_lines(self, file_id):
1699
"""Generate the merged lines.
1700
There is no distinction between lines that are meant to contain <<<<<<<
1704
base = self.base_tree
1707
plan = self._generate_merge_plan(file_id, base)
1708
1386
if 'merge' in debug.debug_flags:
1709
1387
plan = list(plan)
1710
1388
trans_id = self.tt.trans_id_file_id(file_id)
1711
1389
name = self.tt.final_name(trans_id) + '.plan'
1712
contents = ('%11s|%s' % l for l in plan)
1390
contents = ('%10s|%s' % l for l in plan)
1713
1391
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1714
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1715
'>>>>>>> MERGE-SOURCE\n')
1716
lines, conflicts = textmerge.merge_lines(self.reprocess)
1718
base_lines = textmerge.base_from_plan()
1721
return lines, base_lines
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1394
return textmerge.merge_lines(self.reprocess)
1723
1396
def text_merge(self, file_id, trans_id):
1724
1397
"""Perform a (weave) text merge for a given file and file-id.
1725
1398
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1726
1399
and a conflict will be noted.
1728
lines, base_lines = self._merged_lines(file_id)
1401
lines, conflicts = self._merged_lines(file_id)
1729
1402
lines = list(lines)
1730
1403
# Note we're checking whether the OUTPUT is binary in this case,
1731
1404
# because we don't want to get into weave merge guts.
1732
textfile.check_text_lines(lines)
1405
check_text_lines(lines)
1733
1406
self.tt.create_file(lines, trans_id)
1734
if base_lines is not None:
1736
1408
self._raw_conflicts.append(('text conflict', trans_id))
1737
1409
name = self.tt.final_name(trans_id)
1738
1410
parent_id = self.tt.final_parent(trans_id)
1739
1411
file_group = self._dump_conflicts(name, parent_id, file_id,
1741
base_lines=base_lines)
1742
1413
file_group.append(trans_id)
1745
1416
class LCAMerger(WeaveMerger):
1747
def _generate_merge_plan(self, file_id, base):
1748
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)
1751
1440
class Diff3Merger(Merge3Merger):
1752
1441
"""Three-way merger using external diff3 for text merging"""
1754
1443
def dump_file(self, temp_dir, name, tree, file_id):
1755
out_path = osutils.pathjoin(temp_dir, name)
1444
out_path = pathjoin(temp_dir, name)
1756
1445
out_file = open(out_path, "wb")
1758
1447
in_file = tree.get_file(file_id)
1791
1480
osutils.rmtree(temp_dir)
1794
class PathNotInTree(errors.BzrError):
1796
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1798
def __init__(self, path, tree):
1799
errors.BzrError.__init__(self, path=path, tree=tree)
1802
class MergeIntoMerger(Merger):
1803
"""Merger that understands other_tree will be merged into a subdir.
1805
This also changes the Merger api so that it uses real Branch, revision_id,
1806
and RevisonTree objects, rather than using revision specs.
1809
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1810
source_subpath, other_rev_id=None):
1811
"""Create a new MergeIntoMerger object.
1813
source_subpath in other_tree will be effectively copied to
1814
target_subdir in this_tree.
1816
:param this_tree: The tree that we will be merging into.
1817
:param other_branch: The Branch we will be merging from.
1818
:param other_tree: The RevisionTree object we want to merge.
1819
:param target_subdir: The relative path where we want to merge
1820
other_tree into this_tree
1821
:param source_subpath: The relative path specifying the subtree of
1822
other_tree to merge into this_tree.
1824
# It is assumed that we are merging a tree that is not in our current
1825
# ancestry, which means we are using the "EmptyTree" as our basis.
1826
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1827
_mod_revision.NULL_REVISION)
1828
super(MergeIntoMerger, self).__init__(
1829
this_branch=this_tree.branch,
1830
this_tree=this_tree,
1831
other_tree=other_tree,
1832
base_tree=null_ancestor_tree,
1834
self._target_subdir = target_subdir
1835
self._source_subpath = source_subpath
1836
self.other_branch = other_branch
1837
if other_rev_id is None:
1838
other_rev_id = other_tree.get_revision_id()
1839
self.other_rev_id = self.other_basis = other_rev_id
1840
self.base_is_ancestor = True
1841
self.backup_files = True
1842
self.merge_type = Merge3Merger
1843
self.show_base = False
1844
self.reprocess = False
1845
self.interesting_ids = None
1846
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1847
target_subdir=self._target_subdir,
1848
source_subpath=self._source_subpath)
1849
if self._source_subpath != '':
1850
# If this isn't a partial merge make sure the revisions will be
1852
self._maybe_fetch(self.other_branch, self.this_branch,
1855
def set_pending(self):
1856
if self._source_subpath != '':
1858
Merger.set_pending(self)
1861
class _MergeTypeParameterizer(object):
1862
"""Wrap a merge-type class to provide extra parameters.
1864
This is hack used by MergeIntoMerger to pass some extra parameters to its
1865
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1866
the 'merge_type' member. It is difficult override do_merge without
1867
re-writing the whole thing, so instead we create a wrapper which will pass
1868
the extra parameters.
1871
def __init__(self, merge_type, **kwargs):
1872
self._extra_kwargs = kwargs
1873
self._merge_type = merge_type
1875
def __call__(self, *args, **kwargs):
1876
kwargs.update(self._extra_kwargs)
1877
return self._merge_type(*args, **kwargs)
1879
def __getattr__(self, name):
1880
return getattr(self._merge_type, name)
1883
class MergeIntoMergeType(Merge3Merger):
1884
"""Merger that incorporates a tree (or part of a tree) into another."""
1886
def __init__(self, *args, **kwargs):
1887
"""Initialize the merger object.
1889
:param args: See Merge3Merger.__init__'s args.
1890
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1891
source_subpath and target_subdir.
1892
:keyword source_subpath: The relative path specifying the subtree of
1893
other_tree to merge into this_tree.
1894
:keyword target_subdir: The relative path where we want to merge
1895
other_tree into this_tree
1897
# All of the interesting work happens during Merge3Merger.__init__(),
1898
# so we have have to hack in to get our extra parameters set.
1899
self._source_subpath = kwargs.pop('source_subpath')
1900
self._target_subdir = kwargs.pop('target_subdir')
1901
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1903
def _compute_transform(self):
1904
child_pb = ui.ui_factory.nested_progress_bar()
1906
entries = self._entries_to_incorporate()
1907
entries = list(entries)
1908
for num, (entry, parent_id) in enumerate(entries):
1909
child_pb.update('Preparing file merge', num, len(entries))
1910
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1911
trans_id = transform.new_by_entry(self.tt, entry,
1912
parent_trans_id, self.other_tree)
1915
self._finish_computing_transform()
1917
def _entries_to_incorporate(self):
1918
"""Yields pairs of (inventory_entry, new_parent)."""
1919
other_inv = self.other_tree.inventory
1920
subdir_id = other_inv.path2id(self._source_subpath)
1921
if subdir_id is None:
1922
# XXX: The error would be clearer if it gave the URL of the source
1923
# branch, but we don't have a reference to that here.
1924
raise PathNotInTree(self._source_subpath, "Source tree")
1925
subdir = other_inv[subdir_id]
1926
parent_in_target = osutils.dirname(self._target_subdir)
1927
target_id = self.this_tree.inventory.path2id(parent_in_target)
1928
if target_id is None:
1929
raise PathNotInTree(self._target_subdir, "Target tree")
1930
name_in_target = osutils.basename(self._target_subdir)
1931
merge_into_root = subdir.copy()
1932
merge_into_root.name = name_in_target
1933
if self.this_tree.inventory.has_id(merge_into_root.file_id):
1934
# Give the root a new file-id.
1935
# This can happen fairly easily if the directory we are
1936
# incorporating is the root, and both trees have 'TREE_ROOT' as
1937
# their root_id. Users will expect this to Just Work, so we
1938
# change the file-id here.
1939
# Non-root file-ids could potentially conflict too. That's really
1940
# an edge case, so we don't do anything special for those. We let
1941
# them cause conflicts.
1942
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1943
yield (merge_into_root, target_id)
1944
if subdir.kind != 'directory':
1945
# No children, so we are done.
1947
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1948
parent_id = entry.parent_id
1949
if parent_id == subdir.file_id:
1950
# The root's parent ID has changed, so make sure children of
1951
# the root refer to the new ID.
1952
parent_id = merge_into_root.file_id
1953
yield (entry, parent_id)
1956
1483
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1957
1484
backup_files=False,
1958
1485
merge_type=Merge3Merger,