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
250
417
if self.this_rev_id is None:
251
418
if self.this_basis_tree.get_file_sha1(file_id) != \
252
419
self.this_tree.get_file_sha1(file_id):
253
raise WorkingTreeNotRevision(self.this_tree)
420
raise errors.WorkingTreeNotRevision(self.this_tree)
255
422
trees = (self.this_basis_tree, self.other_tree)
256
423
return [get_id(tree, file_id) for tree in trees]
425
@deprecated_method(deprecated_in((2, 1, 0)))
258
426
def check_basis(self, check_clean, require_commits=True):
259
427
if self.this_basis is None and require_commits is True:
260
raise BzrCommandError("This branch has no commits."
261
" (perhaps you would prefer 'bzr pull')")
428
raise errors.BzrCommandError(
429
"This branch has no commits."
430
" (perhaps you would prefer 'bzr pull')")
263
432
self.compare_basis()
264
433
if self.this_basis != self.this_rev_id:
265
434
raise errors.UncommittedChanges(self.this_tree)
436
@deprecated_method(deprecated_in((2, 1, 0)))
267
437
def compare_basis(self):
269
439
basis_tree = self.revision_tree(self.this_tree.last_revision())
270
440
except errors.NoSuchRevision:
271
441
basis_tree = self.this_tree.basis_tree()
272
changes = self.this_tree.changes_from(basis_tree)
273
if not changes.has_changed():
442
if not self.this_tree.has_changes(basis_tree):
274
443
self.this_rev_id = self.this_basis
276
445
def set_interesting_files(self, file_list):
277
446
self.interesting_files = file_list
279
448
def set_pending(self):
280
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):
282
452
self._add_parent()
284
454
def _add_parent(self):
285
455
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
286
456
new_parent_trees = []
457
operation = OperationWithCleanups(self.this_tree.set_parent_trees)
287
458
for revision_id in new_parents:
289
460
tree = self.revision_tree(revision_id)
527
693
winner_idx = {"this": 2, "other": 1, "conflict": 1}
528
694
supports_lca_trees = True
530
def __init__(self, working_tree, this_tree, base_tree, other_tree,
696
def __init__(self, working_tree, this_tree, base_tree, other_tree,
531
697
interesting_ids=None, reprocess=False, show_base=False,
532
pb=DummyProgress(), pp=None, change_reporter=None,
698
pb=None, pp=None, change_reporter=None,
533
699
interesting_files=None, do_merge=True,
534
cherrypick=False, lca_trees=None):
700
cherrypick=False, lca_trees=None, this_branch=None):
535
701
"""Initialize the merger object and perform the merge.
537
703
:param working_tree: The working tree to apply the merge to
538
704
:param this_tree: The local tree in the merge operation
539
705
:param base_tree: The common tree in the merge operation
540
: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
541
708
:param interesting_ids: The file_ids of files that should be
542
709
participate in the merge. May not be combined with
543
710
interesting_files.
544
711
:param: reprocess If True, perform conflict-reduction processing.
545
712
:param show_base: If True, show the base revision in text conflicts.
546
713
(incompatible with reprocess)
547
:param pb: A Progress bar
548
715
:param pp: A ProgressPhase object
549
716
:param change_reporter: An object that should report changes made
550
717
:param interesting_files: The tree-relative paths of files that should
576
744
# making sure we haven't missed any corner cases.
577
745
# if lca_trees is None:
578
746
# self._lca_trees = [self.base_tree]
581
747
self.change_reporter = change_reporter
582
748
self.cherrypick = cherrypick
584
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")
588
756
def do_merge(self):
757
operation = OperationWithCleanups(self._do_merge)
589
758
self.this_tree.lock_tree_write()
759
operation.add_cleanup(self.this_tree.unlock)
590
760
self.base_tree.lock_read()
761
operation.add_cleanup(self.base_tree.unlock)
591
762
self.other_tree.lock_read()
592
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)
595
self._compute_transform()
597
results = self.tt.apply(no_conflicts=True)
598
self.write_modified(results)
600
self.this_tree.add_conflicts(self.cooked_conflicts)
601
except UnsupportedOperation:
605
self.other_tree.unlock()
606
self.base_tree.unlock()
607
self.this_tree.unlock()
773
self.this_tree.add_conflicts(self.cooked_conflicts)
774
except errors.UnsupportedOperation:
610
777
def make_preview_transform(self):
778
operation = OperationWithCleanups(self._make_preview_transform)
611
779
self.base_tree.lock_read()
780
operation.add_cleanup(self.base_tree.unlock)
612
781
self.other_tree.lock_read()
613
self.tt = TransformPreview(self.this_tree)
616
self._compute_transform()
619
self.other_tree.unlock()
620
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()
624
790
def _compute_transform(self):
1064
1230
parent_id_winner = "other"
1065
1231
if name_winner == "this" and parent_id_winner == "this":
1067
if name_winner == "conflict":
1068
trans_id = self.tt.trans_id_file_id(file_id)
1069
self._raw_conflicts.append(('name conflict', trans_id,
1070
this_name, other_name))
1071
if parent_id_winner == "conflict":
1072
trans_id = self.tt.trans_id_file_id(file_id)
1073
self._raw_conflicts.append(('parent conflict', trans_id,
1074
this_parent, other_parent))
1233
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1234
# Creating helpers (.OTHER or .THIS) here cause problems down the
1235
# road if a ContentConflict needs to be created so we should not do
1237
trans_id = self.tt.trans_id_file_id(file_id)
1238
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1239
this_parent, this_name,
1240
other_parent, other_name))
1075
1241
if other_name is None:
1076
# it doesn't matter whether the result was 'other' or
1242
# it doesn't matter whether the result was 'other' or
1077
1243
# 'conflict'-- if there's no 'other', we leave it alone.
1079
# if we get here, name_winner and parent_winner are set to safe values.
1080
trans_id = self.tt.trans_id_file_id(file_id)
1081
1245
parent_id = parents[self.winner_idx[parent_id_winner]]
1082
1246
if parent_id is not None:
1083
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1247
# if we get here, name_winner and parent_winner are set to safe
1084
1249
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1085
parent_trans_id, trans_id)
1250
self.tt.trans_id_file_id(parent_id),
1251
self.tt.trans_id_file_id(file_id))
1087
def merge_contents(self, file_id):
1088
"""Performa a merge on file_id contents."""
1253
def _do_merge_contents(self, file_id):
1254
"""Performs a merge on file_id contents."""
1089
1255
def contents_pair(tree):
1090
1256
if file_id not in tree:
1091
1257
return (None, None)
1098
1264
contents = None
1099
1265
return kind, contents
1101
def contents_conflict():
1102
trans_id = self.tt.trans_id_file_id(file_id)
1103
name = self.tt.final_name(trans_id)
1104
parent_id = self.tt.final_parent(trans_id)
1105
if file_id in self.this_tree.inventory:
1106
self.tt.unversion_file(trans_id)
1107
if file_id in self.this_tree:
1108
self.tt.delete_contents(trans_id)
1109
file_group = self._dump_conflicts(name, parent_id, file_id,
1111
self._raw_conflicts.append(('contents conflict', file_group))
1113
1267
# See SPOT run. run, SPOT, run.
1114
1268
# So we're not QUITE repeating ourselves; we do tricky things with
1116
1270
base_pair = contents_pair(self.base_tree)
1117
1271
other_pair = contents_pair(self.other_tree)
1118
if base_pair == other_pair:
1119
# OTHER introduced no changes
1121
this_pair = contents_pair(self.this_tree)
1122
if this_pair == other_pair:
1123
# THIS and OTHER introduced the same changes
1126
trans_id = self.tt.trans_id_file_id(file_id)
1127
if this_pair == base_pair:
1128
# only OTHER introduced changes
1129
if file_id in self.this_tree:
1130
# Remove any existing contents
1131
self.tt.delete_contents(trans_id)
1132
if file_id in self.other_tree:
1133
# OTHER changed the file
1134
create_by_entry(self.tt,
1135
self.other_tree.inventory[file_id],
1136
self.other_tree, trans_id)
1137
if file_id not in self.this_tree:
1138
self.tt.version_file(file_id, trans_id)
1140
elif file_id in self.this_tree.inventory:
1141
# OTHER deleted the file
1142
self.tt.unversion_file(trans_id)
1144
#BOTH THIS and OTHER introduced changes; scalar conflict
1145
elif this_pair[0] == "file" and other_pair[0] == "file":
1146
# THIS and OTHER are both files, so text merge. Either
1147
# BASE is a file, or both converted to files, so at least we
1148
# have agreement that output should be a file.
1150
self.text_merge(file_id, trans_id)
1152
return contents_conflict()
1153
if file_id not in self.this_tree:
1154
self.tt.version_file(file_id, trans_id)
1156
self.tt.tree_kind(trans_id)
1157
self.tt.delete_contents(trans_id)
1162
# Scalar conflict, can't text merge. Dump conflicts
1163
return contents_conflict()
1273
this_pair = contents_pair(self.this_tree)
1274
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1275
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1276
this_pair, allow_overriding_lca=False)
1278
if base_pair == other_pair:
1281
# We delayed evaluating this_pair as long as we can to avoid
1282
# unnecessary sha1 calculation
1283
this_pair = contents_pair(self.this_tree)
1284
winner = self._three_way(base_pair, other_pair, this_pair)
1285
if winner == 'this':
1286
# No interesting changes introduced by OTHER
1288
# We have a hypothetical conflict, but if we have files, then we
1289
# can try to merge the content
1290
trans_id = self.tt.trans_id_file_id(file_id)
1291
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1292
other_pair[0], winner)
1293
hooks = self.active_hooks
1294
hook_status = 'not_applicable'
1296
hook_status, lines = hook.merge_contents(params)
1297
if hook_status != 'not_applicable':
1298
# Don't try any more hooks, this one applies.
1301
if hook_status == 'not_applicable':
1302
# This is a contents conflict, because none of the available
1303
# functions could merge it.
1305
name = self.tt.final_name(trans_id)
1306
parent_id = self.tt.final_parent(trans_id)
1307
if self.this_tree.has_id(file_id):
1308
self.tt.unversion_file(trans_id)
1309
file_group = self._dump_conflicts(name, parent_id, file_id,
1311
self._raw_conflicts.append(('contents conflict', file_group))
1312
elif hook_status == 'success':
1313
self.tt.create_file(lines, trans_id)
1314
elif hook_status == 'conflicted':
1315
# XXX: perhaps the hook should be able to provide
1316
# the BASE/THIS/OTHER files?
1317
self.tt.create_file(lines, trans_id)
1318
self._raw_conflicts.append(('text conflict', trans_id))
1319
name = self.tt.final_name(trans_id)
1320
parent_id = self.tt.final_parent(trans_id)
1321
self._dump_conflicts(name, parent_id, file_id)
1322
elif hook_status == 'delete':
1323
self.tt.unversion_file(trans_id)
1325
elif hook_status == 'done':
1326
# The hook function did whatever it needs to do directly, no
1327
# further action needed here.
1330
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1331
if not self.this_tree.has_id(file_id) and result == "modified":
1332
self.tt.version_file(file_id, trans_id)
1333
# The merge has been performed, so the old contents should not be
1336
self.tt.delete_contents(trans_id)
1337
except errors.NoSuchFile:
1341
def _default_other_winner_merge(self, merge_hook_params):
1342
"""Replace this contents with other."""
1343
file_id = merge_hook_params.file_id
1344
trans_id = merge_hook_params.trans_id
1345
file_in_this = self.this_tree.has_id(file_id)
1346
if self.other_tree.has_id(file_id):
1347
# OTHER changed the file
1349
if wt.supports_content_filtering():
1350
# We get the path from the working tree if it exists.
1351
# That fails though when OTHER is adding a file, so
1352
# we fall back to the other tree to find the path if
1353
# it doesn't exist locally.
1355
filter_tree_path = wt.id2path(file_id)
1356
except errors.NoSuchId:
1357
filter_tree_path = self.other_tree.id2path(file_id)
1359
# Skip the id2path lookup for older formats
1360
filter_tree_path = None
1361
transform.create_from_tree(self.tt, trans_id,
1362
self.other_tree, file_id,
1363
filter_tree_path=filter_tree_path)
1366
# OTHER deleted the file
1367
return 'delete', None
1369
raise AssertionError(
1370
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1373
def merge_contents(self, merge_hook_params):
1374
"""Fallback merge logic after user installed hooks."""
1375
# This function is used in merge hooks as the fallback instance.
1376
# Perhaps making this function and the functions it calls be a
1377
# a separate class would be better.
1378
if merge_hook_params.winner == 'other':
1379
# OTHER is a straight winner, so replace this contents with other
1380
return self._default_other_winner_merge(merge_hook_params)
1381
elif merge_hook_params.is_file_merge():
1382
# THIS and OTHER are both files, so text merge. Either
1383
# BASE is a file, or both converted to files, so at least we
1384
# have agreement that output should be a file.
1386
self.text_merge(merge_hook_params.file_id,
1387
merge_hook_params.trans_id)
1388
except errors.BinaryFile:
1389
return 'not_applicable', None
1392
return 'not_applicable', None
1165
1394
def get_lines(self, tree, file_id):
1166
1395
"""Return the lines in a file, or an empty list."""
1396
if tree.has_id(file_id):
1168
1397
return tree.get_file(file_id).readlines()
1221
1450
determined automatically. If set_version is true, the .OTHER, .THIS
1222
1451
or .BASE (in that order) will be created as versioned files.
1224
data = [('OTHER', self.other_tree, other_lines),
1453
data = [('OTHER', self.other_tree, other_lines),
1225
1454
('THIS', self.this_tree, this_lines)]
1226
1455
if not no_base:
1227
1456
data.append(('BASE', self.base_tree, base_lines))
1458
# We need to use the actual path in the working tree of the file here,
1459
# ignoring the conflict suffixes
1461
if wt.supports_content_filtering():
1463
filter_tree_path = wt.id2path(file_id)
1464
except errors.NoSuchId:
1465
# file has been deleted
1466
filter_tree_path = None
1468
# Skip the id2path lookup for older formats
1469
filter_tree_path = None
1228
1471
versioned = False
1229
1472
file_group = []
1230
1473
for suffix, tree, lines in data:
1474
if tree.has_id(file_id):
1232
1475
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1476
suffix, lines, filter_tree_path)
1234
1477
file_group.append(trans_id)
1235
1478
if set_version and not versioned:
1236
1479
self.tt.version_file(file_id, trans_id)
1237
1480
versioned = True
1238
1481
return file_group
1240
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1483
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1484
lines=None, filter_tree_path=None):
1242
1485
"""Emit a single conflict file."""
1243
1486
name = name + '.' + suffix
1244
1487
trans_id = self.tt.create_path(name, parent_id)
1245
entry = tree.inventory[file_id]
1246
create_by_entry(self.tt, entry, tree, trans_id, lines)
1488
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1247
1490
return trans_id
1249
1492
def merge_executable(self, file_id, file_status):
1291
1534
def cook_conflicts(self, fs_conflicts):
1292
1535
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1293
from conflicts import Conflict
1295
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1296
fp = FinalPaths(self.tt)
1536
self.cooked_conflicts.extend(transform.cook_conflicts(
1537
fs_conflicts, self.tt))
1538
fp = transform.FinalPaths(self.tt)
1297
1539
for conflict in self._raw_conflicts:
1298
1540
conflict_type = conflict[0]
1299
if conflict_type in ('name conflict', 'parent conflict'):
1300
trans_id = conflict[1]
1301
conflict_args = conflict[2:]
1302
if trans_id not in name_conflicts:
1303
name_conflicts[trans_id] = {}
1304
unique_add(name_conflicts[trans_id], conflict_type,
1306
if conflict_type == 'contents conflict':
1541
if conflict_type == 'path conflict':
1543
this_parent, this_name,
1544
other_parent, other_name) = conflict[1:]
1545
if this_parent is None or this_name is None:
1546
this_path = '<deleted>'
1548
parent_path = fp.get_path(
1549
self.tt.trans_id_file_id(this_parent))
1550
this_path = osutils.pathjoin(parent_path, this_name)
1551
if other_parent is None or other_name is None:
1552
other_path = '<deleted>'
1554
parent_path = fp.get_path(
1555
self.tt.trans_id_file_id(other_parent))
1556
other_path = osutils.pathjoin(parent_path, other_name)
1557
c = _mod_conflicts.Conflict.factory(
1558
'path conflict', path=this_path,
1559
conflict_path=other_path,
1561
elif conflict_type == 'contents conflict':
1307
1562
for trans_id in conflict[1]:
1308
1563
file_id = self.tt.final_file_id(trans_id)
1309
1564
if file_id is not None:
1313
1568
if path.endswith(suffix):
1314
1569
path = path[:-len(suffix)]
1316
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1317
self.cooked_conflicts.append(c)
1318
if conflict_type == 'text conflict':
1571
c = _mod_conflicts.Conflict.factory(conflict_type,
1572
path=path, file_id=file_id)
1573
elif conflict_type == 'text conflict':
1319
1574
trans_id = conflict[1]
1320
1575
path = fp.get_path(trans_id)
1321
1576
file_id = self.tt.final_file_id(trans_id)
1322
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1323
self.cooked_conflicts.append(c)
1325
for trans_id, conflicts in name_conflicts.iteritems():
1327
this_parent, other_parent = conflicts['parent conflict']
1328
if this_parent == other_parent:
1329
raise AssertionError()
1331
this_parent = other_parent = \
1332
self.tt.final_file_id(self.tt.final_parent(trans_id))
1334
this_name, other_name = conflicts['name conflict']
1335
if this_name == other_name:
1336
raise AssertionError()
1338
this_name = other_name = self.tt.final_name(trans_id)
1339
other_path = fp.get_path(trans_id)
1340
if this_parent is not None and this_name is not None:
1341
this_parent_path = \
1342
fp.get_path(self.tt.trans_id_file_id(this_parent))
1343
this_path = pathjoin(this_parent_path, this_name)
1577
c = _mod_conflicts.Conflict.factory(conflict_type,
1578
path=path, file_id=file_id)
1345
this_path = "<deleted>"
1346
file_id = self.tt.final_file_id(trans_id)
1347
c = Conflict.factory('path conflict', path=this_path,
1348
conflict_path=other_path, file_id=file_id)
1580
raise AssertionError('bad conflict type: %r' % (conflict,))
1349
1581
self.cooked_conflicts.append(c)
1350
self.cooked_conflicts.sort(key=Conflict.sort_key)
1582
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1353
1585
class WeaveMerger(Merge3Merger):
1357
1589
supports_reverse_cherrypick = False
1358
1590
history_based = True
1360
def _merged_lines(self, file_id):
1361
"""Generate the merged lines.
1362
There is no distinction between lines that are meant to contain <<<<<<<
1366
base = self.base_tree
1369
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1592
def _generate_merge_plan(self, file_id, base):
1593
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1596
def _merged_lines(self, file_id):
1597
"""Generate the merged lines.
1598
There is no distinction between lines that are meant to contain <<<<<<<
1602
base = self.base_tree
1605
plan = self._generate_merge_plan(file_id, base)
1371
1606
if 'merge' in debug.debug_flags:
1372
1607
plan = list(plan)
1373
1608
trans_id = self.tt.trans_id_file_id(file_id)
1374
1609
name = self.tt.final_name(trans_id) + '.plan'
1375
contents = ('%10s|%s' % l for l in plan)
1610
contents = ('%11s|%s' % l for l in plan)
1376
1611
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1377
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1378
'>>>>>>> MERGE-SOURCE\n')
1379
return textmerge.merge_lines(self.reprocess)
1612
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1613
'>>>>>>> MERGE-SOURCE\n')
1614
lines, conflicts = textmerge.merge_lines(self.reprocess)
1616
base_lines = textmerge.base_from_plan()
1619
return lines, base_lines
1381
1621
def text_merge(self, file_id, trans_id):
1382
1622
"""Perform a (weave) text merge for a given file and file-id.
1383
1623
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1384
1624
and a conflict will be noted.
1386
lines, conflicts = self._merged_lines(file_id)
1626
lines, base_lines = self._merged_lines(file_id)
1387
1627
lines = list(lines)
1388
# Note we're checking whether the OUTPUT is binary in this case,
1628
# Note we're checking whether the OUTPUT is binary in this case,
1389
1629
# because we don't want to get into weave merge guts.
1390
check_text_lines(lines)
1630
textfile.check_text_lines(lines)
1391
1631
self.tt.create_file(lines, trans_id)
1632
if base_lines is not None:
1393
1634
self._raw_conflicts.append(('text conflict', trans_id))
1394
1635
name = self.tt.final_name(trans_id)
1395
1636
parent_id = self.tt.final_parent(trans_id)
1396
file_group = self._dump_conflicts(name, parent_id, file_id,
1637
file_group = self._dump_conflicts(name, parent_id, file_id,
1639
base_lines=base_lines)
1398
1640
file_group.append(trans_id)
1401
1643
class LCAMerger(WeaveMerger):
1403
def _merged_lines(self, file_id):
1404
"""Generate the merged lines.
1405
There is no distinction between lines that are meant to contain <<<<<<<
1409
base = self.base_tree
1412
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1645
def _generate_merge_plan(self, file_id, base):
1646
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1414
if 'merge' in debug.debug_flags:
1416
trans_id = self.tt.trans_id_file_id(file_id)
1417
name = self.tt.final_name(trans_id) + '.plan'
1418
contents = ('%10s|%s' % l for l in plan)
1419
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1420
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1421
'>>>>>>> MERGE-SOURCE\n')
1422
return textmerge.merge_lines(self.reprocess)
1425
1649
class Diff3Merger(Merge3Merger):
1426
1650
"""Three-way merger using external diff3 for text merging"""
1428
1652
def dump_file(self, temp_dir, name, tree, file_id):
1429
out_path = pathjoin(temp_dir, name)
1653
out_path = osutils.pathjoin(temp_dir, name)
1430
1654
out_file = open(out_path, "wb")
1432
1656
in_file = tree.get_file(file_id)