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
23
19
from bzrlib import (
20
branch as _mod_branch,
21
conflicts as _mod_conflicts,
26
25
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_by_entry,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
39
from bzrlib.cleanup import OperationWithCleanups
40
from bzrlib.symbol_versioning import (
63
44
# TODO: Report back as changes are merged in
66
47
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)
48
from_tree.lock_tree_write()
49
operation = OperationWithCleanups(merge_inner)
50
operation.add_cleanup(from_tree.unlock)
51
operation.run_simple(from_tree.branch, to_tree, from_tree,
52
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
55
class MergeHooks(hooks.Hooks):
58
hooks.Hooks.__init__(self)
59
self.create_hook(hooks.HookPoint('merge_file_content',
60
"Called with a bzrlib.merge.Merger object to create a per file "
61
"merge object when starting a merge. "
62
"Should return either None or a subclass of "
63
"``bzrlib.merge.AbstractPerFileMerger``. "
64
"Such objects will then be called per file "
65
"that needs to be merged (including when one "
66
"side has deleted the file and the other has changed it). "
67
"See the AbstractPerFileMerger API docs for details on how it is "
72
class AbstractPerFileMerger(object):
73
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
75
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
77
:ivar merger: The Merge3Merger performing the merge.
80
def __init__(self, merger):
81
"""Create a PerFileMerger for use with merger."""
84
def merge_contents(self, merge_params):
85
"""Attempt to merge the contents of a single file.
87
:param merge_params: A bzrlib.merge.MergeHookParams
88
:return : A tuple of (status, chunks), where status is one of
89
'not_applicable', 'success', 'conflicted', or 'delete'. If status
90
is 'success' or 'conflicted', then chunks should be an iterable of
91
strings for the new file contents.
93
return ('not applicable', None)
96
class ConfigurableFileMerger(AbstractPerFileMerger):
97
"""Merge individual files when configured via a .conf file.
99
This is a base class for concrete custom file merging logic. Concrete
100
classes should implement ``merge_text``.
102
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
104
:ivar affected_files: The configured file paths to merge.
106
:cvar name_prefix: The prefix to use when looking up configuration
107
details. <name_prefix>_merge_files describes the files targeted by the
110
:cvar default_files: The default file paths to merge when no configuration
117
def __init__(self, merger):
118
super(ConfigurableFileMerger, self).__init__(merger)
119
self.affected_files = None
120
self.default_files = self.__class__.default_files or []
121
self.name_prefix = self.__class__.name_prefix
122
if self.name_prefix is None:
123
raise ValueError("name_prefix must be set.")
125
def filename_matches_config(self, params):
126
"""Check whether the file should call the merge hook.
128
<name_prefix>_merge_files configuration variable is a list of files
129
that should use the hook.
131
affected_files = self.affected_files
132
if affected_files is None:
133
config = self.merger.this_branch.get_config()
134
# Until bzr provides a better policy for caching the config, we
135
# just add the part we're interested in to the params to avoid
136
# reading the config files repeatedly (bazaar.conf, location.conf,
138
config_key = self.name_prefix + '_merge_files'
139
affected_files = config.get_user_option_as_list(config_key)
140
if affected_files is None:
141
# If nothing was specified in the config, use the default.
142
affected_files = self.default_files
143
self.affected_files = affected_files
145
filename = self.merger.this_tree.id2path(params.file_id)
146
if filename in affected_files:
150
def merge_contents(self, params):
151
"""Merge the contents of a single file."""
152
# First, check whether this custom merge logic should be used. We
153
# expect most files should not be merged by this handler.
155
# OTHER is a straight winner, rely on default merge.
156
params.winner == 'other' or
157
# THIS and OTHER aren't both files.
158
not params.is_file_merge() or
159
# The filename isn't listed in the 'NAME_merge_files' config
161
not self.filename_matches_config(params)):
162
return 'not_applicable', None
163
return self.merge_text(params)
165
def merge_text(self, params):
166
"""Merge the byte contents of a single file.
168
This is called after checking that the merge should be performed in
169
merge_contents, and it should behave as per
170
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
172
raise NotImplementedError(self.merge_text)
175
class MergeHookParams(object):
176
"""Object holding parameters passed to merge_file_content hooks.
178
There are some fields hooks can access:
180
:ivar file_id: the file ID of the file being merged
181
:ivar trans_id: the transform ID for the merge of this file
182
:ivar this_kind: kind of file_id in 'this' tree
183
:ivar other_kind: kind of file_id in 'other' tree
184
:ivar winner: one of 'this', 'other', 'conflict'
187
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
189
self._merger = merger
190
self.file_id = file_id
191
self.trans_id = trans_id
192
self.this_kind = this_kind
193
self.other_kind = other_kind
196
def is_file_merge(self):
197
"""True if this_kind and other_kind are both 'file'."""
198
return self.this_kind == 'file' and self.other_kind == 'file'
200
@decorators.cachedproperty
201
def base_lines(self):
202
"""The lines of the 'base' version of the file."""
203
return self._merger.get_lines(self._merger.base_tree, self.file_id)
205
@decorators.cachedproperty
206
def this_lines(self):
207
"""The lines of the 'this' version of the file."""
208
return self._merger.get_lines(self._merger.this_tree, self.file_id)
210
@decorators.cachedproperty
211
def other_lines(self):
212
"""The lines of the 'other' version of the file."""
213
return self._merger.get_lines(self._merger.other_tree, self.file_id)
71
216
class Merger(object):
72
220
def __init__(self, this_branch, other_tree=None, base_tree=None,
73
this_tree=None, pb=DummyProgress(), change_reporter=None,
221
this_tree=None, pb=None, change_reporter=None,
74
222
recurse='down', revision_graph=None):
75
223
object.__init__(self)
76
224
self.this_branch = this_branch
247
417
if self.this_rev_id is None:
248
418
if self.this_basis_tree.get_file_sha1(file_id) != \
249
419
self.this_tree.get_file_sha1(file_id):
250
raise WorkingTreeNotRevision(self.this_tree)
420
raise errors.WorkingTreeNotRevision(self.this_tree)
252
422
trees = (self.this_basis_tree, self.other_tree)
253
423
return [get_id(tree, file_id) for tree in trees]
425
@deprecated_method(deprecated_in((2, 1, 0)))
255
426
def check_basis(self, check_clean, require_commits=True):
256
427
if self.this_basis is None and require_commits is True:
257
raise BzrCommandError("This branch has no commits."
258
" (perhaps you would prefer 'bzr pull')")
428
raise errors.BzrCommandError(
429
"This branch has no commits."
430
" (perhaps you would prefer 'bzr pull')")
260
432
self.compare_basis()
261
433
if self.this_basis != self.this_rev_id:
262
434
raise errors.UncommittedChanges(self.this_tree)
436
@deprecated_method(deprecated_in((2, 1, 0)))
264
437
def compare_basis(self):
266
439
basis_tree = self.revision_tree(self.this_tree.last_revision())
267
440
except errors.NoSuchRevision:
268
441
basis_tree = self.this_tree.basis_tree()
269
changes = self.this_tree.changes_from(basis_tree)
270
if not changes.has_changed():
442
if not self.this_tree.has_changes(basis_tree):
271
443
self.this_rev_id = self.this_basis
273
445
def set_interesting_files(self, file_list):
274
446
self.interesting_files = file_list
276
448
def set_pending(self):
277
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
449
if (not self.base_is_ancestor or not self.base_is_other_ancestor
450
or self.other_rev_id is None):
279
452
self._add_parent()
281
454
def _add_parent(self):
282
455
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
283
456
new_parent_trees = []
457
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
284
458
for revision_id in new_parents:
286
460
tree = self.revision_tree(revision_id)
524
693
winner_idx = {"this": 2, "other": 1, "conflict": 1}
525
694
supports_lca_trees = True
527
def __init__(self, working_tree, this_tree, base_tree, other_tree,
696
def __init__(self, working_tree, this_tree, base_tree, other_tree,
528
697
interesting_ids=None, reprocess=False, show_base=False,
529
pb=DummyProgress(), pp=None, change_reporter=None,
698
pb=None, pp=None, change_reporter=None,
530
699
interesting_files=None, do_merge=True,
531
cherrypick=False, lca_trees=None):
700
cherrypick=False, lca_trees=None, this_branch=None):
532
701
"""Initialize the merger object and perform the merge.
534
703
:param working_tree: The working tree to apply the merge to
535
704
:param this_tree: The local tree in the merge operation
536
705
:param base_tree: The common tree in the merge operation
537
:param other_tree: The other other tree to merge changes from
706
:param other_tree: The other tree to merge changes from
707
:param this_branch: The branch associated with this_tree
538
708
:param interesting_ids: The file_ids of files that should be
539
709
participate in the merge. May not be combined with
540
710
interesting_files.
541
711
:param: reprocess If True, perform conflict-reduction processing.
542
712
:param show_base: If True, show the base revision in text conflicts.
543
713
(incompatible with reprocess)
544
:param pb: A Progress bar
545
715
:param pp: A ProgressPhase object
546
716
:param change_reporter: An object that should report changes made
547
717
:param interesting_files: The tree-relative paths of files that should
573
744
# making sure we haven't missed any corner cases.
574
745
# if lca_trees is None:
575
746
# self._lca_trees = [self.base_tree]
578
747
self.change_reporter = change_reporter
579
748
self.cherrypick = cherrypick
581
self.pp = ProgressPhase("Merge phase", 3, self.pb)
752
warnings.warn("pp argument to Merge3Merger is deprecated")
754
warnings.warn("pb argument to Merge3Merger is deprecated")
585
756
def do_merge(self):
757
operation = OperationWithCleanups(self._do_merge)
586
758
self.this_tree.lock_tree_write()
759
operation.add_cleanup(self.this_tree.unlock)
587
760
self.base_tree.lock_read()
761
operation.add_cleanup(self.base_tree.unlock)
588
762
self.other_tree.lock_read()
589
self.tt = TreeTransform(self.this_tree, self.pb)
763
operation.add_cleanup(self.other_tree.unlock)
766
def _do_merge(self, operation):
767
self.tt = transform.TreeTransform(self.this_tree, None)
768
operation.add_cleanup(self.tt.finalize)
769
self._compute_transform()
770
results = self.tt.apply(no_conflicts=True)
771
self.write_modified(results)
592
self._compute_transform()
594
results = self.tt.apply(no_conflicts=True)
595
self.write_modified(results)
597
self.this_tree.add_conflicts(self.cooked_conflicts)
598
except UnsupportedOperation:
602
self.other_tree.unlock()
603
self.base_tree.unlock()
604
self.this_tree.unlock()
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
607
777
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
608
779
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
609
781
self.other_tree.lock_read()
610
self.tt = TransformPreview(self.this_tree)
613
self._compute_transform()
616
self.other_tree.unlock()
617
self.base_tree.unlock()
782
operation.add_cleanup(self.other_tree.unlock)
783
return operation.run_simple()
785
def _make_preview_transform(self):
786
self.tt = transform.TransformPreview(self.this_tree)
787
self._compute_transform()
621
790
def _compute_transform(self):
862
1031
def fix_root(self):
864
1033
self.tt.final_kind(self.tt.root)
1034
except errors.NoSuchFile:
866
1035
self.tt.cancel_deletion(self.tt.root)
867
1036
if self.tt.final_file_id(self.tt.root) is None:
868
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
1037
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
870
if self.other_tree.inventory.root is None:
872
1039
other_root_file_id = self.other_tree.get_root_id()
1040
if other_root_file_id is None:
873
1042
other_root = self.tt.trans_id_file_id(other_root_file_id)
874
1043
if other_root == self.tt.root:
1045
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1046
# the other tree's root is a non-root in the current tree (as when
1047
# a previously unrelated branch is merged into another)
877
1050
self.tt.final_kind(other_root)
880
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
881
# the other tree's root is a non-root in the current tree
883
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
884
self.tt.cancel_creation(other_root)
885
self.tt.cancel_versioning(other_root)
887
def reparent_children(self, ie, target):
888
for thing, child in ie.children.iteritems():
1051
other_root_is_present = True
1052
except errors.NoSuchFile:
1053
# other_root doesn't have a physical representation. We still need
1054
# to move any references to the actual root of the tree.
1055
other_root_is_present = False
1056
# 'other_tree.inventory.root' is not present in this tree. We are
1057
# calling adjust_path for children which *want* to be present with a
1058
# correct place to go.
1059
for thing, child in self.other_tree.inventory.root.children.iteritems():
889
1060
trans_id = self.tt.trans_id_file_id(child.file_id)
890
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1061
if not other_root_is_present:
1062
# FIXME: Make final_kind returns None instead of raising
1063
# NoSuchFile to avoid the ugly construct below -- vila 20100402
1065
self.tt.final_kind(trans_id)
1066
# The item exist in the final tree and has a defined place
1069
except errors.NoSuchFile, e:
1071
# Move the item into the root
1072
self.tt.adjust_path(self.tt.final_name(trans_id),
1073
self.tt.root, trans_id)
1074
if other_root_is_present:
1075
self.tt.cancel_creation(other_root)
1076
self.tt.cancel_versioning(other_root)
892
1078
def write_modified(self, results):
893
1079
modified_hashes = {}
1058
1247
parent_id_winner = "other"
1059
1248
if name_winner == "this" and parent_id_winner == "this":
1061
if name_winner == "conflict":
1062
trans_id = self.tt.trans_id_file_id(file_id)
1063
self._raw_conflicts.append(('name conflict', trans_id,
1064
this_name, other_name))
1065
if parent_id_winner == "conflict":
1066
trans_id = self.tt.trans_id_file_id(file_id)
1067
self._raw_conflicts.append(('parent conflict', trans_id,
1068
this_parent, other_parent))
1250
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1251
# Creating helpers (.OTHER or .THIS) here cause problems down the
1252
# road if a ContentConflict needs to be created so we should not do
1254
trans_id = self.tt.trans_id_file_id(file_id)
1255
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1256
this_parent, this_name,
1257
other_parent, other_name))
1069
1258
if other_name is None:
1070
# it doesn't matter whether the result was 'other' or
1259
# it doesn't matter whether the result was 'other' or
1071
1260
# 'conflict'-- if there's no 'other', we leave it alone.
1073
# if we get here, name_winner and parent_winner are set to safe values.
1074
trans_id = self.tt.trans_id_file_id(file_id)
1075
1262
parent_id = parents[self.winner_idx[parent_id_winner]]
1076
1263
if parent_id is not None:
1077
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1264
# if we get here, name_winner and parent_winner are set to safe
1078
1266
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1079
parent_trans_id, trans_id)
1267
self.tt.trans_id_file_id(parent_id),
1268
self.tt.trans_id_file_id(file_id))
1081
def merge_contents(self, file_id):
1082
"""Performa a merge on file_id contents."""
1270
def _do_merge_contents(self, file_id):
1271
"""Performs a merge on file_id contents."""
1083
1272
def contents_pair(tree):
1084
1273
if file_id not in tree:
1085
1274
return (None, None)
1092
1281
contents = None
1093
1282
return kind, contents
1095
def contents_conflict():
1096
trans_id = self.tt.trans_id_file_id(file_id)
1097
name = self.tt.final_name(trans_id)
1098
parent_id = self.tt.final_parent(trans_id)
1099
if file_id in self.this_tree.inventory:
1100
self.tt.unversion_file(trans_id)
1101
if file_id in self.this_tree:
1102
self.tt.delete_contents(trans_id)
1103
file_group = self._dump_conflicts(name, parent_id, file_id,
1105
self._raw_conflicts.append(('contents conflict', file_group))
1107
1284
# See SPOT run. run, SPOT, run.
1108
1285
# So we're not QUITE repeating ourselves; we do tricky things with
1110
1287
base_pair = contents_pair(self.base_tree)
1111
1288
other_pair = contents_pair(self.other_tree)
1112
if base_pair == other_pair:
1113
# OTHER introduced no changes
1115
this_pair = contents_pair(self.this_tree)
1116
if this_pair == other_pair:
1117
# THIS and OTHER introduced the same changes
1120
trans_id = self.tt.trans_id_file_id(file_id)
1121
if this_pair == base_pair:
1122
# only OTHER introduced changes
1123
if file_id in self.this_tree:
1124
# Remove any existing contents
1125
self.tt.delete_contents(trans_id)
1126
if file_id in self.other_tree:
1127
# OTHER changed the file
1128
create_by_entry(self.tt,
1129
self.other_tree.inventory[file_id],
1130
self.other_tree, trans_id)
1131
if file_id not in self.this_tree.inventory:
1132
self.tt.version_file(file_id, trans_id)
1134
elif file_id in self.this_tree.inventory:
1135
# OTHER deleted the file
1136
self.tt.unversion_file(trans_id)
1138
#BOTH THIS and OTHER introduced changes; scalar conflict
1139
elif this_pair[0] == "file" and other_pair[0] == "file":
1140
# THIS and OTHER are both files, so text merge. Either
1141
# BASE is a file, or both converted to files, so at least we
1142
# have agreement that output should be a file.
1144
self.text_merge(file_id, trans_id)
1146
return contents_conflict()
1147
if file_id not in self.this_tree.inventory:
1148
self.tt.version_file(file_id, trans_id)
1150
self.tt.tree_kind(trans_id)
1151
self.tt.delete_contents(trans_id)
1156
# Scalar conflict, can't text merge. Dump conflicts
1157
return contents_conflict()
1290
this_pair = contents_pair(self.this_tree)
1291
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1292
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1293
this_pair, allow_overriding_lca=False)
1295
if base_pair == other_pair:
1298
# We delayed evaluating this_pair as long as we can to avoid
1299
# unnecessary sha1 calculation
1300
this_pair = contents_pair(self.this_tree)
1301
winner = self._three_way(base_pair, other_pair, this_pair)
1302
if winner == 'this':
1303
# No interesting changes introduced by OTHER
1305
# We have a hypothetical conflict, but if we have files, then we
1306
# can try to merge the content
1307
trans_id = self.tt.trans_id_file_id(file_id)
1308
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1309
other_pair[0], winner)
1310
hooks = self.active_hooks
1311
hook_status = 'not_applicable'
1313
hook_status, lines = hook.merge_contents(params)
1314
if hook_status != 'not_applicable':
1315
# Don't try any more hooks, this one applies.
1318
if hook_status == 'not_applicable':
1319
# This is a contents conflict, because none of the available
1320
# functions could merge it.
1322
name = self.tt.final_name(trans_id)
1323
parent_id = self.tt.final_parent(trans_id)
1324
if self.this_tree.has_id(file_id):
1325
self.tt.unversion_file(trans_id)
1326
file_group = self._dump_conflicts(name, parent_id, file_id,
1328
self._raw_conflicts.append(('contents conflict', file_group))
1329
elif hook_status == 'success':
1330
self.tt.create_file(lines, trans_id)
1331
elif hook_status == 'conflicted':
1332
# XXX: perhaps the hook should be able to provide
1333
# the BASE/THIS/OTHER files?
1334
self.tt.create_file(lines, trans_id)
1335
self._raw_conflicts.append(('text conflict', trans_id))
1336
name = self.tt.final_name(trans_id)
1337
parent_id = self.tt.final_parent(trans_id)
1338
self._dump_conflicts(name, parent_id, file_id)
1339
elif hook_status == 'delete':
1340
self.tt.unversion_file(trans_id)
1342
elif hook_status == 'done':
1343
# The hook function did whatever it needs to do directly, no
1344
# further action needed here.
1347
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1348
if not self.this_tree.has_id(file_id) and result == "modified":
1349
self.tt.version_file(file_id, trans_id)
1350
# The merge has been performed, so the old contents should not be
1353
self.tt.delete_contents(trans_id)
1354
except errors.NoSuchFile:
1358
def _default_other_winner_merge(self, merge_hook_params):
1359
"""Replace this contents with other."""
1360
file_id = merge_hook_params.file_id
1361
trans_id = merge_hook_params.trans_id
1362
file_in_this = self.this_tree.has_id(file_id)
1363
if self.other_tree.has_id(file_id):
1364
# OTHER changed the file
1366
if wt.supports_content_filtering():
1367
# We get the path from the working tree if it exists.
1368
# That fails though when OTHER is adding a file, so
1369
# we fall back to the other tree to find the path if
1370
# it doesn't exist locally.
1372
filter_tree_path = wt.id2path(file_id)
1373
except errors.NoSuchId:
1374
filter_tree_path = self.other_tree.id2path(file_id)
1376
# Skip the id2path lookup for older formats
1377
filter_tree_path = None
1378
transform.create_from_tree(self.tt, trans_id,
1379
self.other_tree, file_id,
1380
filter_tree_path=filter_tree_path)
1383
# OTHER deleted the file
1384
return 'delete', None
1386
raise AssertionError(
1387
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1390
def merge_contents(self, merge_hook_params):
1391
"""Fallback merge logic after user installed hooks."""
1392
# This function is used in merge hooks as the fallback instance.
1393
# Perhaps making this function and the functions it calls be a
1394
# a separate class would be better.
1395
if merge_hook_params.winner == 'other':
1396
# OTHER is a straight winner, so replace this contents with other
1397
return self._default_other_winner_merge(merge_hook_params)
1398
elif merge_hook_params.is_file_merge():
1399
# THIS and OTHER are both files, so text merge. Either
1400
# BASE is a file, or both converted to files, so at least we
1401
# have agreement that output should be a file.
1403
self.text_merge(merge_hook_params.file_id,
1404
merge_hook_params.trans_id)
1405
except errors.BinaryFile:
1406
return 'not_applicable', None
1409
return 'not_applicable', None
1159
1411
def get_lines(self, tree, file_id):
1160
1412
"""Return the lines in a file, or an empty list."""
1413
if tree.has_id(file_id):
1162
1414
return tree.get_file(file_id).readlines()
1215
1467
determined automatically. If set_version is true, the .OTHER, .THIS
1216
1468
or .BASE (in that order) will be created as versioned files.
1218
data = [('OTHER', self.other_tree, other_lines),
1470
data = [('OTHER', self.other_tree, other_lines),
1219
1471
('THIS', self.this_tree, this_lines)]
1220
1472
if not no_base:
1221
1473
data.append(('BASE', self.base_tree, base_lines))
1475
# We need to use the actual path in the working tree of the file here,
1476
# ignoring the conflict suffixes
1478
if wt.supports_content_filtering():
1480
filter_tree_path = wt.id2path(file_id)
1481
except errors.NoSuchId:
1482
# file has been deleted
1483
filter_tree_path = None
1485
# Skip the id2path lookup for older formats
1486
filter_tree_path = None
1222
1488
versioned = False
1223
1489
file_group = []
1224
1490
for suffix, tree, lines in data:
1491
if tree.has_id(file_id):
1226
1492
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1493
suffix, lines, filter_tree_path)
1228
1494
file_group.append(trans_id)
1229
1495
if set_version and not versioned:
1230
1496
self.tt.version_file(file_id, trans_id)
1231
1497
versioned = True
1232
1498
return file_group
1234
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1500
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1501
lines=None, filter_tree_path=None):
1236
1502
"""Emit a single conflict file."""
1237
1503
name = name + '.' + suffix
1238
1504
trans_id = self.tt.create_path(name, parent_id)
1239
entry = tree.inventory[file_id]
1240
create_by_entry(self.tt, entry, tree, trans_id, lines)
1505
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1241
1507
return trans_id
1243
1509
def merge_executable(self, file_id, file_status):
1285
1551
def cook_conflicts(self, fs_conflicts):
1286
1552
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1287
from conflicts import Conflict
1289
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1290
fp = FinalPaths(self.tt)
1553
self.cooked_conflicts.extend(transform.cook_conflicts(
1554
fs_conflicts, self.tt))
1555
fp = transform.FinalPaths(self.tt)
1291
1556
for conflict in self._raw_conflicts:
1292
1557
conflict_type = conflict[0]
1293
if conflict_type in ('name conflict', 'parent conflict'):
1294
trans_id = conflict[1]
1295
conflict_args = conflict[2:]
1296
if trans_id not in name_conflicts:
1297
name_conflicts[trans_id] = {}
1298
unique_add(name_conflicts[trans_id], conflict_type,
1300
if conflict_type == 'contents conflict':
1558
if conflict_type == 'path conflict':
1560
this_parent, this_name,
1561
other_parent, other_name) = conflict[1:]
1562
if this_parent is None or this_name is None:
1563
this_path = '<deleted>'
1565
parent_path = fp.get_path(
1566
self.tt.trans_id_file_id(this_parent))
1567
this_path = osutils.pathjoin(parent_path, this_name)
1568
if other_parent is None or other_name is None:
1569
other_path = '<deleted>'
1571
parent_path = fp.get_path(
1572
self.tt.trans_id_file_id(other_parent))
1573
other_path = osutils.pathjoin(parent_path, other_name)
1574
c = _mod_conflicts.Conflict.factory(
1575
'path conflict', path=this_path,
1576
conflict_path=other_path,
1578
elif conflict_type == 'contents conflict':
1301
1579
for trans_id in conflict[1]:
1302
1580
file_id = self.tt.final_file_id(trans_id)
1303
1581
if file_id is not None:
1307
1585
if path.endswith(suffix):
1308
1586
path = path[:-len(suffix)]
1310
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1311
self.cooked_conflicts.append(c)
1312
if conflict_type == 'text conflict':
1588
c = _mod_conflicts.Conflict.factory(conflict_type,
1589
path=path, file_id=file_id)
1590
elif conflict_type == 'text conflict':
1313
1591
trans_id = conflict[1]
1314
1592
path = fp.get_path(trans_id)
1315
1593
file_id = self.tt.final_file_id(trans_id)
1316
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1317
self.cooked_conflicts.append(c)
1319
for trans_id, conflicts in name_conflicts.iteritems():
1321
this_parent, other_parent = conflicts['parent conflict']
1322
if this_parent == other_parent:
1323
raise AssertionError()
1325
this_parent = other_parent = \
1326
self.tt.final_file_id(self.tt.final_parent(trans_id))
1328
this_name, other_name = conflicts['name conflict']
1329
if this_name == other_name:
1330
raise AssertionError()
1332
this_name = other_name = self.tt.final_name(trans_id)
1333
other_path = fp.get_path(trans_id)
1334
if this_parent is not None and this_name is not None:
1335
this_parent_path = \
1336
fp.get_path(self.tt.trans_id_file_id(this_parent))
1337
this_path = pathjoin(this_parent_path, this_name)
1594
c = _mod_conflicts.Conflict.factory(conflict_type,
1595
path=path, file_id=file_id)
1339
this_path = "<deleted>"
1340
file_id = self.tt.final_file_id(trans_id)
1341
c = Conflict.factory('path conflict', path=this_path,
1342
conflict_path=other_path, file_id=file_id)
1597
raise AssertionError('bad conflict type: %r' % (conflict,))
1343
1598
self.cooked_conflicts.append(c)
1344
self.cooked_conflicts.sort(key=Conflict.sort_key)
1599
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1347
1602
class WeaveMerger(Merge3Merger):
1351
1606
supports_reverse_cherrypick = False
1352
1607
history_based = True
1354
def _merged_lines(self, file_id):
1355
"""Generate the merged lines.
1356
There is no distinction between lines that are meant to contain <<<<<<<
1360
base = self.base_tree
1363
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1609
def _generate_merge_plan(self, file_id, base):
1610
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1613
def _merged_lines(self, file_id):
1614
"""Generate the merged lines.
1615
There is no distinction between lines that are meant to contain <<<<<<<
1619
base = self.base_tree
1622
plan = self._generate_merge_plan(file_id, base)
1365
1623
if 'merge' in debug.debug_flags:
1366
1624
plan = list(plan)
1367
1625
trans_id = self.tt.trans_id_file_id(file_id)
1368
1626
name = self.tt.final_name(trans_id) + '.plan'
1369
contents = ('%10s|%s' % l for l in plan)
1627
contents = ('%11s|%s' % l for l in plan)
1370
1628
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1371
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1372
'>>>>>>> MERGE-SOURCE\n')
1373
return textmerge.merge_lines(self.reprocess)
1629
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1630
'>>>>>>> MERGE-SOURCE\n')
1631
lines, conflicts = textmerge.merge_lines(self.reprocess)
1633
base_lines = textmerge.base_from_plan()
1636
return lines, base_lines
1375
1638
def text_merge(self, file_id, trans_id):
1376
1639
"""Perform a (weave) text merge for a given file and file-id.
1377
1640
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1378
1641
and a conflict will be noted.
1380
lines, conflicts = self._merged_lines(file_id)
1643
lines, base_lines = self._merged_lines(file_id)
1381
1644
lines = list(lines)
1382
# Note we're checking whether the OUTPUT is binary in this case,
1645
# Note we're checking whether the OUTPUT is binary in this case,
1383
1646
# because we don't want to get into weave merge guts.
1384
check_text_lines(lines)
1647
textfile.check_text_lines(lines)
1385
1648
self.tt.create_file(lines, trans_id)
1649
if base_lines is not None:
1387
1651
self._raw_conflicts.append(('text conflict', trans_id))
1388
1652
name = self.tt.final_name(trans_id)
1389
1653
parent_id = self.tt.final_parent(trans_id)
1390
file_group = self._dump_conflicts(name, parent_id, file_id,
1654
file_group = self._dump_conflicts(name, parent_id, file_id,
1656
base_lines=base_lines)
1392
1657
file_group.append(trans_id)
1395
1660
class LCAMerger(WeaveMerger):
1397
def _merged_lines(self, file_id):
1398
"""Generate the merged lines.
1399
There is no distinction between lines that are meant to contain <<<<<<<
1403
base = self.base_tree
1406
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1662
def _generate_merge_plan(self, file_id, base):
1663
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1408
if 'merge' in debug.debug_flags:
1410
trans_id = self.tt.trans_id_file_id(file_id)
1411
name = self.tt.final_name(trans_id) + '.plan'
1412
contents = ('%10s|%s' % l for l in plan)
1413
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1414
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1415
'>>>>>>> MERGE-SOURCE\n')
1416
return textmerge.merge_lines(self.reprocess)
1419
1666
class Diff3Merger(Merge3Merger):
1420
1667
"""Three-way merger using external diff3 for text merging"""
1422
1669
def dump_file(self, temp_dir, name, tree, file_id):
1423
out_path = pathjoin(temp_dir, name)
1670
out_path = osutils.pathjoin(temp_dir, name)
1424
1671
out_file = open(out_path, "wb")
1426
1673
in_file = tree.get_file(file_id)