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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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
this_tree=None, pb=None, change_reporter=None,
73
this_tree=None, pb=DummyProgress(), change_reporter=None,
270
74
recurse='down', revision_graph=None):
271
75
object.__init__(self)
272
76
self.this_branch = this_branch
464
255
if self.this_rev_id is None:
465
256
if self.this_basis_tree.get_file_sha1(file_id) != \
466
257
self.this_tree.get_file_sha1(file_id):
467
raise errors.WorkingTreeNotRevision(self.this_tree)
258
raise WorkingTreeNotRevision(self.this_tree)
469
260
trees = (self.this_basis_tree, self.other_tree)
470
261
return [get_id(tree, file_id) for tree in trees]
472
@deprecated_method(deprecated_in((2, 1, 0)))
473
263
def check_basis(self, check_clean, require_commits=True):
474
264
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')")
265
raise BzrCommandError("This branch has no commits."
266
" (perhaps you would prefer 'bzr pull')")
479
268
self.compare_basis()
480
269
if self.this_basis != self.this_rev_id:
481
270
raise errors.UncommittedChanges(self.this_tree)
483
@deprecated_method(deprecated_in((2, 1, 0)))
484
272
def compare_basis(self):
486
274
basis_tree = self.revision_tree(self.this_tree.last_revision())
487
275
except errors.NoSuchRevision:
488
276
basis_tree = self.this_tree.basis_tree()
489
if not self.this_tree.has_changes(basis_tree):
277
changes = self.this_tree.changes_from(basis_tree)
278
if not changes.has_changed():
490
279
self.this_rev_id = self.this_basis
492
281
def set_interesting_files(self, file_list):
493
282
self.interesting_files = file_list
495
284
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):
285
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
499
287
self._add_parent()
501
289
def _add_parent(self):
502
290
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
503
291
new_parent_trees = []
504
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
505
292
for revision_id in new_parents:
507
294
tree = self.revision_tree(revision_id)
740
534
winner_idx = {"this": 2, "other": 1, "conflict": 1}
741
535
supports_lca_trees = True
743
def __init__(self, working_tree, this_tree, base_tree, other_tree,
537
def __init__(self, working_tree, this_tree, base_tree, other_tree,
744
538
interesting_ids=None, reprocess=False, show_base=False,
745
pb=None, pp=None, change_reporter=None,
539
pb=DummyProgress(), pp=None, change_reporter=None,
746
540
interesting_files=None, do_merge=True,
747
cherrypick=False, lca_trees=None, this_branch=None):
541
cherrypick=False, lca_trees=None):
748
542
"""Initialize the merger object and perform the merge.
750
544
:param working_tree: The working tree to apply the merge to
751
545
:param this_tree: The local tree in the merge operation
752
546
:param base_tree: The common tree in the merge operation
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.
547
:param other_tree: The other other tree to merge changes from
756
548
:param interesting_ids: The file_ids of files that should be
757
549
participate in the merge. May not be combined with
758
550
interesting_files.
759
551
:param: reprocess If True, perform conflict-reduction processing.
760
552
:param show_base: If True, show the base revision in text conflicts.
761
553
(incompatible with reprocess)
554
:param pb: A Progress bar
763
555
:param pp: A ProgressPhase object
764
556
:param change_reporter: An object that should report changes made
765
557
:param interesting_files: The tree-relative paths of files that should
794
583
# making sure we haven't missed any corner cases.
795
584
# if lca_trees is None:
796
585
# self._lca_trees = [self.base_tree]
797
588
self.change_reporter = change_reporter
798
589
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")
806
595
def do_merge(self):
807
operation = OperationWithCleanups(self._do_merge)
808
596
self.this_tree.lock_tree_write()
809
operation.add_cleanup(self.this_tree.unlock)
810
597
self.base_tree.lock_read()
811
operation.add_cleanup(self.base_tree.unlock)
812
598
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)
599
self.tt = TreeTransform(self.this_tree, self.pb)
823
self.this_tree.add_conflicts(self.cooked_conflicts)
824
except errors.UnsupportedOperation:
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()
827
617
def make_preview_transform(self):
828
operation = OperationWithCleanups(self._make_preview_transform)
829
618
self.base_tree.lock_read()
830
operation.add_cleanup(self.base_tree.unlock)
831
619
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()
620
self.tt = TransformPreview(self.this_tree)
623
self._compute_transform()
626
self.other_tree.unlock()
627
self.base_tree.unlock()
840
631
def _compute_transform(self):
1098
882
other_root = self.tt.trans_id_file_id(other_root_file_id)
1099
883
if other_root == self.tt.root:
886
self.tt.final_kind(other_root)
1101
889
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)
890
# the other tree's root is a non-root in the current tree
1106
self.tt.final_kind(other_root)
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():
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():
1116
898
trans_id = self.tt.trans_id_file_id(child.file_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)
899
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1134
901
def write_modified(self, results):
1135
902
modified_hashes = {}
1303
1067
parent_id_winner = "other"
1304
1068
if name_winner == "this" and parent_id_winner == "this":
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))
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))
1314
1078
if other_name is None:
1315
# it doesn't matter whether the result was 'other' or
1079
# it doesn't matter whether the result was 'other' or
1316
1080
# '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)
1318
1084
parent_id = parents[self.winner_idx[parent_id_winner]]
1319
1085
if parent_id is not None:
1320
# if we get here, name_winner and parent_winner are set to safe
1086
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1322
1087
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1323
self.tt.trans_id_file_id(parent_id),
1324
self.tt.trans_id_file_id(file_id))
1088
parent_trans_id, trans_id)
1326
def _do_merge_contents(self, file_id):
1090
def merge_contents(self, file_id):
1327
1091
"""Performs a merge on file_id contents."""
1328
1092
def contents_pair(tree):
1329
1093
if file_id not in tree:
1358
1134
if winner == 'this':
1359
1135
# No interesting changes introduced by OTHER
1360
1136
return "unmodified"
1361
# We have a hypothetical conflict, but if we have files, then we
1362
# can try to merge the content
1363
1137
trans_id = self.tt.trans_id_file_id(file_id)
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':
1138
if winner == 'other':
1452
1139
# OTHER is a straight winner, so replace this contents with other
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
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)
1465
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()
1467
1177
def get_lines(self, tree, file_id):
1468
1178
"""Return the lines in a file, or an empty list."""
1469
if tree.has_id(file_id):
1470
return tree.get_file_lines(file_id)
1180
return tree.get_file(file_id).readlines()
1523
1233
determined automatically. If set_version is true, the .OTHER, .THIS
1524
1234
or .BASE (in that order) will be created as versioned files.
1526
data = [('OTHER', self.other_tree, other_lines),
1236
data = [('OTHER', self.other_tree, other_lines),
1527
1237
('THIS', self.this_tree, this_lines)]
1528
1238
if not no_base:
1529
1239
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
1544
1240
versioned = False
1545
1241
file_group = []
1546
1242
for suffix, tree, lines in data:
1547
if tree.has_id(file_id):
1548
1244
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1549
suffix, lines, filter_tree_path)
1550
1246
file_group.append(trans_id)
1551
1247
if set_version and not versioned:
1552
1248
self.tt.version_file(file_id, trans_id)
1553
1249
versioned = True
1554
1250
return file_group
1556
1252
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1557
lines=None, filter_tree_path=None):
1558
1254
"""Emit a single conflict file."""
1559
1255
name = name + '.' + suffix
1560
1256
trans_id = self.tt.create_path(name, parent_id)
1561
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1257
create_from_tree(self.tt, trans_id, tree, file_id, lines)
1563
1258
return trans_id
1565
1260
def merge_executable(self, file_id, file_status):
1641
1324
if path.endswith(suffix):
1642
1325
path = path[:-len(suffix)]
1644
c = _mod_conflicts.Conflict.factory(conflict_type,
1645
path=path, file_id=file_id)
1646
elif conflict_type == 'text conflict':
1327
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1328
self.cooked_conflicts.append(c)
1329
if conflict_type == 'text conflict':
1647
1330
trans_id = conflict[1]
1648
1331
path = fp.get_path(trans_id)
1649
1332
file_id = self.tt.final_file_id(trans_id)
1650
c = _mod_conflicts.Conflict.factory(conflict_type,
1651
path=path, file_id=file_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)
1653
raise AssertionError('bad conflict type: %r' % (conflict,))
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)
1654
1360
self.cooked_conflicts.append(c)
1655
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1361
self.cooked_conflicts.sort(key=Conflict.sort_key)
1658
1364
class WeaveMerger(Merge3Merger):
1662
1368
supports_reverse_cherrypick = False
1663
1369
history_based = True
1665
def _generate_merge_plan(self, file_id, base):
1666
return self.this_tree.plan_file_merge(file_id, self.other_tree,
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,
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)
1679
1382
if 'merge' in debug.debug_flags:
1680
1383
plan = list(plan)
1681
1384
trans_id = self.tt.trans_id_file_id(file_id)
1682
1385
name = self.tt.final_name(trans_id) + '.plan'
1683
contents = ('%11s|%s' % l for l in plan)
1386
contents = ('%10s|%s' % l for l in plan)
1684
1387
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
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
1388
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1389
'>>>>>>> MERGE-SOURCE\n')
1390
return textmerge.merge_lines(self.reprocess)
1694
1392
def text_merge(self, file_id, trans_id):
1695
1393
"""Perform a (weave) text merge for a given file and file-id.
1696
1394
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1697
1395
and a conflict will be noted.
1699
lines, base_lines = self._merged_lines(file_id)
1397
lines, conflicts = self._merged_lines(file_id)
1700
1398
lines = list(lines)
1701
# Note we're checking whether the OUTPUT is binary in this case,
1399
# Note we're checking whether the OUTPUT is binary in this case,
1702
1400
# because we don't want to get into weave merge guts.
1703
textfile.check_text_lines(lines)
1401
check_text_lines(lines)
1704
1402
self.tt.create_file(lines, trans_id)
1705
if base_lines is not None:
1707
1404
self._raw_conflicts.append(('text conflict', trans_id))
1708
1405
name = self.tt.final_name(trans_id)
1709
1406
parent_id = self.tt.final_parent(trans_id)
1710
file_group = self._dump_conflicts(name, parent_id, file_id,
1712
base_lines=base_lines)
1407
file_group = self._dump_conflicts(name, parent_id, file_id,
1713
1409
file_group.append(trans_id)
1716
1412
class LCAMerger(WeaveMerger):
1718
def _generate_merge_plan(self, file_id, base):
1719
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
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,
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)
1722
1436
class Diff3Merger(Merge3Merger):
1723
1437
"""Three-way merger using external diff3 for text merging"""
1725
1439
def dump_file(self, temp_dir, name, tree, file_id):
1726
out_path = osutils.pathjoin(temp_dir, name)
1440
out_path = pathjoin(temp_dir, name)
1727
1441
out_file = open(out_path, "wb")
1729
1443
in_file = tree.get_file(file_id)
1762
1476
osutils.rmtree(temp_dir)
1765
class PathNotInTree(errors.BzrError):
1767
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1769
def __init__(self, path, tree):
1770
errors.BzrError.__init__(self, path=path, tree=tree)
1773
class MergeIntoMerger(Merger):
1774
"""Merger that understands other_tree will be merged into a subdir.
1776
This also changes the Merger api so that it uses real Branch, revision_id,
1777
and RevisonTree objects, rather than using revision specs.
1780
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1781
source_subpath, other_rev_id=None):
1782
"""Create a new MergeIntoMerger object.
1784
source_subpath in other_tree will be effectively copied to
1785
target_subdir in this_tree.
1787
:param this_tree: The tree that we will be merging into.
1788
:param other_branch: The Branch we will be merging from.
1789
:param other_tree: The RevisionTree object we want to merge.
1790
:param target_subdir: The relative path where we want to merge
1791
other_tree into this_tree
1792
:param source_subpath: The relative path specifying the subtree of
1793
other_tree to merge into this_tree.
1795
# It is assumed that we are merging a tree that is not in our current
1796
# ancestry, which means we are using the "EmptyTree" as our basis.
1797
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1798
_mod_revision.NULL_REVISION)
1799
super(MergeIntoMerger, self).__init__(
1800
this_branch=this_tree.branch,
1801
this_tree=this_tree,
1802
other_tree=other_tree,
1803
base_tree=null_ancestor_tree,
1805
self._target_subdir = target_subdir
1806
self._source_subpath = source_subpath
1807
self.other_branch = other_branch
1808
if other_rev_id is None:
1809
other_rev_id = other_tree.get_revision_id()
1810
self.other_rev_id = self.other_basis = other_rev_id
1811
self.base_is_ancestor = True
1812
self.backup_files = True
1813
self.merge_type = Merge3Merger
1814
self.show_base = False
1815
self.reprocess = False
1816
self.interesting_ids = None
1817
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1818
target_subdir=self._target_subdir,
1819
source_subpath=self._source_subpath)
1820
if self._source_subpath != '':
1821
# If this isn't a partial merge make sure the revisions will be
1823
self._maybe_fetch(self.other_branch, self.this_branch,
1826
def set_pending(self):
1827
if self._source_subpath != '':
1829
Merger.set_pending(self)
1832
class _MergeTypeParameterizer(object):
1833
"""Wrap a merge-type class to provide extra parameters.
1835
This is hack used by MergeIntoMerger to pass some extra parameters to its
1836
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1837
the 'merge_type' member. It is difficult override do_merge without
1838
re-writing the whole thing, so instead we create a wrapper which will pass
1839
the extra parameters.
1842
def __init__(self, merge_type, **kwargs):
1843
self._extra_kwargs = kwargs
1844
self._merge_type = merge_type
1846
def __call__(self, *args, **kwargs):
1847
kwargs.update(self._extra_kwargs)
1848
return self._merge_type(*args, **kwargs)
1850
def __getattr__(self, name):
1851
return getattr(self._merge_type, name)
1854
class MergeIntoMergeType(Merge3Merger):
1855
"""Merger that incorporates a tree (or part of a tree) into another."""
1857
def __init__(self, *args, **kwargs):
1858
"""Initialize the merger object.
1860
:param args: See Merge3Merger.__init__'s args.
1861
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1862
source_subpath and target_subdir.
1863
:keyword source_subpath: The relative path specifying the subtree of
1864
other_tree to merge into this_tree.
1865
:keyword target_subdir: The relative path where we want to merge
1866
other_tree into this_tree
1868
# All of the interesting work happens during Merge3Merger.__init__(),
1869
# so we have have to hack in to get our extra parameters set.
1870
self._source_subpath = kwargs.pop('source_subpath')
1871
self._target_subdir = kwargs.pop('target_subdir')
1872
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1874
def _compute_transform(self):
1875
child_pb = ui.ui_factory.nested_progress_bar()
1877
entries = self._entries_to_incorporate()
1878
entries = list(entries)
1879
for num, (entry, parent_id) in enumerate(entries):
1880
child_pb.update('Preparing file merge', num, len(entries))
1881
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1882
trans_id = transform.new_by_entry(self.tt, entry,
1883
parent_trans_id, self.other_tree)
1886
self._finish_computing_transform()
1888
def _entries_to_incorporate(self):
1889
"""Yields pairs of (inventory_entry, new_parent)."""
1890
other_inv = self.other_tree.inventory
1891
subdir_id = other_inv.path2id(self._source_subpath)
1892
if subdir_id is None:
1893
# XXX: The error would be clearer if it gave the URL of the source
1894
# branch, but we don't have a reference to that here.
1895
raise PathNotInTree(self._source_subpath, "Source tree")
1896
subdir = other_inv[subdir_id]
1897
parent_in_target = osutils.dirname(self._target_subdir)
1898
target_id = self.this_tree.inventory.path2id(parent_in_target)
1899
if target_id is None:
1900
raise PathNotInTree(self._target_subdir, "Target tree")
1901
name_in_target = osutils.basename(self._target_subdir)
1902
merge_into_root = subdir.copy()
1903
merge_into_root.name = name_in_target
1904
if merge_into_root.file_id in self.this_tree.inventory:
1905
# Give the root a new file-id.
1906
# This can happen fairly easily if the directory we are
1907
# incorporating is the root, and both trees have 'TREE_ROOT' as
1908
# their root_id. Users will expect this to Just Work, so we
1909
# change the file-id here.
1910
# Non-root file-ids could potentially conflict too. That's really
1911
# an edge case, so we don't do anything special for those. We let
1912
# them cause conflicts.
1913
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1914
yield (merge_into_root, target_id)
1915
if subdir.kind != 'directory':
1916
# No children, so we are done.
1918
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1919
parent_id = entry.parent_id
1920
if parent_id == subdir.file_id:
1921
# The root's parent ID has changed, so make sure children of
1922
# the root refer to the new ID.
1923
parent_id = merge_into_root.file_id
1924
yield (entry, parent_id)
1927
1479
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1928
1480
backup_files=False,
1929
1481
merge_type=Merge3Merger,