13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from itertools import chain
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
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
this_tree=None, pb=DummyProgress(), change_reporter=None,
269
this_tree=None, pb=None, change_reporter=None,
74
270
recurse='down', revision_graph=None):
75
271
object.__init__(self)
76
272
self.this_branch = this_branch
255
464
if self.this_rev_id is None:
256
465
if self.this_basis_tree.get_file_sha1(file_id) != \
257
466
self.this_tree.get_file_sha1(file_id):
258
raise WorkingTreeNotRevision(self.this_tree)
467
raise errors.WorkingTreeNotRevision(self.this_tree)
260
469
trees = (self.this_basis_tree, self.other_tree)
261
470
return [get_id(tree, file_id) for tree in trees]
472
@deprecated_method(deprecated_in((2, 1, 0)))
263
473
def check_basis(self, check_clean, require_commits=True):
264
474
if self.this_basis is None and require_commits is True:
265
raise BzrCommandError("This branch has no commits."
266
" (perhaps you would prefer 'bzr pull')")
475
raise errors.BzrCommandError(
476
"This branch has no commits."
477
" (perhaps you would prefer 'bzr pull')")
268
479
self.compare_basis()
269
480
if self.this_basis != self.this_rev_id:
270
481
raise errors.UncommittedChanges(self.this_tree)
483
@deprecated_method(deprecated_in((2, 1, 0)))
272
484
def compare_basis(self):
274
486
basis_tree = self.revision_tree(self.this_tree.last_revision())
275
487
except errors.NoSuchRevision:
276
488
basis_tree = self.this_tree.basis_tree()
277
changes = self.this_tree.changes_from(basis_tree)
278
if not changes.has_changed():
489
if not self.this_tree.has_changes(basis_tree):
279
490
self.this_rev_id = self.this_basis
281
492
def set_interesting_files(self, file_list):
282
493
self.interesting_files = file_list
284
495
def set_pending(self):
285
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):
287
499
self._add_parent()
289
501
def _add_parent(self):
290
502
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
291
503
new_parent_trees = []
504
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
292
505
for revision_id in new_parents:
294
507
tree = self.revision_tree(revision_id)
534
740
winner_idx = {"this": 2, "other": 1, "conflict": 1}
535
741
supports_lca_trees = True
537
def __init__(self, working_tree, this_tree, base_tree, other_tree,
743
def __init__(self, working_tree, this_tree, base_tree, other_tree,
538
744
interesting_ids=None, reprocess=False, show_base=False,
539
pb=DummyProgress(), pp=None, change_reporter=None,
745
pb=None, pp=None, change_reporter=None,
540
746
interesting_files=None, do_merge=True,
541
cherrypick=False, lca_trees=None):
747
cherrypick=False, lca_trees=None, this_branch=None):
542
748
"""Initialize the merger object and perform the merge.
544
750
:param working_tree: The working tree to apply the merge to
545
751
:param this_tree: The local tree in the merge operation
546
752
:param base_tree: The common tree in the merge operation
547
:param other_tree: The other other tree to merge changes from
753
:param other_tree: The other tree to merge changes from
754
:param this_branch: The branch associated with this_tree. Defaults to
755
this_tree.branch if not supplied.
548
756
:param interesting_ids: The file_ids of files that should be
549
757
participate in the merge. May not be combined with
550
758
interesting_files.
551
759
:param: reprocess If True, perform conflict-reduction processing.
552
760
:param show_base: If True, show the base revision in text conflicts.
553
761
(incompatible with reprocess)
554
:param pb: A Progress bar
555
763
:param pp: A ProgressPhase object
556
764
:param change_reporter: An object that should report changes made
557
765
:param interesting_files: The tree-relative paths of files that should
583
794
# making sure we haven't missed any corner cases.
584
795
# if lca_trees is None:
585
796
# self._lca_trees = [self.base_tree]
588
797
self.change_reporter = change_reporter
589
798
self.cherrypick = cherrypick
591
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")
595
806
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
596
808
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
597
810
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
598
812
self.other_tree.lock_read()
599
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)
602
self._compute_transform()
604
results = self.tt.apply(no_conflicts=True)
605
self.write_modified(results)
607
self.this_tree.add_conflicts(self.cooked_conflicts)
608
except UnsupportedOperation:
612
self.other_tree.unlock()
613
self.base_tree.unlock()
614
self.this_tree.unlock()
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
617
827
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
618
829
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
619
831
self.other_tree.lock_read()
620
self.tt = TransformPreview(self.this_tree)
623
self._compute_transform()
626
self.other_tree.unlock()
627
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()
631
840
def _compute_transform(self):
882
1098
other_root = self.tt.trans_id_file_id(other_root_file_id)
883
1099
if other_root == self.tt.root:
1101
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1102
# the other tree's root is a non-root in the current tree (as when
1103
# a previously unrelated branch is merged into another)
886
1106
self.tt.final_kind(other_root)
889
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
890
# the other tree's root is a non-root in the current tree
892
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
893
self.tt.cancel_creation(other_root)
894
self.tt.cancel_versioning(other_root)
896
def reparent_children(self, ie, target):
897
for thing, child in ie.children.iteritems():
1107
other_root_is_present = True
1108
except errors.NoSuchFile:
1109
# other_root doesn't have a physical representation. We still need
1110
# to move any references to the actual root of the tree.
1111
other_root_is_present = False
1112
# 'other_tree.inventory.root' is not present in this tree. We are
1113
# calling adjust_path for children which *want* to be present with a
1114
# correct place to go.
1115
for thing, child in self.other_tree.inventory.root.children.iteritems():
898
1116
trans_id = self.tt.trans_id_file_id(child.file_id)
899
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1117
if not other_root_is_present:
1118
# FIXME: Make final_kind returns None instead of raising
1119
# NoSuchFile to avoid the ugly construct below -- vila 20100402
1121
self.tt.final_kind(trans_id)
1122
# The item exist in the final tree and has a defined place
1125
except errors.NoSuchFile, e:
1127
# Move the item into the root
1128
self.tt.adjust_path(self.tt.final_name(trans_id),
1129
self.tt.root, trans_id)
1130
if other_root_is_present:
1131
self.tt.cancel_creation(other_root)
1132
self.tt.cancel_versioning(other_root)
901
1134
def write_modified(self, results):
902
1135
modified_hashes = {}
1067
1303
parent_id_winner = "other"
1068
1304
if name_winner == "this" and parent_id_winner == "this":
1070
if name_winner == "conflict":
1071
trans_id = self.tt.trans_id_file_id(file_id)
1072
self._raw_conflicts.append(('name conflict', trans_id,
1073
this_name, other_name))
1074
if parent_id_winner == "conflict":
1075
trans_id = self.tt.trans_id_file_id(file_id)
1076
self._raw_conflicts.append(('parent conflict', trans_id,
1077
this_parent, other_parent))
1306
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1307
# Creating helpers (.OTHER or .THIS) here cause problems down the
1308
# road if a ContentConflict needs to be created so we should not do
1310
trans_id = self.tt.trans_id_file_id(file_id)
1311
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1312
this_parent, this_name,
1313
other_parent, other_name))
1078
1314
if other_name is None:
1079
# it doesn't matter whether the result was 'other' or
1315
# it doesn't matter whether the result was 'other' or
1080
1316
# 'conflict'-- if there's no 'other', we leave it alone.
1082
# if we get here, name_winner and parent_winner are set to safe values.
1083
trans_id = self.tt.trans_id_file_id(file_id)
1084
1318
parent_id = parents[self.winner_idx[parent_id_winner]]
1085
1319
if parent_id is not None:
1086
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1320
# if we get here, name_winner and parent_winner are set to safe
1087
1322
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1088
parent_trans_id, trans_id)
1323
self.tt.trans_id_file_id(parent_id),
1324
self.tt.trans_id_file_id(file_id))
1090
def merge_contents(self, file_id):
1326
def _do_merge_contents(self, file_id):
1091
1327
"""Performs a merge on file_id contents."""
1092
1328
def contents_pair(tree):
1093
1329
if file_id not in tree:
1134
1358
if winner == 'this':
1135
1359
# No interesting changes introduced by OTHER
1136
1360
return "unmodified"
1361
# We have a hypothetical conflict, but if we have files, then we
1362
# can try to merge the content
1137
1363
trans_id = self.tt.trans_id_file_id(file_id)
1138
if winner == 'other':
1364
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1365
other_pair[0], winner)
1366
hooks = self.active_hooks
1367
hook_status = 'not_applicable'
1369
hook_status, lines = hook.merge_contents(params)
1370
if hook_status != 'not_applicable':
1371
# Don't try any more hooks, this one applies.
1374
if hook_status == 'not_applicable':
1375
# This is a contents conflict, because none of the available
1376
# functions could merge it.
1378
name = self.tt.final_name(trans_id)
1379
parent_id = self.tt.final_parent(trans_id)
1380
if self.this_tree.has_id(file_id):
1381
self.tt.unversion_file(trans_id)
1382
file_group = self._dump_conflicts(name, parent_id, file_id,
1384
self._raw_conflicts.append(('contents conflict', file_group))
1385
elif hook_status == 'success':
1386
self.tt.create_file(lines, trans_id)
1387
elif hook_status == 'conflicted':
1388
# XXX: perhaps the hook should be able to provide
1389
# the BASE/THIS/OTHER files?
1390
self.tt.create_file(lines, trans_id)
1391
self._raw_conflicts.append(('text conflict', trans_id))
1392
name = self.tt.final_name(trans_id)
1393
parent_id = self.tt.final_parent(trans_id)
1394
self._dump_conflicts(name, parent_id, file_id)
1395
elif hook_status == 'delete':
1396
self.tt.unversion_file(trans_id)
1398
elif hook_status == 'done':
1399
# The hook function did whatever it needs to do directly, no
1400
# further action needed here.
1403
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1404
if not self.this_tree.has_id(file_id) and result == "modified":
1405
self.tt.version_file(file_id, trans_id)
1406
# The merge has been performed, so the old contents should not be
1409
self.tt.delete_contents(trans_id)
1410
except errors.NoSuchFile:
1414
def _default_other_winner_merge(self, merge_hook_params):
1415
"""Replace this contents with other."""
1416
file_id = merge_hook_params.file_id
1417
trans_id = merge_hook_params.trans_id
1418
file_in_this = self.this_tree.has_id(file_id)
1419
if self.other_tree.has_id(file_id):
1420
# OTHER changed the file
1422
if wt.supports_content_filtering():
1423
# We get the path from the working tree if it exists.
1424
# That fails though when OTHER is adding a file, so
1425
# we fall back to the other tree to find the path if
1426
# it doesn't exist locally.
1428
filter_tree_path = wt.id2path(file_id)
1429
except errors.NoSuchId:
1430
filter_tree_path = self.other_tree.id2path(file_id)
1432
# Skip the id2path lookup for older formats
1433
filter_tree_path = None
1434
transform.create_from_tree(self.tt, trans_id,
1435
self.other_tree, file_id,
1436
filter_tree_path=filter_tree_path)
1439
# OTHER deleted the file
1440
return 'delete', None
1442
raise AssertionError(
1443
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1446
def merge_contents(self, merge_hook_params):
1447
"""Fallback merge logic after user installed hooks."""
1448
# This function is used in merge hooks as the fallback instance.
1449
# Perhaps making this function and the functions it calls be a
1450
# a separate class would be better.
1451
if merge_hook_params.winner == 'other':
1139
1452
# OTHER is a straight winner, so replace this contents with other
1140
file_in_this = file_id in self.this_tree
1142
# Remove any existing contents
1143
self.tt.delete_contents(trans_id)
1144
if file_id in self.other_tree:
1145
# OTHER changed the file
1146
create_from_tree(self.tt, trans_id,
1147
self.other_tree, file_id)
1148
if not file_in_this:
1149
self.tt.version_file(file_id, trans_id)
1152
# OTHER deleted the file
1153
self.tt.unversion_file(trans_id)
1453
return self._default_other_winner_merge(merge_hook_params)
1454
elif merge_hook_params.is_file_merge():
1455
# THIS and OTHER are both files, so text merge. Either
1456
# BASE is a file, or both converted to files, so at least we
1457
# have agreement that output should be a file.
1459
self.text_merge(merge_hook_params.file_id,
1460
merge_hook_params.trans_id)
1461
except errors.BinaryFile:
1462
return 'not_applicable', None
1156
# We have a hypothetical conflict, but if we have files, then we
1157
# can try to merge the content
1158
if this_pair[0] == 'file' and other_pair[0] == 'file':
1159
# THIS and OTHER are both files, so text merge. Either
1160
# BASE is a file, or both converted to files, so at least we
1161
# have agreement that output should be a file.
1163
self.text_merge(file_id, trans_id)
1165
return contents_conflict()
1166
if file_id not in self.this_tree:
1167
self.tt.version_file(file_id, trans_id)
1169
self.tt.tree_kind(trans_id)
1170
self.tt.delete_contents(trans_id)
1175
return contents_conflict()
1465
return 'not_applicable', None
1177
1467
def get_lines(self, tree, file_id):
1178
1468
"""Return the lines in a file, or an empty list."""
1180
return tree.get_file(file_id).readlines()
1469
if tree.has_id(file_id):
1470
return tree.get_file_lines(file_id)
1233
1523
determined automatically. If set_version is true, the .OTHER, .THIS
1234
1524
or .BASE (in that order) will be created as versioned files.
1236
data = [('OTHER', self.other_tree, other_lines),
1526
data = [('OTHER', self.other_tree, other_lines),
1237
1527
('THIS', self.this_tree, this_lines)]
1238
1528
if not no_base:
1239
1529
data.append(('BASE', self.base_tree, base_lines))
1531
# We need to use the actual path in the working tree of the file here,
1532
# ignoring the conflict suffixes
1534
if wt.supports_content_filtering():
1536
filter_tree_path = wt.id2path(file_id)
1537
except errors.NoSuchId:
1538
# file has been deleted
1539
filter_tree_path = None
1541
# Skip the id2path lookup for older formats
1542
filter_tree_path = None
1240
1544
versioned = False
1241
1545
file_group = []
1242
1546
for suffix, tree, lines in data:
1547
if tree.has_id(file_id):
1244
1548
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1549
suffix, lines, filter_tree_path)
1246
1550
file_group.append(trans_id)
1247
1551
if set_version and not versioned:
1248
1552
self.tt.version_file(file_id, trans_id)
1249
1553
versioned = True
1250
1554
return file_group
1252
1556
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1557
lines=None, filter_tree_path=None):
1254
1558
"""Emit a single conflict file."""
1255
1559
name = name + '.' + suffix
1256
1560
trans_id = self.tt.create_path(name, parent_id)
1257
create_from_tree(self.tt, trans_id, tree, file_id, lines)
1561
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1258
1563
return trans_id
1260
1565
def merge_executable(self, file_id, file_status):
1324
1641
if path.endswith(suffix):
1325
1642
path = path[:-len(suffix)]
1327
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1328
self.cooked_conflicts.append(c)
1329
if conflict_type == 'text conflict':
1644
c = _mod_conflicts.Conflict.factory(conflict_type,
1645
path=path, file_id=file_id)
1646
elif conflict_type == 'text conflict':
1330
1647
trans_id = conflict[1]
1331
1648
path = fp.get_path(trans_id)
1332
1649
file_id = self.tt.final_file_id(trans_id)
1333
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1334
self.cooked_conflicts.append(c)
1336
for trans_id, conflicts in name_conflicts.iteritems():
1338
this_parent, other_parent = conflicts['parent conflict']
1339
if this_parent == other_parent:
1340
raise AssertionError()
1342
this_parent = other_parent = \
1343
self.tt.final_file_id(self.tt.final_parent(trans_id))
1345
this_name, other_name = conflicts['name conflict']
1346
if this_name == other_name:
1347
raise AssertionError()
1349
this_name = other_name = self.tt.final_name(trans_id)
1350
other_path = fp.get_path(trans_id)
1351
if this_parent is not None and this_name is not None:
1352
this_parent_path = \
1353
fp.get_path(self.tt.trans_id_file_id(this_parent))
1354
this_path = pathjoin(this_parent_path, this_name)
1650
c = _mod_conflicts.Conflict.factory(conflict_type,
1651
path=path, file_id=file_id)
1356
this_path = "<deleted>"
1357
file_id = self.tt.final_file_id(trans_id)
1358
c = Conflict.factory('path conflict', path=this_path,
1359
conflict_path=other_path, file_id=file_id)
1653
raise AssertionError('bad conflict type: %r' % (conflict,))
1360
1654
self.cooked_conflicts.append(c)
1361
self.cooked_conflicts.sort(key=Conflict.sort_key)
1655
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1364
1658
class WeaveMerger(Merge3Merger):
1368
1662
supports_reverse_cherrypick = False
1369
1663
history_based = True
1371
def _merged_lines(self, file_id):
1372
"""Generate the merged lines.
1373
There is no distinction between lines that are meant to contain <<<<<<<
1377
base = self.base_tree
1380
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1665
def _generate_merge_plan(self, file_id, base):
1666
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1669
def _merged_lines(self, file_id):
1670
"""Generate the merged lines.
1671
There is no distinction between lines that are meant to contain <<<<<<<
1675
base = self.base_tree
1678
plan = self._generate_merge_plan(file_id, base)
1382
1679
if 'merge' in debug.debug_flags:
1383
1680
plan = list(plan)
1384
1681
trans_id = self.tt.trans_id_file_id(file_id)
1385
1682
name = self.tt.final_name(trans_id) + '.plan'
1386
contents = ('%10s|%s' % l for l in plan)
1683
contents = ('%11s|%s' % l for l in plan)
1387
1684
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1388
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1389
'>>>>>>> MERGE-SOURCE\n')
1390
return textmerge.merge_lines(self.reprocess)
1685
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1686
'>>>>>>> MERGE-SOURCE\n')
1687
lines, conflicts = textmerge.merge_lines(self.reprocess)
1689
base_lines = textmerge.base_from_plan()
1692
return lines, base_lines
1392
1694
def text_merge(self, file_id, trans_id):
1393
1695
"""Perform a (weave) text merge for a given file and file-id.
1394
1696
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1395
1697
and a conflict will be noted.
1397
lines, conflicts = self._merged_lines(file_id)
1699
lines, base_lines = self._merged_lines(file_id)
1398
1700
lines = list(lines)
1399
# Note we're checking whether the OUTPUT is binary in this case,
1701
# Note we're checking whether the OUTPUT is binary in this case,
1400
1702
# because we don't want to get into weave merge guts.
1401
check_text_lines(lines)
1703
textfile.check_text_lines(lines)
1402
1704
self.tt.create_file(lines, trans_id)
1705
if base_lines is not None:
1404
1707
self._raw_conflicts.append(('text conflict', trans_id))
1405
1708
name = self.tt.final_name(trans_id)
1406
1709
parent_id = self.tt.final_parent(trans_id)
1407
file_group = self._dump_conflicts(name, parent_id, file_id,
1710
file_group = self._dump_conflicts(name, parent_id, file_id,
1712
base_lines=base_lines)
1409
1713
file_group.append(trans_id)
1412
1716
class LCAMerger(WeaveMerger):
1414
def _merged_lines(self, file_id):
1415
"""Generate the merged lines.
1416
There is no distinction between lines that are meant to contain <<<<<<<
1420
base = self.base_tree
1423
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1718
def _generate_merge_plan(self, file_id, base):
1719
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1425
if 'merge' in debug.debug_flags:
1427
trans_id = self.tt.trans_id_file_id(file_id)
1428
name = self.tt.final_name(trans_id) + '.plan'
1429
contents = ('%10s|%s' % l for l in plan)
1430
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1431
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1432
'>>>>>>> MERGE-SOURCE\n')
1433
return textmerge.merge_lines(self.reprocess)
1436
1722
class Diff3Merger(Merge3Merger):
1437
1723
"""Three-way merger using external diff3 for text merging"""
1439
1725
def dump_file(self, temp_dir, name, tree, file_id):
1440
out_path = pathjoin(temp_dir, name)
1726
out_path = osutils.pathjoin(temp_dir, name)
1441
1727
out_file = open(out_path, "wb")
1443
1729
in_file = tree.get_file(file_id)
1476
1762
osutils.rmtree(temp_dir)
1765
class PathNotInTree(errors.BzrError):
1767
_fmt = """%(tree)s does not contain %(path)s."""
1769
def __init__(self, path, tree):
1770
errors.BzrError.__init__(self, path=path, tree=tree)
1773
class MergeIntoMerger(Merger):
1774
"""Merger that understands other_tree will be merged into a subdir.
1776
This also changes the Merger api so that it uses real Branch, revision_id,
1777
and RevisonTree objects, rather than using revision specs.
1780
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1781
source_subpath, other_rev_id=None):
1782
"""Create a new MergeIntoMerger object.
1784
source_subpath in other_tree will be effectively copied to
1785
target_subdir in this_tree.
1787
:param this_tree: The tree that we will be merging into.
1788
:param other_branch: The Branch we will be merging from.
1789
:param other_tree: The RevisionTree object we want to merge.
1790
:param target_subdir: The relative path where we want to merge
1791
other_tree into this_tree
1792
:param source_subpath: The relative path specifying the subtree of
1793
other_tree to merge into this_tree.
1795
# It is assumed that we are merging a tree that is not in our current
1796
# ancestry, which means we are using the "EmptyTree" as our basis.
1797
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1798
_mod_revision.NULL_REVISION)
1799
super(MergeIntoMerger, self).__init__(
1800
this_branch=this_tree.branch,
1801
this_tree=this_tree,
1802
other_tree=other_tree,
1803
base_tree=null_ancestor_tree,
1805
self._target_subdir = target_subdir
1806
self._source_subpath = source_subpath
1807
self.other_branch = other_branch
1808
if other_rev_id is None:
1809
other_rev_id = other_tree.get_revision_id()
1810
self.other_rev_id = self.other_basis = other_rev_id
1811
self.base_is_ancestor = True
1812
self.backup_files = True
1813
self.merge_type = Merge3Merger
1814
self.show_base = False
1815
self.reprocess = False
1816
self.interesting_ids = None
1817
self.merge_type = _Wrapper(Merge3MergeIntoMerger,
1818
target_subdir=self._target_subdir,
1819
source_subpath=self._source_subpath)
1820
if self._source_subpath != '':
1821
# If this isn't a partial merge make sure the revisions will be
1823
self._maybe_fetch(self.other_branch, self.this_branch,
1826
def set_pending(self):
1827
if self._source_subpath != '':
1829
Merger.set_pending(self)
1832
class _Wrapper(object):
1833
"""Wrap a class to provide extra parameters."""
1835
# Merger.do_merge() sets up its own set of parameters to pass to the
1836
# 'merge_type' member. And it is difficult override do_merge without
1837
# re-writing the whole thing, so instead we create a wrapper which will
1838
# pass the extra parameters.
1840
def __init__(self, merge_type, **kwargs):
1841
self._extra_kwargs = kwargs
1842
self._merge_type = merge_type
1844
def __call__(self, *args, **kwargs):
1845
kwargs.update(self._extra_kwargs)
1846
return self._merge_type(*args, **kwargs)
1848
def __getattr__(self, name):
1849
return getattr(self._merge_type, name)
1852
class Merge3MergeIntoMerger(Merge3Merger):
1853
"""Merger that incorporates a tree (or part of a tree) into another."""
1855
def __init__(self, *args, **kwargs):
1856
# All of the interesting work happens during Merge3Merger.__init__(),
1857
# so we have have to hack in to get our extra parameters set.
1858
self._source_subpath = kwargs.pop('source_subpath')
1859
self._target_subdir = kwargs.pop('target_subdir')
1860
super(Merge3MergeIntoMerger, self).__init__(*args, **kwargs)
1862
def _compute_transform(self):
1863
child_pb = ui.ui_factory.nested_progress_bar()
1865
entries = self._entries_to_incorporate()
1866
entries = list(entries)
1867
for num, (entry, parent_id) in enumerate(entries):
1868
child_pb.update('Preparing file merge', num, len(entries))
1869
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1870
trans_id = transform.new_by_entry(self.tt, entry,
1871
parent_trans_id, self.other_tree)
1874
self._finish_transform()
1876
def _entries_to_incorporate(self):
1877
"""Yields pairs of (inventory_entry, new_parent)."""
1878
other_inv = self.other_tree.inventory
1879
subdir_id = other_inv.path2id(self._source_subpath)
1880
if subdir_id is None:
1881
# XXX: The error would be clearer if it gave the URL of the source
1882
# branch, but we don't have a reference to that here.
1883
raise PathNotInTree(self._source_subpath, "Source tree")
1884
subdir = other_inv[subdir_id]
1885
parent_in_target = osutils.dirname(self._target_subdir)
1886
target_id = self.this_tree.inventory.path2id(parent_in_target)
1887
if target_id is None:
1888
raise PathNotInTree(self._target_subdir, "Target tree")
1889
name_in_target = osutils.basename(self._target_subdir)
1890
merge_into_root = subdir.copy()
1891
merge_into_root.name = name_in_target
1892
if merge_into_root.file_id in self.this_tree.inventory:
1893
# Give the root a new file-id.
1894
# This can happen fairly easily if the directory we are
1895
# incorporating is the root, and both trees have 'TREE_ROOT' as
1896
# their root_id. Users will expect this to Just Work, so we
1897
# change the file-id here.
1898
# Non-root file-ids could potentially conflict too. That's really
1899
# an edge case, so we don't do anything special for those. We let
1900
# them cause conflicts.
1901
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1902
yield (merge_into_root, target_id)
1903
if subdir.kind != 'directory':
1904
# No children, so we are done.
1906
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1907
parent_id = entry.parent_id
1908
if parent_id == subdir.file_id:
1909
# The root's parent ID has changed, so make sure children of
1910
# the root refer to the new ID.
1911
parent_id = merge_into_root.file_id
1912
yield (entry, parent_id)
1479
1915
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1480
1916
backup_files=False,
1481
1917
merge_type=Merge3Merger,