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,
23
conflicts as _mod_conflicts,
26
26
graph as _mod_graph,
30
30
revision as _mod_revision,
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_from_tree,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
40
from bzrlib.cleanup import OperationWithCleanups
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
55
from_tree.lock_tree_write()
69
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
70
interesting_ids=interesting_ids, this_tree=from_tree)
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)
75
264
class Merger(object):
76
268
def __init__(self, this_branch, other_tree=None, base_tree=None,
77
269
this_tree=None, pb=None, change_reporter=None,
78
270
recurse='down', revision_graph=None):
600
794
# making sure we haven't missed any corner cases.
601
795
# if lca_trees is None:
602
796
# self._lca_trees = [self.base_tree]
605
797
self.change_reporter = change_reporter
606
798
self.cherrypick = cherrypick
608
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")
612
806
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
613
808
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
614
810
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
615
812
self.other_tree.lock_read()
616
self.tt = TreeTransform(self.this_tree, self.pb)
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)
619
self._compute_transform()
621
results = self.tt.apply(no_conflicts=True)
622
self.write_modified(results)
624
self.this_tree.add_conflicts(self.cooked_conflicts)
625
except UnsupportedOperation:
629
self.other_tree.unlock()
630
self.base_tree.unlock()
631
self.this_tree.unlock()
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
634
827
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
635
829
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
636
831
self.other_tree.lock_read()
637
self.tt = TransformPreview(self.this_tree)
640
self._compute_transform()
643
self.other_tree.unlock()
644
self.base_tree.unlock()
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()
648
840
def _compute_transform(self):
899
1098
other_root = self.tt.trans_id_file_id(other_root_file_id)
900
1099
if other_root == self.tt.root:
1101
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)
903
1106
self.tt.final_kind(other_root)
906
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
907
# the other tree's root is a non-root in the current tree
909
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
910
self.tt.cancel_creation(other_root)
911
self.tt.cancel_versioning(other_root)
913
def reparent_children(self, ie, target):
914
for thing, child in ie.children.iteritems():
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():
915
1116
trans_id = self.tt.trans_id_file_id(child.file_id)
916
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_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)
918
1134
def write_modified(self, results):
919
1135
modified_hashes = {}
1084
1303
parent_id_winner = "other"
1085
1304
if name_winner == "this" and parent_id_winner == "this":
1087
if name_winner == "conflict":
1088
trans_id = self.tt.trans_id_file_id(file_id)
1089
self._raw_conflicts.append(('name conflict', trans_id,
1090
this_name, other_name))
1091
if parent_id_winner == "conflict":
1092
trans_id = self.tt.trans_id_file_id(file_id)
1093
self._raw_conflicts.append(('parent conflict', trans_id,
1094
this_parent, other_parent))
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))
1095
1314
if other_name is None:
1096
1315
# it doesn't matter whether the result was 'other' or
1097
1316
# 'conflict'-- if there's no 'other', we leave it alone.
1099
# if we get here, name_winner and parent_winner are set to safe values.
1100
trans_id = self.tt.trans_id_file_id(file_id)
1101
1318
parent_id = parents[self.winner_idx[parent_id_winner]]
1102
1319
if parent_id is not None:
1103
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1320
# if we get here, name_winner and parent_winner are set to safe
1104
1322
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1105
parent_trans_id, trans_id)
1323
self.tt.trans_id_file_id(parent_id),
1324
self.tt.trans_id_file_id(file_id))
1107
def merge_contents(self, file_id):
1326
def _do_merge_contents(self, file_id):
1108
1327
"""Performs a merge on file_id contents."""
1109
1328
def contents_pair(tree):
1110
1329
if file_id not in tree:
1151
1358
if winner == 'this':
1152
1359
# No interesting changes introduced by OTHER
1153
1360
return "unmodified"
1361
# We have a hypothetical conflict, but if we have files, then we
1362
# can try to merge the content
1154
1363
trans_id = self.tt.trans_id_file_id(file_id)
1155
if winner == 'other':
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':
1156
1452
# OTHER is a straight winner, so replace this contents with other
1157
file_in_this = file_id in self.this_tree
1159
# Remove any existing contents
1160
self.tt.delete_contents(trans_id)
1161
if file_id in self.other_tree:
1162
# OTHER changed the file
1163
create_from_tree(self.tt, trans_id,
1164
self.other_tree, file_id)
1165
if not file_in_this:
1166
self.tt.version_file(file_id, trans_id)
1169
# OTHER deleted the file
1170
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
1173
# We have a hypothetical conflict, but if we have files, then we
1174
# can try to merge the content
1175
if this_pair[0] == 'file' and other_pair[0] == 'file':
1176
# THIS and OTHER are both files, so text merge. Either
1177
# BASE is a file, or both converted to files, so at least we
1178
# have agreement that output should be a file.
1180
self.text_merge(file_id, trans_id)
1182
return contents_conflict()
1183
if file_id not in self.this_tree:
1184
self.tt.version_file(file_id, trans_id)
1186
self.tt.tree_kind(trans_id)
1187
self.tt.delete_contents(trans_id)
1192
return contents_conflict()
1465
return 'not_applicable', None
1194
1467
def get_lines(self, tree, file_id):
1195
1468
"""Return the lines in a file, or an empty list."""
1197
return tree.get_file(file_id).readlines()
1469
if tree.has_id(file_id):
1470
return tree.get_file_lines(file_id)
1341
1641
if path.endswith(suffix):
1342
1642
path = path[:-len(suffix)]
1344
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1345
self.cooked_conflicts.append(c)
1346
if conflict_type == 'text conflict':
1644
c = _mod_conflicts.Conflict.factory(conflict_type,
1645
path=path, file_id=file_id)
1646
elif conflict_type == 'text conflict':
1347
1647
trans_id = conflict[1]
1348
1648
path = fp.get_path(trans_id)
1349
1649
file_id = self.tt.final_file_id(trans_id)
1350
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1351
self.cooked_conflicts.append(c)
1353
for trans_id, conflicts in name_conflicts.iteritems():
1355
this_parent, other_parent = conflicts['parent conflict']
1356
if this_parent == other_parent:
1357
raise AssertionError()
1359
this_parent = other_parent = \
1360
self.tt.final_file_id(self.tt.final_parent(trans_id))
1362
this_name, other_name = conflicts['name conflict']
1363
if this_name == other_name:
1364
raise AssertionError()
1366
this_name = other_name = self.tt.final_name(trans_id)
1367
other_path = fp.get_path(trans_id)
1368
if this_parent is not None and this_name is not None:
1369
this_parent_path = \
1370
fp.get_path(self.tt.trans_id_file_id(this_parent))
1371
this_path = pathjoin(this_parent_path, this_name)
1650
c = _mod_conflicts.Conflict.factory(conflict_type,
1651
path=path, file_id=file_id)
1373
this_path = "<deleted>"
1374
file_id = self.tt.final_file_id(trans_id)
1375
c = Conflict.factory('path conflict', path=this_path,
1376
conflict_path=other_path, file_id=file_id)
1653
raise AssertionError('bad conflict type: %r' % (conflict,))
1377
1654
self.cooked_conflicts.append(c)
1378
self.cooked_conflicts.sort(key=Conflict.sort_key)
1655
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1381
1658
class WeaveMerger(Merge3Merger):
1385
1662
supports_reverse_cherrypick = False
1386
1663
history_based = True
1388
def _merged_lines(self, file_id):
1389
"""Generate the merged lines.
1390
There is no distinction between lines that are meant to contain <<<<<<<
1394
base = self.base_tree
1397
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1665
def _generate_merge_plan(self, file_id, base):
1666
return 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)
1399
1679
if 'merge' in debug.debug_flags:
1400
1680
plan = list(plan)
1401
1681
trans_id = self.tt.trans_id_file_id(file_id)
1402
1682
name = self.tt.final_name(trans_id) + '.plan'
1403
contents = ('%10s|%s' % l for l in plan)
1683
contents = ('%11s|%s' % l for l in plan)
1404
1684
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1405
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1406
'>>>>>>> MERGE-SOURCE\n')
1407
return textmerge.merge_lines(self.reprocess)
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
1409
1694
def text_merge(self, file_id, trans_id):
1410
1695
"""Perform a (weave) text merge for a given file and file-id.
1411
1696
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1412
1697
and a conflict will be noted.
1414
lines, conflicts = self._merged_lines(file_id)
1699
lines, base_lines = self._merged_lines(file_id)
1415
1700
lines = list(lines)
1416
1701
# Note we're checking whether the OUTPUT is binary in this case,
1417
1702
# because we don't want to get into weave merge guts.
1418
check_text_lines(lines)
1703
textfile.check_text_lines(lines)
1419
1704
self.tt.create_file(lines, trans_id)
1705
if base_lines is not None:
1421
1707
self._raw_conflicts.append(('text conflict', trans_id))
1422
1708
name = self.tt.final_name(trans_id)
1423
1709
parent_id = self.tt.final_parent(trans_id)
1424
1710
file_group = self._dump_conflicts(name, parent_id, file_id,
1712
base_lines=base_lines)
1426
1713
file_group.append(trans_id)
1429
1716
class LCAMerger(WeaveMerger):
1431
def _merged_lines(self, file_id):
1432
"""Generate the merged lines.
1433
There is no distinction between lines that are meant to contain <<<<<<<
1437
base = self.base_tree
1440
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1718
def _generate_merge_plan(self, file_id, base):
1719
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1442
if 'merge' in debug.debug_flags:
1444
trans_id = self.tt.trans_id_file_id(file_id)
1445
name = self.tt.final_name(trans_id) + '.plan'
1446
contents = ('%10s|%s' % l for l in plan)
1447
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1448
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1449
'>>>>>>> MERGE-SOURCE\n')
1450
return textmerge.merge_lines(self.reprocess)
1453
1722
class Diff3Merger(Merge3Merger):
1454
1723
"""Three-way merger using external diff3 for text merging"""
1456
1725
def dump_file(self, temp_dir, name, tree, file_id):
1457
out_path = pathjoin(temp_dir, name)
1726
out_path = osutils.pathjoin(temp_dir, name)
1458
1727
out_file = open(out_path, "wb")
1460
1729
in_file = tree.get_file(file_id)
1493
1762
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)
1496
1927
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1497
1928
backup_files=False,
1498
1929
merge_type=Merge3Merger,