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
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
55
from_tree.lock_tree_write()
56
operation = 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)
71
264
class Merger(object):
72
268
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
269
this_tree=None, pb=None, change_reporter=None,
74
270
recurse='down', revision_graph=None):
257
464
if self.this_rev_id is None:
258
465
if self.this_basis_tree.get_file_sha1(file_id) != \
259
466
self.this_tree.get_file_sha1(file_id):
260
raise WorkingTreeNotRevision(self.this_tree)
467
raise errors.WorkingTreeNotRevision(self.this_tree)
262
469
trees = (self.this_basis_tree, self.other_tree)
263
470
return [get_id(tree, file_id) for tree in trees]
472
@deprecated_method(deprecated_in((2, 1, 0)))
265
473
def check_basis(self, check_clean, require_commits=True):
266
474
if self.this_basis is None and require_commits is True:
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
475
raise errors.BzrCommandError(
476
"This branch has no commits."
477
" (perhaps you would prefer 'bzr pull')")
270
479
self.compare_basis()
271
480
if self.this_basis != self.this_rev_id:
272
481
raise errors.UncommittedChanges(self.this_tree)
483
@deprecated_method(deprecated_in((2, 1, 0)))
274
484
def compare_basis(self):
276
486
basis_tree = self.revision_tree(self.this_tree.last_revision())
277
487
except errors.NoSuchRevision:
278
488
basis_tree = self.this_tree.basis_tree()
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
489
if not self.this_tree.has_changes(basis_tree):
281
490
self.this_rev_id = self.this_basis
283
492
def set_interesting_files(self, file_list):
284
493
self.interesting_files = file_list
286
495
def set_pending(self):
287
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
496
if (not self.base_is_ancestor or not self.base_is_other_ancestor
497
or self.other_rev_id is None):
289
499
self._add_parent()
291
501
def _add_parent(self):
292
502
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
293
503
new_parent_trees = []
504
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
294
505
for revision_id in new_parents:
296
507
tree = self.revision_tree(revision_id)
587
794
# making sure we haven't missed any corner cases.
588
795
# if lca_trees is None:
589
796
# self._lca_trees = [self.base_tree]
592
797
self.change_reporter = change_reporter
593
798
self.cherrypick = cherrypick
595
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")
599
806
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
600
808
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
601
810
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
602
812
self.other_tree.lock_read()
603
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)
606
self._compute_transform()
608
results = self.tt.apply(no_conflicts=True)
609
self.write_modified(results)
611
self.this_tree.add_conflicts(self.cooked_conflicts)
612
except UnsupportedOperation:
616
self.other_tree.unlock()
617
self.base_tree.unlock()
618
self.this_tree.unlock()
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
621
827
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
622
829
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
623
831
self.other_tree.lock_read()
624
self.tt = TransformPreview(self.this_tree)
627
self._compute_transform()
630
self.other_tree.unlock()
631
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()
635
840
def _compute_transform(self):
1071
1295
parent_id_winner = "other"
1072
1296
if name_winner == "this" and parent_id_winner == "this":
1074
if name_winner == "conflict":
1075
trans_id = self.tt.trans_id_file_id(file_id)
1076
self._raw_conflicts.append(('name conflict', trans_id,
1077
this_name, other_name))
1078
if parent_id_winner == "conflict":
1079
trans_id = self.tt.trans_id_file_id(file_id)
1080
self._raw_conflicts.append(('parent conflict', trans_id,
1081
this_parent, other_parent))
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))
1082
1306
if other_name is None:
1083
1307
# it doesn't matter whether the result was 'other' or
1084
1308
# 'conflict'-- if there's no 'other', we leave it alone.
1086
# if we get here, name_winner and parent_winner are set to safe values.
1087
trans_id = self.tt.trans_id_file_id(file_id)
1088
1310
parent_id = parents[self.winner_idx[parent_id_winner]]
1089
1311
if parent_id is not None:
1090
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1312
# if we get here, name_winner and parent_winner are set to safe
1091
1314
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1092
parent_trans_id, trans_id)
1315
self.tt.trans_id_file_id(parent_id),
1316
self.tt.trans_id_file_id(file_id))
1094
def merge_contents(self, file_id):
1318
def _do_merge_contents(self, file_id):
1095
1319
"""Performs a merge on file_id contents."""
1096
1320
def contents_pair(tree):
1097
1321
if file_id not in tree:
1138
1350
if winner == 'this':
1139
1351
# No interesting changes introduced by OTHER
1140
1352
return "unmodified"
1353
# We have a hypothetical conflict, but if we have files, then we
1354
# can try to merge the content
1141
1355
trans_id = self.tt.trans_id_file_id(file_id)
1142
if winner == 'other':
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':
1143
1441
# OTHER is a straight winner, so replace this contents with other
1144
file_in_this = file_id in self.this_tree
1146
# Remove any existing contents
1147
self.tt.delete_contents(trans_id)
1148
if file_id in self.other_tree:
1149
# OTHER changed the file
1150
create_from_tree(self.tt, trans_id,
1151
self.other_tree, file_id)
1152
if not file_in_this:
1153
self.tt.version_file(file_id, trans_id)
1156
# OTHER deleted the file
1157
self.tt.unversion_file(trans_id)
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
1160
# We have a hypothetical conflict, but if we have files, then we
1161
# can try to merge the content
1162
if this_pair[0] == 'file' and other_pair[0] == 'file':
1163
# THIS and OTHER are both files, so text merge. Either
1164
# BASE is a file, or both converted to files, so at least we
1165
# have agreement that output should be a file.
1167
self.text_merge(file_id, trans_id)
1169
return contents_conflict()
1170
if file_id not in self.this_tree:
1171
self.tt.version_file(file_id, trans_id)
1173
self.tt.tree_kind(trans_id)
1174
self.tt.delete_contents(trans_id)
1179
return contents_conflict()
1454
return 'not_applicable', None
1181
1456
def get_lines(self, tree, file_id):
1182
1457
"""Return the lines in a file, or an empty list."""
1184
return tree.get_file(file_id).readlines()
1458
if tree.has_id(file_id):
1459
return tree.get_file_lines(file_id)
1328
1627
if path.endswith(suffix):
1329
1628
path = path[:-len(suffix)]
1331
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1332
self.cooked_conflicts.append(c)
1333
if conflict_type == 'text conflict':
1630
c = _mod_conflicts.Conflict.factory(conflict_type,
1631
path=path, file_id=file_id)
1632
elif conflict_type == 'text conflict':
1334
1633
trans_id = conflict[1]
1335
1634
path = fp.get_path(trans_id)
1336
1635
file_id = self.tt.final_file_id(trans_id)
1337
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1338
self.cooked_conflicts.append(c)
1340
for trans_id, conflicts in name_conflicts.iteritems():
1342
this_parent, other_parent = conflicts['parent conflict']
1343
if this_parent == other_parent:
1344
raise AssertionError()
1346
this_parent = other_parent = \
1347
self.tt.final_file_id(self.tt.final_parent(trans_id))
1349
this_name, other_name = conflicts['name conflict']
1350
if this_name == other_name:
1351
raise AssertionError()
1353
this_name = other_name = self.tt.final_name(trans_id)
1354
other_path = fp.get_path(trans_id)
1355
if this_parent is not None and this_name is not None:
1356
this_parent_path = \
1357
fp.get_path(self.tt.trans_id_file_id(this_parent))
1358
this_path = pathjoin(this_parent_path, this_name)
1636
c = _mod_conflicts.Conflict.factory(conflict_type,
1637
path=path, file_id=file_id)
1360
this_path = "<deleted>"
1361
file_id = self.tt.final_file_id(trans_id)
1362
c = Conflict.factory('path conflict', path=this_path,
1363
conflict_path=other_path, file_id=file_id)
1639
raise AssertionError('bad conflict type: %r' % (conflict,))
1364
1640
self.cooked_conflicts.append(c)
1365
self.cooked_conflicts.sort(key=Conflict.sort_key)
1641
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1368
1644
class WeaveMerger(Merge3Merger):
1372
1648
supports_reverse_cherrypick = False
1373
1649
history_based = True
1375
def _merged_lines(self, file_id):
1376
"""Generate the merged lines.
1377
There is no distinction between lines that are meant to contain <<<<<<<
1381
base = self.base_tree
1384
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1651
def _generate_merge_plan(self, file_id, base):
1652
return 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)
1386
1665
if 'merge' in debug.debug_flags:
1387
1666
plan = list(plan)
1388
1667
trans_id = self.tt.trans_id_file_id(file_id)
1389
1668
name = self.tt.final_name(trans_id) + '.plan'
1390
contents = ('%10s|%s' % l for l in plan)
1669
contents = ('%11s|%s' % l for l in plan)
1391
1670
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1394
return textmerge.merge_lines(self.reprocess)
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
1396
1680
def text_merge(self, file_id, trans_id):
1397
1681
"""Perform a (weave) text merge for a given file and file-id.
1398
1682
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1399
1683
and a conflict will be noted.
1401
lines, conflicts = self._merged_lines(file_id)
1685
lines, base_lines = self._merged_lines(file_id)
1402
1686
lines = list(lines)
1403
1687
# Note we're checking whether the OUTPUT is binary in this case,
1404
1688
# because we don't want to get into weave merge guts.
1405
check_text_lines(lines)
1689
textfile.check_text_lines(lines)
1406
1690
self.tt.create_file(lines, trans_id)
1691
if base_lines is not None:
1408
1693
self._raw_conflicts.append(('text conflict', trans_id))
1409
1694
name = self.tt.final_name(trans_id)
1410
1695
parent_id = self.tt.final_parent(trans_id)
1411
1696
file_group = self._dump_conflicts(name, parent_id, file_id,
1698
base_lines=base_lines)
1413
1699
file_group.append(trans_id)
1416
1702
class LCAMerger(WeaveMerger):
1418
def _merged_lines(self, file_id):
1419
"""Generate the merged lines.
1420
There is no distinction between lines that are meant to contain <<<<<<<
1424
base = self.base_tree
1427
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1704
def _generate_merge_plan(self, file_id, base):
1705
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1429
if 'merge' in debug.debug_flags:
1431
trans_id = self.tt.trans_id_file_id(file_id)
1432
name = self.tt.final_name(trans_id) + '.plan'
1433
contents = ('%10s|%s' % l for l in plan)
1434
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1435
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1436
'>>>>>>> MERGE-SOURCE\n')
1437
return textmerge.merge_lines(self.reprocess)
1440
1708
class Diff3Merger(Merge3Merger):
1441
1709
"""Three-way merger using external diff3 for text merging"""
1443
1711
def dump_file(self, temp_dir, name, tree, file_id):
1444
out_path = pathjoin(temp_dir, name)
1712
out_path = osutils.pathjoin(temp_dir, name)
1445
1713
out_file = open(out_path, "wb")
1447
1715
in_file = tree.get_file(file_id)
1480
1748
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)
1483
1913
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1484
1914
backup_files=False,
1485
1915
merge_type=Merge3Merger,