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
67
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)
69
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
70
interesting_ids=interesting_ids, this_tree=from_tree)
264
75
class Merger(object):
268
76
def __init__(self, this_branch, other_tree=None, base_tree=None,
269
77
this_tree=None, pb=None, change_reporter=None,
270
78
recurse='down', revision_graph=None):
794
600
# making sure we haven't missed any corner cases.
795
601
# if lca_trees is None:
796
602
# self._lca_trees = [self.base_tree]
797
605
self.change_reporter = change_reporter
798
606
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")
806
612
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
808
613
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
810
614
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
812
615
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)
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
617
self.tt = TreeTransform(self.this_tree, self.pb)
620
self._compute_transform()
622
results = self.tt.apply(no_conflicts=True)
623
self.write_modified(results)
625
self.this_tree.add_conflicts(self.cooked_conflicts)
626
except UnsupportedOperation:
631
self.other_tree.unlock()
632
self.base_tree.unlock()
633
self.this_tree.unlock()
827
636
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
829
637
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
831
638
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()
639
self.tt = TransformPreview(self.this_tree)
642
self._compute_transform()
645
self.other_tree.unlock()
646
self.base_tree.unlock()
840
650
def _compute_transform(self):
1295
1086
parent_id_winner = "other"
1296
1087
if name_winner == "this" and parent_id_winner == "this":
1298
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1299
# Creating helpers (.OTHER or .THIS) here cause problems down the
1300
# road if a ContentConflict needs to be created so we should not do
1302
trans_id = self.tt.trans_id_file_id(file_id)
1303
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1304
this_parent, this_name,
1305
other_parent, other_name))
1089
if name_winner == "conflict":
1090
trans_id = self.tt.trans_id_file_id(file_id)
1091
self._raw_conflicts.append(('name conflict', trans_id,
1092
this_name, other_name))
1093
if parent_id_winner == "conflict":
1094
trans_id = self.tt.trans_id_file_id(file_id)
1095
self._raw_conflicts.append(('parent conflict', trans_id,
1096
this_parent, other_parent))
1306
1097
if other_name is None:
1307
1098
# it doesn't matter whether the result was 'other' or
1308
1099
# 'conflict'-- if there's no 'other', we leave it alone.
1101
# if we get here, name_winner and parent_winner are set to safe values.
1102
trans_id = self.tt.trans_id_file_id(file_id)
1310
1103
parent_id = parents[self.winner_idx[parent_id_winner]]
1311
1104
if parent_id is not None:
1312
# if we get here, name_winner and parent_winner are set to safe
1105
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1314
1106
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1315
self.tt.trans_id_file_id(parent_id),
1316
self.tt.trans_id_file_id(file_id))
1107
parent_trans_id, trans_id)
1318
def _do_merge_contents(self, file_id):
1109
def merge_contents(self, file_id):
1319
1110
"""Performs a merge on file_id contents."""
1320
1111
def contents_pair(tree):
1321
1112
if file_id not in tree:
1350
1153
if winner == 'this':
1351
1154
# No interesting changes introduced by OTHER
1352
1155
return "unmodified"
1353
# We have a hypothetical conflict, but if we have files, then we
1354
# can try to merge the content
1355
1156
trans_id = self.tt.trans_id_file_id(file_id)
1356
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1357
other_pair[0], winner)
1358
hooks = self.active_hooks
1359
hook_status = 'not_applicable'
1361
hook_status, lines = hook.merge_contents(params)
1362
if hook_status != 'not_applicable':
1363
# Don't try any more hooks, this one applies.
1366
if hook_status == 'not_applicable':
1367
# This is a contents conflict, because none of the available
1368
# functions could merge it.
1370
name = self.tt.final_name(trans_id)
1371
parent_id = self.tt.final_parent(trans_id)
1372
if self.this_tree.has_id(file_id):
1373
self.tt.unversion_file(trans_id)
1374
file_group = self._dump_conflicts(name, parent_id, file_id,
1376
self._raw_conflicts.append(('contents conflict', file_group))
1377
elif hook_status == 'success':
1378
self.tt.create_file(lines, trans_id)
1379
elif hook_status == 'conflicted':
1380
# XXX: perhaps the hook should be able to provide
1381
# the BASE/THIS/OTHER files?
1382
self.tt.create_file(lines, trans_id)
1383
self._raw_conflicts.append(('text conflict', trans_id))
1384
name = self.tt.final_name(trans_id)
1385
parent_id = self.tt.final_parent(trans_id)
1386
self._dump_conflicts(name, parent_id, file_id)
1387
elif hook_status == 'delete':
1388
self.tt.unversion_file(trans_id)
1390
elif hook_status == 'done':
1391
# The hook function did whatever it needs to do directly, no
1392
# further action needed here.
1395
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1396
if not self.this_tree.has_id(file_id) and result == "modified":
1397
self.tt.version_file(file_id, trans_id)
1398
# The merge has been performed, so the old contents should not be
1400
self.tt.delete_contents(trans_id)
1403
def _default_other_winner_merge(self, merge_hook_params):
1404
"""Replace this contents with other."""
1405
file_id = merge_hook_params.file_id
1406
trans_id = merge_hook_params.trans_id
1407
file_in_this = self.this_tree.has_id(file_id)
1408
if self.other_tree.has_id(file_id):
1409
# OTHER changed the file
1411
if wt.supports_content_filtering():
1412
# We get the path from the working tree if it exists.
1413
# That fails though when OTHER is adding a file, so
1414
# we fall back to the other tree to find the path if
1415
# it doesn't exist locally.
1417
filter_tree_path = wt.id2path(file_id)
1418
except errors.NoSuchId:
1419
filter_tree_path = self.other_tree.id2path(file_id)
1421
# Skip the id2path lookup for older formats
1422
filter_tree_path = None
1423
transform.create_from_tree(self.tt, trans_id,
1424
self.other_tree, file_id,
1425
filter_tree_path=filter_tree_path)
1428
# OTHER deleted the file
1429
return 'delete', None
1431
raise AssertionError(
1432
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1435
def merge_contents(self, merge_hook_params):
1436
"""Fallback merge logic after user installed hooks."""
1437
# This function is used in merge hooks as the fallback instance.
1438
# Perhaps making this function and the functions it calls be a
1439
# a separate class would be better.
1440
if merge_hook_params.winner == 'other':
1157
if winner == 'other':
1441
1158
# OTHER is a straight winner, so replace this contents with other
1442
return self._default_other_winner_merge(merge_hook_params)
1443
elif merge_hook_params.is_file_merge():
1444
# THIS and OTHER are both files, so text merge. Either
1445
# BASE is a file, or both converted to files, so at least we
1446
# have agreement that output should be a file.
1448
self.text_merge(merge_hook_params.file_id,
1449
merge_hook_params.trans_id)
1450
except errors.BinaryFile:
1451
return 'not_applicable', None
1159
file_in_this = file_id in self.this_tree
1161
# Remove any existing contents
1162
self.tt.delete_contents(trans_id)
1163
if file_id in self.other_tree:
1164
# OTHER changed the file
1166
if wt.supports_content_filtering():
1167
# We get the path from the working tree if it exists.
1168
# That fails though when OTHER is adding a file, so
1169
# we fall back to the other tree to find the path if
1170
# it doesn't exist locally.
1172
filter_tree_path = wt.id2path(file_id)
1173
except errors.NoSuchId:
1174
filter_tree_path = self.other_tree.id2path(file_id)
1176
# Skip the id2path lookup for older formats
1177
filter_tree_path = None
1178
create_from_tree(self.tt, trans_id,
1179
self.other_tree, file_id,
1180
filter_tree_path=filter_tree_path)
1181
if not file_in_this:
1182
self.tt.version_file(file_id, trans_id)
1185
# OTHER deleted the file
1186
self.tt.unversion_file(trans_id)
1454
return 'not_applicable', None
1189
# We have a hypothetical conflict, but if we have files, then we
1190
# can try to merge the content
1191
if this_pair[0] == 'file' and other_pair[0] == 'file':
1192
# THIS and OTHER are both files, so text merge. Either
1193
# BASE is a file, or both converted to files, so at least we
1194
# have agreement that output should be a file.
1196
self.text_merge(file_id, trans_id)
1198
return contents_conflict()
1199
if file_id not in self.this_tree:
1200
self.tt.version_file(file_id, trans_id)
1202
self.tt.tree_kind(trans_id)
1203
self.tt.delete_contents(trans_id)
1208
return contents_conflict()
1456
1210
def get_lines(self, tree, file_id):
1457
1211
"""Return the lines in a file, or an empty list."""
1458
if tree.has_id(file_id):
1459
return tree.get_file_lines(file_id)
1213
return tree.get_file(file_id).readlines()
1627
1372
if path.endswith(suffix):
1628
1373
path = path[:-len(suffix)]
1630
c = _mod_conflicts.Conflict.factory(conflict_type,
1631
path=path, file_id=file_id)
1632
elif conflict_type == 'text conflict':
1375
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1376
self.cooked_conflicts.append(c)
1377
if conflict_type == 'text conflict':
1633
1378
trans_id = conflict[1]
1634
1379
path = fp.get_path(trans_id)
1635
1380
file_id = self.tt.final_file_id(trans_id)
1636
c = _mod_conflicts.Conflict.factory(conflict_type,
1637
path=path, file_id=file_id)
1381
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1382
self.cooked_conflicts.append(c)
1384
for trans_id, conflicts in name_conflicts.iteritems():
1386
this_parent, other_parent = conflicts['parent conflict']
1387
if this_parent == other_parent:
1388
raise AssertionError()
1390
this_parent = other_parent = \
1391
self.tt.final_file_id(self.tt.final_parent(trans_id))
1393
this_name, other_name = conflicts['name conflict']
1394
if this_name == other_name:
1395
raise AssertionError()
1397
this_name = other_name = self.tt.final_name(trans_id)
1398
other_path = fp.get_path(trans_id)
1399
if this_parent is not None and this_name is not None:
1400
this_parent_path = \
1401
fp.get_path(self.tt.trans_id_file_id(this_parent))
1402
this_path = pathjoin(this_parent_path, this_name)
1639
raise AssertionError('bad conflict type: %r' % (conflict,))
1404
this_path = "<deleted>"
1405
file_id = self.tt.final_file_id(trans_id)
1406
c = Conflict.factory('path conflict', path=this_path,
1407
conflict_path=other_path, file_id=file_id)
1640
1408
self.cooked_conflicts.append(c)
1641
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1409
self.cooked_conflicts.sort(key=Conflict.sort_key)
1644
1412
class WeaveMerger(Merge3Merger):
1648
1416
supports_reverse_cherrypick = False
1649
1417
history_based = True
1651
def _generate_merge_plan(self, file_id, base):
1652
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1419
def _merged_lines(self, file_id):
1420
"""Generate the merged lines.
1421
There is no distinction between lines that are meant to contain <<<<<<<
1425
base = self.base_tree
1428
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1655
def _merged_lines(self, file_id):
1656
"""Generate the merged lines.
1657
There is no distinction between lines that are meant to contain <<<<<<<
1661
base = self.base_tree
1664
plan = self._generate_merge_plan(file_id, base)
1665
1430
if 'merge' in debug.debug_flags:
1666
1431
plan = list(plan)
1667
1432
trans_id = self.tt.trans_id_file_id(file_id)
1668
1433
name = self.tt.final_name(trans_id) + '.plan'
1669
contents = ('%11s|%s' % l for l in plan)
1434
contents = ('%10s|%s' % l for l in plan)
1670
1435
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1671
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1672
'>>>>>>> MERGE-SOURCE\n')
1673
lines, conflicts = textmerge.merge_lines(self.reprocess)
1675
base_lines = textmerge.base_from_plan()
1678
return lines, base_lines
1436
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1437
'>>>>>>> MERGE-SOURCE\n')
1438
return textmerge.merge_lines(self.reprocess)
1680
1440
def text_merge(self, file_id, trans_id):
1681
1441
"""Perform a (weave) text merge for a given file and file-id.
1682
1442
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1683
1443
and a conflict will be noted.
1685
lines, base_lines = self._merged_lines(file_id)
1445
lines, conflicts = self._merged_lines(file_id)
1686
1446
lines = list(lines)
1687
1447
# Note we're checking whether the OUTPUT is binary in this case,
1688
1448
# because we don't want to get into weave merge guts.
1689
textfile.check_text_lines(lines)
1449
check_text_lines(lines)
1690
1450
self.tt.create_file(lines, trans_id)
1691
if base_lines is not None:
1693
1452
self._raw_conflicts.append(('text conflict', trans_id))
1694
1453
name = self.tt.final_name(trans_id)
1695
1454
parent_id = self.tt.final_parent(trans_id)
1696
1455
file_group = self._dump_conflicts(name, parent_id, file_id,
1698
base_lines=base_lines)
1699
1457
file_group.append(trans_id)
1702
1460
class LCAMerger(WeaveMerger):
1704
def _generate_merge_plan(self, file_id, base):
1705
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1462
def _merged_lines(self, file_id):
1463
"""Generate the merged lines.
1464
There is no distinction between lines that are meant to contain <<<<<<<
1468
base = self.base_tree
1471
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1473
if 'merge' in debug.debug_flags:
1475
trans_id = self.tt.trans_id_file_id(file_id)
1476
name = self.tt.final_name(trans_id) + '.plan'
1477
contents = ('%10s|%s' % l for l in plan)
1478
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1479
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1480
'>>>>>>> MERGE-SOURCE\n')
1481
return textmerge.merge_lines(self.reprocess)
1708
1484
class Diff3Merger(Merge3Merger):
1709
1485
"""Three-way merger using external diff3 for text merging"""
1711
1487
def dump_file(self, temp_dir, name, tree, file_id):
1712
out_path = osutils.pathjoin(temp_dir, name)
1488
out_path = pathjoin(temp_dir, name)
1713
1489
out_file = open(out_path, "wb")
1715
1491
in_file = tree.get_file(file_id)
1748
1524
osutils.rmtree(temp_dir)
1751
class PathNotInTree(errors.BzrError):
1753
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1755
def __init__(self, path, tree):
1756
errors.BzrError.__init__(self, path=path, tree=tree)
1759
class MergeIntoMerger(Merger):
1760
"""Merger that understands other_tree will be merged into a subdir.
1762
This also changes the Merger api so that it uses real Branch, revision_id,
1763
and RevisonTree objects, rather than using revision specs.
1766
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1767
source_subpath, other_rev_id=None):
1768
"""Create a new MergeIntoMerger object.
1770
source_subpath in other_tree will be effectively copied to
1771
target_subdir in this_tree.
1773
:param this_tree: The tree that we will be merging into.
1774
:param other_branch: The Branch we will be merging from.
1775
:param other_tree: The RevisionTree object we want to merge.
1776
:param target_subdir: The relative path where we want to merge
1777
other_tree into this_tree
1778
:param source_subpath: The relative path specifying the subtree of
1779
other_tree to merge into this_tree.
1781
# It is assumed that we are merging a tree that is not in our current
1782
# ancestry, which means we are using the "EmptyTree" as our basis.
1783
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1784
_mod_revision.NULL_REVISION)
1785
super(MergeIntoMerger, self).__init__(
1786
this_branch=this_tree.branch,
1787
this_tree=this_tree,
1788
other_tree=other_tree,
1789
base_tree=null_ancestor_tree,
1791
self._target_subdir = target_subdir
1792
self._source_subpath = source_subpath
1793
self.other_branch = other_branch
1794
if other_rev_id is None:
1795
other_rev_id = other_tree.get_revision_id()
1796
self.other_rev_id = self.other_basis = other_rev_id
1797
self.base_is_ancestor = True
1798
self.backup_files = True
1799
self.merge_type = Merge3Merger
1800
self.show_base = False
1801
self.reprocess = False
1802
self.interesting_ids = None
1803
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1804
target_subdir=self._target_subdir,
1805
source_subpath=self._source_subpath)
1806
if self._source_subpath != '':
1807
# If this isn't a partial merge make sure the revisions will be
1809
self._maybe_fetch(self.other_branch, self.this_branch,
1812
def set_pending(self):
1813
if self._source_subpath != '':
1815
Merger.set_pending(self)
1818
class _MergeTypeParameterizer(object):
1819
"""Wrap a merge-type class to provide extra parameters.
1821
This is hack used by MergeIntoMerger to pass some extra parameters to its
1822
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1823
the 'merge_type' member. It is difficult override do_merge without
1824
re-writing the whole thing, so instead we create a wrapper which will pass
1825
the extra parameters.
1828
def __init__(self, merge_type, **kwargs):
1829
self._extra_kwargs = kwargs
1830
self._merge_type = merge_type
1832
def __call__(self, *args, **kwargs):
1833
kwargs.update(self._extra_kwargs)
1834
return self._merge_type(*args, **kwargs)
1836
def __getattr__(self, name):
1837
return getattr(self._merge_type, name)
1840
class MergeIntoMergeType(Merge3Merger):
1841
"""Merger that incorporates a tree (or part of a tree) into another."""
1843
def __init__(self, *args, **kwargs):
1844
"""Initialize the merger object.
1846
:param args: See Merge3Merger.__init__'s args.
1847
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1848
source_subpath and target_subdir.
1849
:keyword source_subpath: The relative path specifying the subtree of
1850
other_tree to merge into this_tree.
1851
:keyword target_subdir: The relative path where we want to merge
1852
other_tree into this_tree
1854
# All of the interesting work happens during Merge3Merger.__init__(),
1855
# so we have have to hack in to get our extra parameters set.
1856
self._source_subpath = kwargs.pop('source_subpath')
1857
self._target_subdir = kwargs.pop('target_subdir')
1858
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1860
def _compute_transform(self):
1861
child_pb = ui.ui_factory.nested_progress_bar()
1863
entries = self._entries_to_incorporate()
1864
entries = list(entries)
1865
for num, (entry, parent_id) in enumerate(entries):
1866
child_pb.update('Preparing file merge', num, len(entries))
1867
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1868
trans_id = transform.new_by_entry(self.tt, entry,
1869
parent_trans_id, self.other_tree)
1872
self._finish_computing_transform()
1874
def _entries_to_incorporate(self):
1875
"""Yields pairs of (inventory_entry, new_parent)."""
1876
other_inv = self.other_tree.inventory
1877
subdir_id = other_inv.path2id(self._source_subpath)
1878
if subdir_id is None:
1879
# XXX: The error would be clearer if it gave the URL of the source
1880
# branch, but we don't have a reference to that here.
1881
raise PathNotInTree(self._source_subpath, "Source tree")
1882
subdir = other_inv[subdir_id]
1883
parent_in_target = osutils.dirname(self._target_subdir)
1884
target_id = self.this_tree.inventory.path2id(parent_in_target)
1885
if target_id is None:
1886
raise PathNotInTree(self._target_subdir, "Target tree")
1887
name_in_target = osutils.basename(self._target_subdir)
1888
merge_into_root = subdir.copy()
1889
merge_into_root.name = name_in_target
1890
if merge_into_root.file_id in self.this_tree.inventory:
1891
# Give the root a new file-id.
1892
# This can happen fairly easily if the directory we are
1893
# incorporating is the root, and both trees have 'TREE_ROOT' as
1894
# their root_id. Users will expect this to Just Work, so we
1895
# change the file-id here.
1896
# Non-root file-ids could potentially conflict too. That's really
1897
# an edge case, so we don't do anything special for those. We let
1898
# them cause conflicts.
1899
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1900
yield (merge_into_root, target_id)
1901
if subdir.kind != 'directory':
1902
# No children, so we are done.
1904
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1905
parent_id = entry.parent_id
1906
if parent_id == subdir.file_id:
1907
# The root's parent ID has changed, so make sure children of
1908
# the root refer to the new ID.
1909
parent_id = merge_into_root.file_id
1910
yield (entry, parent_id)
1913
1527
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1914
1528
backup_files=False,
1915
1529
merge_type=Merge3Merger,