14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from itertools import chain
19
from bzrlib.lazy_import import lazy_import
20
lazy_import(globals(), """
21
23
from bzrlib import (
22
branch as _mod_branch,
23
conflicts as _mod_conflicts,
26
26
graph as _mod_graph,
30
30
revision as _mod_revision,
40
from bzrlib.cleanup import OperationWithCleanups
47
from bzrlib.symbol_versioning import (
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_from_tree,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
51
63
# TODO: Report back as changes are merged in
54
66
def transform_tree(from_tree, to_tree, interesting_ids=None):
55
from_tree.lock_tree_write()
56
operation = OperationWithCleanups(merge_inner)
57
operation.add_cleanup(from_tree.unlock)
58
operation.run_simple(from_tree.branch, to_tree, from_tree,
59
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
62
class MergeHooks(hooks.Hooks):
65
hooks.Hooks.__init__(self)
66
self.create_hook(hooks.HookPoint('merge_file_content',
67
"Called with a bzrlib.merge.Merger object to create a per file "
68
"merge object when starting a merge. "
69
"Should return either None or a subclass of "
70
"``bzrlib.merge.AbstractPerFileMerger``. "
71
"Such objects will then be called per file "
72
"that needs to be merged (including when one "
73
"side has deleted the file and the other has changed it). "
74
"See the AbstractPerFileMerger API docs for details on how it is "
79
class AbstractPerFileMerger(object):
80
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
82
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
84
:ivar merger: The Merge3Merger performing the merge.
87
def __init__(self, merger):
88
"""Create a PerFileMerger for use with merger."""
91
def merge_contents(self, merge_params):
92
"""Attempt to merge the contents of a single file.
94
:param merge_params: A bzrlib.merge.MergeHookParams
95
:return : A tuple of (status, chunks), where status is one of
96
'not_applicable', 'success', 'conflicted', or 'delete'. If status
97
is 'success' or 'conflicted', then chunks should be an iterable of
98
strings for the new file contents.
100
return ('not applicable', None)
103
class PerFileMerger(AbstractPerFileMerger):
104
"""Merge individual files when self.file_matches returns True.
106
This class is intended to be subclassed. The file_matches and
107
merge_matching methods should be overridden with concrete implementations.
110
def file_matches(self, params):
111
"""Return True if merge_matching should be called on this file.
113
Only called with merges of plain files with no clear winner.
115
Subclasses must override this.
117
raise NotImplementedError(self.file_matches)
119
def get_filename(self, params, tree):
120
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
121
self.merger.this_tree) and a MergeHookParams.
123
return osutils.basename(tree.id2path(params.file_id))
125
def get_filepath(self, params, tree):
126
"""Calculate the path to the file in a tree.
128
:param params: A MergeHookParams describing the file to merge
129
:param tree: a Tree, e.g. self.merger.this_tree.
131
return tree.id2path(params.file_id)
133
def merge_contents(self, params):
134
"""Merge the contents of a single file."""
135
# Check whether this custom merge logic should be used.
137
# OTHER is a straight winner, rely on default merge.
138
params.winner == 'other' or
139
# THIS and OTHER aren't both files.
140
not params.is_file_merge() or
141
# The filename doesn't match *.xml
142
not self.file_matches(params)):
143
return 'not_applicable', None
144
return self.merge_matching(params)
146
def merge_matching(self, params):
147
"""Merge the contents of a single file that has matched the criteria
148
in PerFileMerger.merge_contents (is a conflict, is a file,
149
self.file_matches is True).
151
Subclasses must override this.
153
raise NotImplementedError(self.merge_matching)
156
class ConfigurableFileMerger(PerFileMerger):
157
"""Merge individual files when configured via a .conf file.
159
This is a base class for concrete custom file merging logic. Concrete
160
classes should implement ``merge_text``.
162
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
164
:ivar affected_files: The configured file paths to merge.
166
:cvar name_prefix: The prefix to use when looking up configuration
167
details. <name_prefix>_merge_files describes the files targeted by the
170
:cvar default_files: The default file paths to merge when no configuration
177
def __init__(self, merger):
178
super(ConfigurableFileMerger, self).__init__(merger)
179
self.affected_files = None
180
self.default_files = self.__class__.default_files or []
181
self.name_prefix = self.__class__.name_prefix
182
if self.name_prefix is None:
183
raise ValueError("name_prefix must be set.")
185
def file_matches(self, params):
186
"""Check whether the file should call the merge hook.
188
<name_prefix>_merge_files configuration variable is a list of files
189
that should use the hook.
191
affected_files = self.affected_files
192
if affected_files is None:
193
config = self.merger.this_branch.get_config()
194
# Until bzr provides a better policy for caching the config, we
195
# just add the part we're interested in to the params to avoid
196
# reading the config files repeatedly (bazaar.conf, location.conf,
198
config_key = self.name_prefix + '_merge_files'
199
affected_files = config.get_user_option_as_list(config_key)
200
if affected_files is None:
201
# If nothing was specified in the config, use the default.
202
affected_files = self.default_files
203
self.affected_files = affected_files
205
filepath = self.get_filepath(params, self.merger.this_tree)
206
if filepath in affected_files:
210
def merge_matching(self, params):
211
return self.merge_text(params)
213
def merge_text(self, params):
214
"""Merge the byte contents of a single file.
216
This is called after checking that the merge should be performed in
217
merge_contents, and it should behave as per
218
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
220
raise NotImplementedError(self.merge_text)
223
class MergeHookParams(object):
224
"""Object holding parameters passed to merge_file_content hooks.
226
There are some fields hooks can access:
228
:ivar file_id: the file ID of the file being merged
229
:ivar trans_id: the transform ID for the merge of this file
230
:ivar this_kind: kind of file_id in 'this' tree
231
:ivar other_kind: kind of file_id in 'other' tree
232
:ivar winner: one of 'this', 'other', 'conflict'
235
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
237
self._merger = merger
238
self.file_id = file_id
239
self.trans_id = trans_id
240
self.this_kind = this_kind
241
self.other_kind = other_kind
244
def is_file_merge(self):
245
"""True if this_kind and other_kind are both 'file'."""
246
return self.this_kind == 'file' and self.other_kind == 'file'
248
@decorators.cachedproperty
249
def base_lines(self):
250
"""The lines of the 'base' version of the file."""
251
return self._merger.get_lines(self._merger.base_tree, self.file_id)
253
@decorators.cachedproperty
254
def this_lines(self):
255
"""The lines of the 'this' version of the file."""
256
return self._merger.get_lines(self._merger.this_tree, self.file_id)
258
@decorators.cachedproperty
259
def other_lines(self):
260
"""The lines of the 'other' version of the file."""
261
return self._merger.get_lines(self._merger.other_tree, self.file_id)
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
264
71
class Merger(object):
268
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
269
73
this_tree=None, pb=None, change_reporter=None,
270
74
recurse='down', revision_graph=None):
464
257
if self.this_rev_id is None:
465
258
if self.this_basis_tree.get_file_sha1(file_id) != \
466
259
self.this_tree.get_file_sha1(file_id):
467
raise errors.WorkingTreeNotRevision(self.this_tree)
260
raise WorkingTreeNotRevision(self.this_tree)
469
262
trees = (self.this_basis_tree, self.other_tree)
470
263
return [get_id(tree, file_id) for tree in trees]
472
@deprecated_method(deprecated_in((2, 1, 0)))
473
265
def check_basis(self, check_clean, require_commits=True):
474
266
if self.this_basis is None and require_commits is True:
475
raise errors.BzrCommandError(
476
"This branch has no commits."
477
" (perhaps you would prefer 'bzr pull')")
267
raise BzrCommandError("This branch has no commits."
268
" (perhaps you would prefer 'bzr pull')")
479
270
self.compare_basis()
480
271
if self.this_basis != self.this_rev_id:
481
272
raise errors.UncommittedChanges(self.this_tree)
483
@deprecated_method(deprecated_in((2, 1, 0)))
484
274
def compare_basis(self):
486
276
basis_tree = self.revision_tree(self.this_tree.last_revision())
487
277
except errors.NoSuchRevision:
488
278
basis_tree = self.this_tree.basis_tree()
489
if not self.this_tree.has_changes(basis_tree):
279
changes = self.this_tree.changes_from(basis_tree)
280
if not changes.has_changed():
490
281
self.this_rev_id = self.this_basis
492
283
def set_interesting_files(self, file_list):
493
284
self.interesting_files = file_list
495
286
def set_pending(self):
496
if (not self.base_is_ancestor or not self.base_is_other_ancestor
497
or self.other_rev_id is None):
287
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
499
289
self._add_parent()
501
291
def _add_parent(self):
502
292
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
503
293
new_parent_trees = []
504
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
505
294
for revision_id in new_parents:
507
296
tree = self.revision_tree(revision_id)
794
587
# making sure we haven't missed any corner cases.
795
588
# if lca_trees is None:
796
589
# self._lca_trees = [self.base_tree]
797
592
self.change_reporter = change_reporter
798
593
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")
806
599
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
808
600
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
810
601
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
812
602
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)
603
self.tt = TreeTransform(self.this_tree, self.pb)
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
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()
827
621
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
829
622
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
831
623
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()
624
self.tt = TransformPreview(self.this_tree)
627
self._compute_transform()
630
self.other_tree.unlock()
631
self.base_tree.unlock()
840
635
def _compute_transform(self):
1295
1071
parent_id_winner = "other"
1296
1072
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))
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))
1306
1082
if other_name is None:
1307
1083
# it doesn't matter whether the result was 'other' or
1308
1084
# '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)
1310
1088
parent_id = parents[self.winner_idx[parent_id_winner]]
1311
1089
if parent_id is not None:
1312
# if we get here, name_winner and parent_winner are set to safe
1090
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1314
1091
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))
1092
parent_trans_id, trans_id)
1318
def _do_merge_contents(self, file_id):
1094
def merge_contents(self, file_id):
1319
1095
"""Performs a merge on file_id contents."""
1320
1096
def contents_pair(tree):
1321
1097
if file_id not in tree:
1350
1138
if winner == 'this':
1351
1139
# No interesting changes introduced by OTHER
1352
1140
return "unmodified"
1353
# We have a hypothetical conflict, but if we have files, then we
1354
# can try to merge the content
1355
1141
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':
1142
if winner == 'other':
1441
1143
# 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
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)
1454
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()
1456
1181
def get_lines(self, tree, file_id):
1457
1182
"""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)
1184
return tree.get_file(file_id).readlines()
1627
1328
if path.endswith(suffix):
1628
1329
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':
1331
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1332
self.cooked_conflicts.append(c)
1333
if conflict_type == 'text conflict':
1633
1334
trans_id = conflict[1]
1634
1335
path = fp.get_path(trans_id)
1635
1336
file_id = self.tt.final_file_id(trans_id)
1636
c = _mod_conflicts.Conflict.factory(conflict_type,
1637
path=path, file_id=file_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)
1639
raise AssertionError('bad conflict type: %r' % (conflict,))
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)
1640
1364
self.cooked_conflicts.append(c)
1641
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1365
self.cooked_conflicts.sort(key=Conflict.sort_key)
1644
1368
class WeaveMerger(Merge3Merger):
1648
1372
supports_reverse_cherrypick = False
1649
1373
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,
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,
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
1386
if 'merge' in debug.debug_flags:
1666
1387
plan = list(plan)
1667
1388
trans_id = self.tt.trans_id_file_id(file_id)
1668
1389
name = self.tt.final_name(trans_id) + '.plan'
1669
contents = ('%11s|%s' % l for l in plan)
1390
contents = ('%10s|%s' % l for l in plan)
1670
1391
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
1392
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1393
'>>>>>>> MERGE-SOURCE\n')
1394
return textmerge.merge_lines(self.reprocess)
1680
1396
def text_merge(self, file_id, trans_id):
1681
1397
"""Perform a (weave) text merge for a given file and file-id.
1682
1398
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1683
1399
and a conflict will be noted.
1685
lines, base_lines = self._merged_lines(file_id)
1401
lines, conflicts = self._merged_lines(file_id)
1686
1402
lines = list(lines)
1687
1403
# Note we're checking whether the OUTPUT is binary in this case,
1688
1404
# because we don't want to get into weave merge guts.
1689
textfile.check_text_lines(lines)
1405
check_text_lines(lines)
1690
1406
self.tt.create_file(lines, trans_id)
1691
if base_lines is not None:
1693
1408
self._raw_conflicts.append(('text conflict', trans_id))
1694
1409
name = self.tt.final_name(trans_id)
1695
1410
parent_id = self.tt.final_parent(trans_id)
1696
1411
file_group = self._dump_conflicts(name, parent_id, file_id,
1698
base_lines=base_lines)
1699
1413
file_group.append(trans_id)
1702
1416
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,
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,
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)
1708
1440
class Diff3Merger(Merge3Merger):
1709
1441
"""Three-way merger using external diff3 for text merging"""
1711
1443
def dump_file(self, temp_dir, name, tree, file_id):
1712
out_path = osutils.pathjoin(temp_dir, name)
1444
out_path = pathjoin(temp_dir, name)
1713
1445
out_file = open(out_path, "wb")
1715
1447
in_file = tree.get_file(file_id)
1748
1480
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
1483
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1914
1484
backup_files=False,
1915
1485
merge_type=Merge3Merger,