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,
23
conflicts as _mod_conflicts,
26
26
graph as _mod_graph,
30
30
revision as _mod_revision,
40
from bzrlib.cleanup import OperationWithCleanups
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 = 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)
66
self.create_hook(hooks.HookPoint('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):
794
585
# making sure we haven't missed any corner cases.
795
586
# if lca_trees is None:
796
587
# self._lca_trees = [self.base_tree]
797
590
self.change_reporter = change_reporter
798
591
self.cherrypick = cherrypick
593
self.pp = ProgressPhase("Merge phase", 3, self.pb)
802
warnings.warn("pp argument to Merge3Merger is deprecated")
804
warnings.warn("pb argument to Merge3Merger is deprecated")
806
597
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
808
598
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
810
599
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
812
600
self.other_tree.lock_read()
813
operation.add_cleanup(self.other_tree.unlock)
816
def _do_merge(self, operation):
817
self.tt = transform.TreeTransform(self.this_tree, None)
818
operation.add_cleanup(self.tt.finalize)
819
self._compute_transform()
820
results = self.tt.apply(no_conflicts=True)
821
self.write_modified(results)
601
self.tt = TreeTransform(self.this_tree, self.pb)
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
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()
827
619
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
829
620
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
831
621
self.other_tree.lock_read()
832
operation.add_cleanup(self.other_tree.unlock)
833
return operation.run_simple()
835
def _make_preview_transform(self):
836
self.tt = transform.TransformPreview(self.this_tree)
837
self._compute_transform()
622
self.tt = TransformPreview(self.this_tree)
625
self._compute_transform()
628
self.other_tree.unlock()
629
self.base_tree.unlock()
840
633
def _compute_transform(self):
1098
884
other_root = self.tt.trans_id_file_id(other_root_file_id)
1099
885
if other_root == self.tt.root:
888
self.tt.final_kind(other_root)
1101
891
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1102
# the other tree's root is a non-root in the current tree (as when
1103
# a previously unrelated branch is merged into another)
892
# the other tree's root is a non-root in the current tree
1106
self.tt.final_kind(other_root)
1107
other_root_is_present = True
1108
except errors.NoSuchFile:
1109
# other_root doesn't have a physical representation. We still need
1110
# to move any references to the actual root of the tree.
1111
other_root_is_present = False
1112
# 'other_tree.inventory.root' is not present in this tree. We are
1113
# calling adjust_path for children which *want* to be present with a
1114
# correct place to go.
1115
for thing, child in self.other_tree.inventory.root.children.iteritems():
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():
1116
900
trans_id = self.tt.trans_id_file_id(child.file_id)
1117
if not other_root_is_present:
1118
# FIXME: Make final_kind returns None instead of raising
1119
# NoSuchFile to avoid the ugly construct below -- vila 20100402
1121
self.tt.final_kind(trans_id)
1122
# The item exist in the final tree and has a defined place
1125
except errors.NoSuchFile, e:
1127
# Move the item into the root
1128
self.tt.adjust_path(self.tt.final_name(trans_id),
1129
self.tt.root, trans_id)
1130
if other_root_is_present:
1131
self.tt.cancel_creation(other_root)
1132
self.tt.cancel_versioning(other_root)
901
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1134
903
def write_modified(self, results):
1135
904
modified_hashes = {}
1303
1069
parent_id_winner = "other"
1304
1070
if name_winner == "this" and parent_id_winner == "this":
1306
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1307
# Creating helpers (.OTHER or .THIS) here cause problems down the
1308
# road if a ContentConflict needs to be created so we should not do
1310
trans_id = self.tt.trans_id_file_id(file_id)
1311
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1312
this_parent, this_name,
1313
other_parent, other_name))
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))
1314
1080
if other_name is None:
1315
1081
# it doesn't matter whether the result was 'other' or
1316
1082
# '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)
1318
1086
parent_id = parents[self.winner_idx[parent_id_winner]]
1319
1087
if parent_id is not None:
1320
# if we get here, name_winner and parent_winner are set to safe
1088
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1322
1089
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1323
self.tt.trans_id_file_id(parent_id),
1324
self.tt.trans_id_file_id(file_id))
1090
parent_trans_id, trans_id)
1326
def _do_merge_contents(self, file_id):
1092
def merge_contents(self, file_id):
1327
1093
"""Performs a merge on file_id contents."""
1328
1094
def contents_pair(tree):
1329
1095
if file_id not in tree:
1358
1136
if winner == 'this':
1359
1137
# No interesting changes introduced by OTHER
1360
1138
return "unmodified"
1361
# We have a hypothetical conflict, but if we have files, then we
1362
# can try to merge the content
1363
1139
trans_id = self.tt.trans_id_file_id(file_id)
1364
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1365
other_pair[0], winner)
1366
hooks = self.active_hooks
1367
hook_status = 'not_applicable'
1369
hook_status, lines = hook.merge_contents(params)
1370
if hook_status != 'not_applicable':
1371
# Don't try any more hooks, this one applies.
1374
if hook_status == 'not_applicable':
1375
# This is a contents conflict, because none of the available
1376
# functions could merge it.
1378
name = self.tt.final_name(trans_id)
1379
parent_id = self.tt.final_parent(trans_id)
1380
if self.this_tree.has_id(file_id):
1381
self.tt.unversion_file(trans_id)
1382
file_group = self._dump_conflicts(name, parent_id, file_id,
1384
self._raw_conflicts.append(('contents conflict', file_group))
1385
elif hook_status == 'success':
1386
self.tt.create_file(lines, trans_id)
1387
elif hook_status == 'conflicted':
1388
# XXX: perhaps the hook should be able to provide
1389
# the BASE/THIS/OTHER files?
1390
self.tt.create_file(lines, trans_id)
1391
self._raw_conflicts.append(('text conflict', trans_id))
1392
name = self.tt.final_name(trans_id)
1393
parent_id = self.tt.final_parent(trans_id)
1394
self._dump_conflicts(name, parent_id, file_id)
1395
elif hook_status == 'delete':
1396
self.tt.unversion_file(trans_id)
1398
elif hook_status == 'done':
1399
# The hook function did whatever it needs to do directly, no
1400
# further action needed here.
1403
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1404
if not self.this_tree.has_id(file_id) and result == "modified":
1405
self.tt.version_file(file_id, trans_id)
1406
# The merge has been performed, so the old contents should not be
1409
self.tt.delete_contents(trans_id)
1410
except errors.NoSuchFile:
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':
1140
if winner == 'other':
1452
1141
# OTHER is a straight winner, so replace this contents with other
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
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)
1465
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()
1467
1179
def get_lines(self, tree, file_id):
1468
1180
"""Return the lines in a file, or an empty list."""
1469
if tree.has_id(file_id):
1470
return tree.get_file_lines(file_id)
1182
return tree.get_file(file_id).readlines()
1641
1326
if path.endswith(suffix):
1642
1327
path = path[:-len(suffix)]
1644
c = _mod_conflicts.Conflict.factory(conflict_type,
1645
path=path, file_id=file_id)
1646
elif conflict_type == 'text conflict':
1329
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1330
self.cooked_conflicts.append(c)
1331
if conflict_type == 'text conflict':
1647
1332
trans_id = conflict[1]
1648
1333
path = fp.get_path(trans_id)
1649
1334
file_id = self.tt.final_file_id(trans_id)
1650
c = _mod_conflicts.Conflict.factory(conflict_type,
1651
path=path, file_id=file_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)
1653
raise AssertionError('bad conflict type: %r' % (conflict,))
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)
1654
1362
self.cooked_conflicts.append(c)
1655
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1363
self.cooked_conflicts.sort(key=Conflict.sort_key)
1658
1366
class WeaveMerger(Merge3Merger):
1662
1370
supports_reverse_cherrypick = False
1663
1371
history_based = True
1665
def _generate_merge_plan(self, file_id, base):
1666
return self.this_tree.plan_file_merge(file_id, self.other_tree,
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,
1669
def _merged_lines(self, file_id):
1670
"""Generate the merged lines.
1671
There is no distinction between lines that are meant to contain <<<<<<<
1675
base = self.base_tree
1678
plan = self._generate_merge_plan(file_id, base)
1679
1384
if 'merge' in debug.debug_flags:
1680
1385
plan = list(plan)
1681
1386
trans_id = self.tt.trans_id_file_id(file_id)
1682
1387
name = self.tt.final_name(trans_id) + '.plan'
1683
contents = ('%11s|%s' % l for l in plan)
1388
contents = ('%10s|%s' % l for l in plan)
1684
1389
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1685
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1686
'>>>>>>> MERGE-SOURCE\n')
1687
lines, conflicts = textmerge.merge_lines(self.reprocess)
1689
base_lines = textmerge.base_from_plan()
1692
return lines, base_lines
1390
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1391
'>>>>>>> MERGE-SOURCE\n')
1392
return textmerge.merge_lines(self.reprocess)
1694
1394
def text_merge(self, file_id, trans_id):
1695
1395
"""Perform a (weave) text merge for a given file and file-id.
1696
1396
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1697
1397
and a conflict will be noted.
1699
lines, base_lines = self._merged_lines(file_id)
1399
lines, conflicts = self._merged_lines(file_id)
1700
1400
lines = list(lines)
1701
1401
# Note we're checking whether the OUTPUT is binary in this case,
1702
1402
# because we don't want to get into weave merge guts.
1703
textfile.check_text_lines(lines)
1403
check_text_lines(lines)
1704
1404
self.tt.create_file(lines, trans_id)
1705
if base_lines is not None:
1707
1406
self._raw_conflicts.append(('text conflict', trans_id))
1708
1407
name = self.tt.final_name(trans_id)
1709
1408
parent_id = self.tt.final_parent(trans_id)
1710
1409
file_group = self._dump_conflicts(name, parent_id, file_id,
1712
base_lines=base_lines)
1713
1411
file_group.append(trans_id)
1716
1414
class LCAMerger(WeaveMerger):
1718
def _generate_merge_plan(self, file_id, base):
1719
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
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,
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)
1722
1438
class Diff3Merger(Merge3Merger):
1723
1439
"""Three-way merger using external diff3 for text merging"""
1725
1441
def dump_file(self, temp_dir, name, tree, file_id):
1726
out_path = osutils.pathjoin(temp_dir, name)
1442
out_path = pathjoin(temp_dir, name)
1727
1443
out_file = open(out_path, "wb")
1729
1445
in_file = tree.get_file(file_id)
1762
1478
osutils.rmtree(temp_dir)
1765
class PathNotInTree(errors.BzrError):
1767
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1769
def __init__(self, path, tree):
1770
errors.BzrError.__init__(self, path=path, tree=tree)
1773
class MergeIntoMerger(Merger):
1774
"""Merger that understands other_tree will be merged into a subdir.
1776
This also changes the Merger api so that it uses real Branch, revision_id,
1777
and RevisonTree objects, rather than using revision specs.
1780
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1781
source_subpath, other_rev_id=None):
1782
"""Create a new MergeIntoMerger object.
1784
source_subpath in other_tree will be effectively copied to
1785
target_subdir in this_tree.
1787
:param this_tree: The tree that we will be merging into.
1788
:param other_branch: The Branch we will be merging from.
1789
:param other_tree: The RevisionTree object we want to merge.
1790
:param target_subdir: The relative path where we want to merge
1791
other_tree into this_tree
1792
:param source_subpath: The relative path specifying the subtree of
1793
other_tree to merge into this_tree.
1795
# It is assumed that we are merging a tree that is not in our current
1796
# ancestry, which means we are using the "EmptyTree" as our basis.
1797
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1798
_mod_revision.NULL_REVISION)
1799
super(MergeIntoMerger, self).__init__(
1800
this_branch=this_tree.branch,
1801
this_tree=this_tree,
1802
other_tree=other_tree,
1803
base_tree=null_ancestor_tree,
1805
self._target_subdir = target_subdir
1806
self._source_subpath = source_subpath
1807
self.other_branch = other_branch
1808
if other_rev_id is None:
1809
other_rev_id = other_tree.get_revision_id()
1810
self.other_rev_id = self.other_basis = other_rev_id
1811
self.base_is_ancestor = True
1812
self.backup_files = True
1813
self.merge_type = Merge3Merger
1814
self.show_base = False
1815
self.reprocess = False
1816
self.interesting_ids = None
1817
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1818
target_subdir=self._target_subdir,
1819
source_subpath=self._source_subpath)
1820
if self._source_subpath != '':
1821
# If this isn't a partial merge make sure the revisions will be
1823
self._maybe_fetch(self.other_branch, self.this_branch,
1826
def set_pending(self):
1827
if self._source_subpath != '':
1829
Merger.set_pending(self)
1832
class _MergeTypeParameterizer(object):
1833
"""Wrap a merge-type class to provide extra parameters.
1835
This is hack used by MergeIntoMerger to pass some extra parameters to its
1836
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1837
the 'merge_type' member. It is difficult override do_merge without
1838
re-writing the whole thing, so instead we create a wrapper which will pass
1839
the extra parameters.
1842
def __init__(self, merge_type, **kwargs):
1843
self._extra_kwargs = kwargs
1844
self._merge_type = merge_type
1846
def __call__(self, *args, **kwargs):
1847
kwargs.update(self._extra_kwargs)
1848
return self._merge_type(*args, **kwargs)
1850
def __getattr__(self, name):
1851
return getattr(self._merge_type, name)
1854
class MergeIntoMergeType(Merge3Merger):
1855
"""Merger that incorporates a tree (or part of a tree) into another."""
1857
def __init__(self, *args, **kwargs):
1858
"""Initialize the merger object.
1860
:param args: See Merge3Merger.__init__'s args.
1861
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1862
source_subpath and target_subdir.
1863
:keyword source_subpath: The relative path specifying the subtree of
1864
other_tree to merge into this_tree.
1865
:keyword target_subdir: The relative path where we want to merge
1866
other_tree into this_tree
1868
# All of the interesting work happens during Merge3Merger.__init__(),
1869
# so we have have to hack in to get our extra parameters set.
1870
self._source_subpath = kwargs.pop('source_subpath')
1871
self._target_subdir = kwargs.pop('target_subdir')
1872
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1874
def _compute_transform(self):
1875
child_pb = ui.ui_factory.nested_progress_bar()
1877
entries = self._entries_to_incorporate()
1878
entries = list(entries)
1879
for num, (entry, parent_id) in enumerate(entries):
1880
child_pb.update('Preparing file merge', num, len(entries))
1881
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1882
trans_id = transform.new_by_entry(self.tt, entry,
1883
parent_trans_id, self.other_tree)
1886
self._finish_computing_transform()
1888
def _entries_to_incorporate(self):
1889
"""Yields pairs of (inventory_entry, new_parent)."""
1890
other_inv = self.other_tree.inventory
1891
subdir_id = other_inv.path2id(self._source_subpath)
1892
if subdir_id is None:
1893
# XXX: The error would be clearer if it gave the URL of the source
1894
# branch, but we don't have a reference to that here.
1895
raise PathNotInTree(self._source_subpath, "Source tree")
1896
subdir = other_inv[subdir_id]
1897
parent_in_target = osutils.dirname(self._target_subdir)
1898
target_id = self.this_tree.inventory.path2id(parent_in_target)
1899
if target_id is None:
1900
raise PathNotInTree(self._target_subdir, "Target tree")
1901
name_in_target = osutils.basename(self._target_subdir)
1902
merge_into_root = subdir.copy()
1903
merge_into_root.name = name_in_target
1904
if merge_into_root.file_id in self.this_tree.inventory:
1905
# Give the root a new file-id.
1906
# This can happen fairly easily if the directory we are
1907
# incorporating is the root, and both trees have 'TREE_ROOT' as
1908
# their root_id. Users will expect this to Just Work, so we
1909
# change the file-id here.
1910
# Non-root file-ids could potentially conflict too. That's really
1911
# an edge case, so we don't do anything special for those. We let
1912
# them cause conflicts.
1913
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1914
yield (merge_into_root, target_id)
1915
if subdir.kind != 'directory':
1916
# No children, so we are done.
1918
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1919
parent_id = entry.parent_id
1920
if parent_id == subdir.file_id:
1921
# The root's parent ID has changed, so make sure children of
1922
# the root refer to the new ID.
1923
parent_id = merge_into_root.file_id
1924
yield (entry, parent_id)
1927
1481
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1928
1482
backup_files=False,
1929
1483
merge_type=Merge3Merger,