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(), """
23
21
from bzrlib import (
22
branch as _mod_branch,
24
conflicts as _mod_conflicts,
26
27
graph as _mod_graph,
30
31
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
47
from bzrlib.symbol_versioning import (
63
51
# TODO: Report back as changes are merged in
66
54
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)
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)
71
264
class Merger(object):
72
268
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
269
this_tree=None, pb=None, change_reporter=None,
74
270
recurse='down', revision_graph=None):
244
451
if self.other_rev_id is None:
245
452
other_basis_tree = self.revision_tree(self.other_basis)
246
453
if other_basis_tree.has_changes(self.other_tree):
247
raise WorkingTreeNotRevision(self.this_tree)
454
raise errors.WorkingTreeNotRevision(self.this_tree)
248
455
other_rev_id = self.other_basis
249
456
self.other_tree = other_basis_tree
458
@deprecated_method(deprecated_in((2, 1, 0)))
251
459
def file_revisions(self, file_id):
252
460
self.ensure_revision_trees()
253
def get_id(tree, file_id):
254
revision_id = tree.inventory[file_id].revision
256
461
if self.this_rev_id is None:
257
462
if self.this_basis_tree.get_file_sha1(file_id) != \
258
463
self.this_tree.get_file_sha1(file_id):
259
raise WorkingTreeNotRevision(self.this_tree)
464
raise errors.WorkingTreeNotRevision(self.this_tree)
261
466
trees = (self.this_basis_tree, self.other_tree)
262
return [get_id(tree, file_id) for tree in trees]
467
return [tree.get_file_revision(file_id) for tree in trees]
469
@deprecated_method(deprecated_in((2, 1, 0)))
264
470
def check_basis(self, check_clean, require_commits=True):
265
471
if self.this_basis is None and require_commits is True:
266
raise BzrCommandError("This branch has no commits."
267
" (perhaps you would prefer 'bzr pull')")
472
raise errors.BzrCommandError(
473
"This branch has no commits."
474
" (perhaps you would prefer 'bzr pull')")
269
476
self.compare_basis()
270
477
if self.this_basis != self.this_rev_id:
271
478
raise errors.UncommittedChanges(self.this_tree)
480
@deprecated_method(deprecated_in((2, 1, 0)))
273
481
def compare_basis(self):
275
483
basis_tree = self.revision_tree(self.this_tree.last_revision())
385
592
self.base_rev_id = self.revision_graph.find_unique_lca(
387
self._is_criss_cross = True
388
if self.base_rev_id == NULL_REVISION:
389
raise UnrelatedBranches()
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()
390
601
if self._is_criss_cross:
391
warning('Warning: criss-cross merge encountered. See bzr'
392
' help criss-cross.')
393
mutter('Criss-cross lcas: %r' % lcas)
394
interesting_revision_ids = [self.base_rev_id]
395
interesting_revision_ids.extend(lcas)
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)
396
610
interesting_trees = dict((t.get_revision_id(), t)
397
611
for t in self.this_branch.repository.revision_trees(
398
612
interesting_revision_ids))
399
613
self._cached_trees.update(interesting_trees)
400
self.base_tree = interesting_trees.pop(self.base_rev_id)
401
sorted_lca_keys = self.revision_graph.find_merge_order(
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)
403
618
self._lca_trees = [interesting_trees[key]
404
619
for key in sorted_lca_keys]
406
621
self.base_tree = self.revision_tree(self.base_rev_id)
407
622
self.base_is_ancestor = True
408
623
self.base_is_other_ancestor = True
409
mutter('Base revid: %r' % self.base_rev_id)
624
trace.mutter('Base revid: %r' % self.base_rev_id)
411
626
def set_base(self, base_revision):
412
627
"""Set the base revision to use for the merge.
414
629
:param base_revision: A 2-list containing a path and revision number.
416
mutter("doing merge() with no base_revision specified")
631
trace.mutter("doing merge() with no base_revision specified")
417
632
if base_revision == [None, None]:
585
801
# making sure we haven't missed any corner cases.
586
802
# if lca_trees is None:
587
803
# self._lca_trees = [self.base_tree]
590
804
self.change_reporter = change_reporter
591
805
self.cherrypick = cherrypick
593
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")
597
813
def do_merge(self):
814
operation = cleanup.OperationWithCleanups(self._do_merge)
598
815
self.this_tree.lock_tree_write()
816
operation.add_cleanup(self.this_tree.unlock)
599
817
self.base_tree.lock_read()
818
operation.add_cleanup(self.base_tree.unlock)
600
819
self.other_tree.lock_read()
601
self.tt = TreeTransform(self.this_tree, self.pb)
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)
604
self._compute_transform()
606
results = self.tt.apply(no_conflicts=True)
607
self.write_modified(results)
609
self.this_tree.add_conflicts(self.cooked_conflicts)
610
except UnsupportedOperation:
614
self.other_tree.unlock()
615
self.base_tree.unlock()
616
self.this_tree.unlock()
830
self.this_tree.add_conflicts(self.cooked_conflicts)
831
except errors.UnsupportedOperation:
619
834
def make_preview_transform(self):
835
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
620
836
self.base_tree.lock_read()
837
operation.add_cleanup(self.base_tree.unlock)
621
838
self.other_tree.lock_read()
622
self.tt = TransformPreview(self.this_tree)
625
self._compute_transform()
628
self.other_tree.unlock()
629
self.base_tree.unlock()
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()
633
847
def _compute_transform(self):
884
1103
other_root = self.tt.trans_id_file_id(other_root_file_id)
885
1104
if other_root == self.tt.root:
888
self.tt.final_kind(other_root)
891
1106
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
892
# the other tree's root is a non-root in the current tree
1107
# the other tree's root is a non-root in the current tree (as when
1108
# a previously unrelated branch is merged into another)
894
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
895
self.tt.cancel_creation(other_root)
896
self.tt.cancel_versioning(other_root)
898
def reparent_children(self, ie, target):
899
for thing, child in ie.children.iteritems():
1110
if self.tt.final_kind(other_root) is not None:
1111
other_root_is_present = True
1113
# other_root doesn't have a physical representation. We still need
1114
# to move any references to the actual root of the tree.
1115
other_root_is_present = False
1116
# 'other_tree.inventory.root' is not present in this tree. We are
1117
# calling adjust_path for children which *want* to be present with a
1118
# correct place to go.
1119
for _, child in self.other_tree.inventory.root.children.iteritems():
900
1120
trans_id = self.tt.trans_id_file_id(child.file_id)
901
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1121
if not other_root_is_present:
1122
if self.tt.final_kind(trans_id) is not None:
1123
# The item exist in the final tree and has a defined place
1126
# Move the item into the root
1128
final_name = self.tt.final_name(trans_id)
1129
except errors.NoFinalPath:
1130
# This file is not present anymore, ignore it.
1132
self.tt.adjust_path(final_name, self.tt.root, trans_id)
1133
if other_root_is_present:
1134
self.tt.cancel_creation(other_root)
1135
self.tt.cancel_versioning(other_root)
903
1137
def write_modified(self, results):
904
1138
modified_hashes = {}
1069
1306
parent_id_winner = "other"
1070
1307
if name_winner == "this" and parent_id_winner == "this":
1072
if name_winner == "conflict":
1073
trans_id = self.tt.trans_id_file_id(file_id)
1074
self._raw_conflicts.append(('name conflict', trans_id,
1075
this_name, other_name))
1076
if parent_id_winner == "conflict":
1077
trans_id = self.tt.trans_id_file_id(file_id)
1078
self._raw_conflicts.append(('parent conflict', trans_id,
1079
this_parent, other_parent))
1309
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1310
# Creating helpers (.OTHER or .THIS) here cause problems down the
1311
# road if a ContentConflict needs to be created so we should not do
1313
trans_id = self.tt.trans_id_file_id(file_id)
1314
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1315
this_parent, this_name,
1316
other_parent, other_name))
1080
1317
if other_name is None:
1081
1318
# it doesn't matter whether the result was 'other' or
1082
1319
# 'conflict'-- if there's no 'other', we leave it alone.
1084
# if we get here, name_winner and parent_winner are set to safe values.
1085
trans_id = self.tt.trans_id_file_id(file_id)
1086
1321
parent_id = parents[self.winner_idx[parent_id_winner]]
1087
1322
if parent_id is not None:
1088
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1323
# if we get here, name_winner and parent_winner are set to safe
1089
1325
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1090
parent_trans_id, trans_id)
1326
self.tt.trans_id_file_id(parent_id),
1327
self.tt.trans_id_file_id(file_id))
1092
def merge_contents(self, file_id):
1329
def _do_merge_contents(self, file_id):
1093
1330
"""Performs a merge on file_id contents."""
1094
1331
def contents_pair(tree):
1095
1332
if file_id not in tree:
1136
1361
if winner == 'this':
1137
1362
# No interesting changes introduced by OTHER
1138
1363
return "unmodified"
1364
# We have a hypothetical conflict, but if we have files, then we
1365
# can try to merge the content
1139
1366
trans_id = self.tt.trans_id_file_id(file_id)
1140
if winner == 'other':
1367
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1368
other_pair[0], winner)
1369
hooks = self.active_hooks
1370
hook_status = 'not_applicable'
1372
hook_status, lines = hook.merge_contents(params)
1373
if hook_status != 'not_applicable':
1374
# Don't try any more hooks, this one applies.
1377
if hook_status == 'not_applicable':
1378
# This is a contents conflict, because none of the available
1379
# functions could merge it.
1381
name = self.tt.final_name(trans_id)
1382
parent_id = self.tt.final_parent(trans_id)
1383
if self.this_tree.has_id(file_id):
1384
self.tt.unversion_file(trans_id)
1385
file_group = self._dump_conflicts(name, parent_id, file_id,
1387
self._raw_conflicts.append(('contents conflict', file_group))
1388
elif hook_status == 'success':
1389
self.tt.create_file(lines, trans_id)
1390
elif hook_status == 'conflicted':
1391
# XXX: perhaps the hook should be able to provide
1392
# the BASE/THIS/OTHER files?
1393
self.tt.create_file(lines, trans_id)
1394
self._raw_conflicts.append(('text conflict', trans_id))
1395
name = self.tt.final_name(trans_id)
1396
parent_id = self.tt.final_parent(trans_id)
1397
self._dump_conflicts(name, parent_id, file_id)
1398
elif hook_status == 'delete':
1399
self.tt.unversion_file(trans_id)
1401
elif hook_status == 'done':
1402
# The hook function did whatever it needs to do directly, no
1403
# further action needed here.
1406
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1407
if not self.this_tree.has_id(file_id) and result == "modified":
1408
self.tt.version_file(file_id, trans_id)
1409
# The merge has been performed, so the old contents should not be
1411
self.tt.delete_contents(trans_id)
1414
def _default_other_winner_merge(self, merge_hook_params):
1415
"""Replace this contents with other."""
1416
file_id = merge_hook_params.file_id
1417
trans_id = merge_hook_params.trans_id
1418
file_in_this = self.this_tree.has_id(file_id)
1419
if self.other_tree.has_id(file_id):
1420
# OTHER changed the file
1422
if wt.supports_content_filtering():
1423
# We get the path from the working tree if it exists.
1424
# That fails though when OTHER is adding a file, so
1425
# we fall back to the other tree to find the path if
1426
# it doesn't exist locally.
1428
filter_tree_path = wt.id2path(file_id)
1429
except errors.NoSuchId:
1430
filter_tree_path = self.other_tree.id2path(file_id)
1432
# Skip the id2path lookup for older formats
1433
filter_tree_path = None
1434
transform.create_from_tree(self.tt, trans_id,
1435
self.other_tree, file_id,
1436
filter_tree_path=filter_tree_path)
1439
# OTHER deleted the file
1440
return 'delete', None
1442
raise AssertionError(
1443
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1446
def merge_contents(self, merge_hook_params):
1447
"""Fallback merge logic after user installed hooks."""
1448
# This function is used in merge hooks as the fallback instance.
1449
# Perhaps making this function and the functions it calls be a
1450
# a separate class would be better.
1451
if merge_hook_params.winner == 'other':
1141
1452
# 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)
1453
return self._default_other_winner_merge(merge_hook_params)
1454
elif merge_hook_params.is_file_merge():
1455
# THIS and OTHER are both files, so text merge. Either
1456
# BASE is a file, or both converted to files, so at least we
1457
# have agreement that output should be a file.
1459
self.text_merge(merge_hook_params.file_id,
1460
merge_hook_params.trans_id)
1461
except errors.BinaryFile:
1462
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()
1465
return 'not_applicable', None
1179
1467
def get_lines(self, tree, file_id):
1180
1468
"""Return the lines in a file, or an empty list."""
1182
return tree.get_file(file_id).readlines()
1469
if tree.has_id(file_id):
1470
return tree.get_file_lines(file_id)
1326
1638
if path.endswith(suffix):
1327
1639
path = path[:-len(suffix)]
1329
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1330
self.cooked_conflicts.append(c)
1331
if conflict_type == 'text conflict':
1641
c = _mod_conflicts.Conflict.factory(conflict_type,
1642
path=path, file_id=file_id)
1643
elif conflict_type == 'text conflict':
1332
1644
trans_id = conflict[1]
1333
1645
path = fp.get_path(trans_id)
1334
1646
file_id = self.tt.final_file_id(trans_id)
1335
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1336
self.cooked_conflicts.append(c)
1338
for trans_id, conflicts in name_conflicts.iteritems():
1340
this_parent, other_parent = conflicts['parent conflict']
1341
if this_parent == other_parent:
1342
raise AssertionError()
1344
this_parent = other_parent = \
1345
self.tt.final_file_id(self.tt.final_parent(trans_id))
1347
this_name, other_name = conflicts['name conflict']
1348
if this_name == other_name:
1349
raise AssertionError()
1351
this_name = other_name = self.tt.final_name(trans_id)
1352
other_path = fp.get_path(trans_id)
1353
if this_parent is not None and this_name is not None:
1354
this_parent_path = \
1355
fp.get_path(self.tt.trans_id_file_id(this_parent))
1356
this_path = pathjoin(this_parent_path, this_name)
1647
c = _mod_conflicts.Conflict.factory(conflict_type,
1648
path=path, file_id=file_id)
1358
this_path = "<deleted>"
1359
file_id = self.tt.final_file_id(trans_id)
1360
c = Conflict.factory('path conflict', path=this_path,
1361
conflict_path=other_path, file_id=file_id)
1650
raise AssertionError('bad conflict type: %r' % (conflict,))
1362
1651
self.cooked_conflicts.append(c)
1363
self.cooked_conflicts.sort(key=Conflict.sort_key)
1652
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1366
1655
class WeaveMerger(Merge3Merger):
1370
1659
supports_reverse_cherrypick = False
1371
1660
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,
1662
def _generate_merge_plan(self, file_id, base):
1663
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1666
def _merged_lines(self, file_id):
1667
"""Generate the merged lines.
1668
There is no distinction between lines that are meant to contain <<<<<<<
1672
base = self.base_tree
1675
plan = self._generate_merge_plan(file_id, base)
1384
1676
if 'merge' in debug.debug_flags:
1385
1677
plan = list(plan)
1386
1678
trans_id = self.tt.trans_id_file_id(file_id)
1387
1679
name = self.tt.final_name(trans_id) + '.plan'
1388
contents = ('%10s|%s' % l for l in plan)
1680
contents = ('%11s|%s' % l for l in plan)
1389
1681
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)
1682
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1683
'>>>>>>> MERGE-SOURCE\n')
1684
lines, conflicts = textmerge.merge_lines(self.reprocess)
1686
base_lines = textmerge.base_from_plan()
1689
return lines, base_lines
1394
1691
def text_merge(self, file_id, trans_id):
1395
1692
"""Perform a (weave) text merge for a given file and file-id.
1396
1693
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1397
1694
and a conflict will be noted.
1399
lines, conflicts = self._merged_lines(file_id)
1696
lines, base_lines = self._merged_lines(file_id)
1400
1697
lines = list(lines)
1401
1698
# Note we're checking whether the OUTPUT is binary in this case,
1402
1699
# because we don't want to get into weave merge guts.
1403
check_text_lines(lines)
1700
textfile.check_text_lines(lines)
1404
1701
self.tt.create_file(lines, trans_id)
1702
if base_lines is not None:
1406
1704
self._raw_conflicts.append(('text conflict', trans_id))
1407
1705
name = self.tt.final_name(trans_id)
1408
1706
parent_id = self.tt.final_parent(trans_id)
1409
1707
file_group = self._dump_conflicts(name, parent_id, file_id,
1709
base_lines=base_lines)
1411
1710
file_group.append(trans_id)
1414
1713
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,
1715
def _generate_merge_plan(self, file_id, base):
1716
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
1719
class Diff3Merger(Merge3Merger):
1439
1720
"""Three-way merger using external diff3 for text merging"""
1441
1722
def dump_file(self, temp_dir, name, tree, file_id):
1442
out_path = pathjoin(temp_dir, name)
1723
out_path = osutils.pathjoin(temp_dir, name)
1443
1724
out_file = open(out_path, "wb")
1445
1726
in_file = tree.get_file(file_id)
1478
1759
osutils.rmtree(temp_dir)
1762
class PathNotInTree(errors.BzrError):
1764
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1766
def __init__(self, path, tree):
1767
errors.BzrError.__init__(self, path=path, tree=tree)
1770
class MergeIntoMerger(Merger):
1771
"""Merger that understands other_tree will be merged into a subdir.
1773
This also changes the Merger api so that it uses real Branch, revision_id,
1774
and RevisonTree objects, rather than using revision specs.
1777
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1778
source_subpath, other_rev_id=None):
1779
"""Create a new MergeIntoMerger object.
1781
source_subpath in other_tree will be effectively copied to
1782
target_subdir in this_tree.
1784
:param this_tree: The tree that we will be merging into.
1785
:param other_branch: The Branch we will be merging from.
1786
:param other_tree: The RevisionTree object we want to merge.
1787
:param target_subdir: The relative path where we want to merge
1788
other_tree into this_tree
1789
:param source_subpath: The relative path specifying the subtree of
1790
other_tree to merge into this_tree.
1792
# It is assumed that we are merging a tree that is not in our current
1793
# ancestry, which means we are using the "EmptyTree" as our basis.
1794
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1795
_mod_revision.NULL_REVISION)
1796
super(MergeIntoMerger, self).__init__(
1797
this_branch=this_tree.branch,
1798
this_tree=this_tree,
1799
other_tree=other_tree,
1800
base_tree=null_ancestor_tree,
1802
self._target_subdir = target_subdir
1803
self._source_subpath = source_subpath
1804
self.other_branch = other_branch
1805
if other_rev_id is None:
1806
other_rev_id = other_tree.get_revision_id()
1807
self.other_rev_id = self.other_basis = other_rev_id
1808
self.base_is_ancestor = True
1809
self.backup_files = True
1810
self.merge_type = Merge3Merger
1811
self.show_base = False
1812
self.reprocess = False
1813
self.interesting_ids = None
1814
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1815
target_subdir=self._target_subdir,
1816
source_subpath=self._source_subpath)
1817
if self._source_subpath != '':
1818
# If this isn't a partial merge make sure the revisions will be
1820
self._maybe_fetch(self.other_branch, self.this_branch,
1823
def set_pending(self):
1824
if self._source_subpath != '':
1826
Merger.set_pending(self)
1829
class _MergeTypeParameterizer(object):
1830
"""Wrap a merge-type class to provide extra parameters.
1832
This is hack used by MergeIntoMerger to pass some extra parameters to its
1833
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1834
the 'merge_type' member. It is difficult override do_merge without
1835
re-writing the whole thing, so instead we create a wrapper which will pass
1836
the extra parameters.
1839
def __init__(self, merge_type, **kwargs):
1840
self._extra_kwargs = kwargs
1841
self._merge_type = merge_type
1843
def __call__(self, *args, **kwargs):
1844
kwargs.update(self._extra_kwargs)
1845
return self._merge_type(*args, **kwargs)
1847
def __getattr__(self, name):
1848
return getattr(self._merge_type, name)
1851
class MergeIntoMergeType(Merge3Merger):
1852
"""Merger that incorporates a tree (or part of a tree) into another."""
1854
def __init__(self, *args, **kwargs):
1855
"""Initialize the merger object.
1857
:param args: See Merge3Merger.__init__'s args.
1858
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1859
source_subpath and target_subdir.
1860
:keyword source_subpath: The relative path specifying the subtree of
1861
other_tree to merge into this_tree.
1862
:keyword target_subdir: The relative path where we want to merge
1863
other_tree into this_tree
1865
# All of the interesting work happens during Merge3Merger.__init__(),
1866
# so we have have to hack in to get our extra parameters set.
1867
self._source_subpath = kwargs.pop('source_subpath')
1868
self._target_subdir = kwargs.pop('target_subdir')
1869
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1871
def _compute_transform(self):
1872
child_pb = ui.ui_factory.nested_progress_bar()
1874
entries = self._entries_to_incorporate()
1875
entries = list(entries)
1876
for num, (entry, parent_id) in enumerate(entries):
1877
child_pb.update('Preparing file merge', num, len(entries))
1878
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1879
trans_id = transform.new_by_entry(self.tt, entry,
1880
parent_trans_id, self.other_tree)
1883
self._finish_computing_transform()
1885
def _entries_to_incorporate(self):
1886
"""Yields pairs of (inventory_entry, new_parent)."""
1887
other_inv = self.other_tree.inventory
1888
subdir_id = other_inv.path2id(self._source_subpath)
1889
if subdir_id is None:
1890
# XXX: The error would be clearer if it gave the URL of the source
1891
# branch, but we don't have a reference to that here.
1892
raise PathNotInTree(self._source_subpath, "Source tree")
1893
subdir = other_inv[subdir_id]
1894
parent_in_target = osutils.dirname(self._target_subdir)
1895
target_id = self.this_tree.inventory.path2id(parent_in_target)
1896
if target_id is None:
1897
raise PathNotInTree(self._target_subdir, "Target tree")
1898
name_in_target = osutils.basename(self._target_subdir)
1899
merge_into_root = subdir.copy()
1900
merge_into_root.name = name_in_target
1901
if merge_into_root.file_id in self.this_tree.inventory:
1902
# Give the root a new file-id.
1903
# This can happen fairly easily if the directory we are
1904
# incorporating is the root, and both trees have 'TREE_ROOT' as
1905
# their root_id. Users will expect this to Just Work, so we
1906
# change the file-id here.
1907
# Non-root file-ids could potentially conflict too. That's really
1908
# an edge case, so we don't do anything special for those. We let
1909
# them cause conflicts.
1910
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1911
yield (merge_into_root, target_id)
1912
if subdir.kind != 'directory':
1913
# No children, so we are done.
1915
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1916
parent_id = entry.parent_id
1917
if parent_id == subdir.file_id:
1918
# The root's parent ID has changed, so make sure children of
1919
# the root refer to the new ID.
1920
parent_id = merge_into_root.file_id
1921
yield (entry, parent_id)
1481
1924
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1482
1925
backup_files=False,
1483
1926
merge_type=Merge3Merger,