986
982
tt.create_directory(trans_id)
987
983
elif c_type == 'unversioned parent':
988
984
tt.version_file(tt.get_tree_file_id(conflict[1]), conflict[1])
991
class Merge3Merger(object):
993
supports_reprocess = True
994
supports_show_base = True
995
history_based = False
996
def __init__(self, working_tree, this_tree, base_tree, other_tree,
997
reprocess=False, show_base=False):
998
object.__init__(self)
999
self.this_tree = working_tree
1000
self.base_tree = base_tree
1001
self.other_tree = other_tree
1002
self._raw_conflicts = []
1003
self.cooked_conflicts = []
1004
self.reprocess = reprocess
1005
self.show_base = show_base
1007
all_ids = set(base_tree)
1008
all_ids.update(other_tree)
1009
self.tt = TreeTransform(working_tree)
1011
for file_id in all_ids:
1012
self.merge_names(file_id)
1013
file_status = self.merge_contents(file_id)
1014
self.merge_executable(file_id, file_status)
1016
resolve_conflicts(self.tt)
1017
self.cook_conflicts()
1026
def parent(entry, file_id):
1029
return entry.parent_id
1032
def name(entry, file_id):
1038
def contents_sha1(tree, file_id):
1039
if file_id not in tree:
1041
return tree.get_file_sha1(file_id)
1044
def executable(tree, file_id):
1045
if file_id not in tree:
1047
if tree.kind(file_id) != "file":
1049
return tree.is_executable(file_id)
1052
def kind(tree, file_id):
1053
if file_id not in tree:
1055
return tree.kind(file_id)
1058
def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1059
"""Do a three-way test on a scalar.
1060
Return "this", "other" or "conflict", depending whether a value wins.
1062
key_base = key(base_tree, file_id)
1063
key_other = key(other_tree, file_id)
1064
#if base == other, either they all agree, or only THIS has changed.
1065
if key_base == key_other:
1067
key_this = key(this_tree, file_id)
1068
if key_this not in (key_base, key_other):
1070
# "Ambiguous clean merge"
1071
elif key_this == key_other:
1074
assert key_this == key_base
1077
def merge_names(self, file_id):
1078
def get_entry(tree):
1079
if file_id in tree.inventory:
1080
return tree.inventory[file_id]
1083
this_entry = get_entry(self.this_tree)
1084
other_entry = get_entry(self.other_tree)
1085
base_entry = get_entry(self.base_tree)
1086
name_winner = self.scalar_three_way(this_entry, base_entry,
1087
other_entry, file_id, self.name)
1088
parent_id_winner = self.scalar_three_way(this_entry, base_entry,
1089
other_entry, file_id,
1091
if this_entry is None:
1092
if name_winner == "this":
1093
name_winner = "other"
1094
if parent_id_winner == "this":
1095
parent_id_winner = "other"
1096
if name_winner == "this" and parent_id_winner == "this":
1098
if name_winner == "conflict":
1099
trans_id = self.tt.get_trans_id(file_id)
1100
self._raw_conflicts.append(('name conflict', trans_id,
1101
self.name(this_entry, file_id),
1102
self.name(other_entry, file_id)))
1103
if parent_id_winner == "conflict":
1104
trans_id = self.tt.get_trans_id(file_id)
1105
self._raw_conflicts.append(('parent conflict', trans_id,
1106
self.parent(this_entry, file_id),
1107
self.parent(other_entry, file_id)))
1108
if other_entry is None:
1109
# it doesn't matter whether the result was 'other' or
1110
# 'conflict'-- if there's no 'other', we leave it alone.
1112
# if we get here, name_winner and parent_winner are set to safe values.
1113
winner_entry = {"this": this_entry, "other": other_entry,
1114
"conflict": other_entry}
1115
trans_id = self.tt.get_trans_id(file_id)
1116
parent_id = winner_entry[parent_id_winner].parent_id
1117
parent_trans_id = self.tt.get_trans_id(parent_id)
1118
self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
1122
def merge_contents(self, file_id):
1123
def contents_pair(tree):
1124
if file_id not in tree:
1126
kind = tree.kind(file_id)
1128
contents = tree.get_file_sha1(file_id)
1129
elif kind == "symlink":
1130
contents = tree.get_symlink_target(file_id)
1133
return kind, contents
1134
# See SPOT run. run, SPOT, run.
1135
# So we're not QUITE repeating ourselves; we do tricky things with
1137
base_pair = contents_pair(self.base_tree)
1138
other_pair = contents_pair(self.other_tree)
1139
if base_pair == other_pair:
1141
this_pair = contents_pair(self.this_tree)
1142
if this_pair == other_pair:
1145
trans_id = self.tt.get_trans_id(file_id)
1146
if this_pair == base_pair:
1147
if file_id in self.this_tree:
1148
self.tt.delete_contents(trans_id)
1149
if file_id in self.other_tree.inventory:
1150
create_by_entry(self.tt,
1151
self.other_tree.inventory[file_id],
1152
self.other_tree, trans_id)
1154
if file_id in self.this_tree:
1155
self.tt.unversion_file(trans_id)
1157
elif this_pair[0] == "file" and other_pair[0] == "file":
1158
# If this and other are both files, either base is a file, or
1159
# both converted to files, so at least we have agreement that
1160
# output should be a file.
1161
self.text_merge(file_id, trans_id)
1164
trans_id = self.tt.get_trans_id(file_id)
1165
name = self.tt.final_name(trans_id)
1166
parent_id = self.tt.final_parent(trans_id)
1167
if file_id in self.this_tree.inventory:
1168
self.tt.unversion_file(trans_id)
1169
self.tt.delete_contents(trans_id)
1171
self.tt.cancel_versioning(trans_id)
1172
file_group = self._dump_conflicts(name, parent_id, file_id,
1174
self._raw_conflicts.append(('contents conflict', file_group))
1176
def get_lines(self, tree, file_id):
1178
return tree.get_file(file_id).readlines()
1182
def text_merge(self, file_id, trans_id):
1183
"""Perform a three-way text merge on a file_id"""
1184
# it's possible that we got here with base as a different type.
1185
# if so, we just want two-way text conflicts.
1186
if file_id in self.base_tree and \
1187
self.base_tree.kind(file_id) == "file":
1188
base_lines = self.get_lines(self.base_tree, file_id)
1191
other_lines = self.get_lines(self.other_tree, file_id)
1192
this_lines = self.get_lines(self.this_tree, file_id)
1193
m3 = Merge3(base_lines, this_lines, other_lines)
1194
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1195
if self.show_base is True:
1196
base_marker = '|' * 7
1200
def iter_merge3(retval):
1201
retval["text_conflicts"] = False
1202
for line in m3.merge_lines(name_a = "TREE",
1203
name_b = "MERGE-SOURCE",
1204
name_base = "BASE-REVISION",
1205
start_marker=start_marker,
1206
base_marker=base_marker,
1207
reprocess=self.reprocess):
1208
if line.startswith(start_marker):
1209
retval["text_conflicts"] = True
1210
yield line.replace(start_marker, '<' * 7)
1214
merge3_iterator = iter_merge3(retval)
1215
self.tt.create_file(merge3_iterator, trans_id)
1216
if retval["text_conflicts"] is True:
1217
self._raw_conflicts.append(('text conflict', trans_id))
1218
name = self.tt.final_name(trans_id)
1219
parent_id = self.tt.final_parent(trans_id)
1220
file_group = self._dump_conflicts(name, parent_id, file_id,
1221
this_lines, base_lines,
1223
file_group.append(trans_id)
1225
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1226
base_lines=None, other_lines=None, set_version=False,
1228
data = [('OTHER', self.other_tree, other_lines),
1229
('THIS', self.this_tree, this_lines)]
1231
data.append(('BASE', self.base_tree, base_lines))
1234
for suffix, tree, lines in data:
1236
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1238
file_group.append(trans_id)
1239
if set_version and not versioned:
1240
self.tt.version_file(file_id, trans_id)
1244
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1246
name = name + '.' + suffix
1247
trans_id = self.tt.create_path(name, parent_id)
1248
entry = tree.inventory[file_id]
1249
create_by_entry(self.tt, entry, tree, trans_id, lines)
1252
def merge_executable(self, file_id, file_status):
1253
if file_status == "deleted":
1255
trans_id = self.tt.get_trans_id(file_id)
1257
if self.tt.final_kind(trans_id) != "file":
1261
winner = self.scalar_three_way(self.this_tree, self.base_tree,
1262
self.other_tree, file_id,
1264
if winner == "conflict":
1265
# There must be a None in here, if we have a conflict, but we
1266
# need executability since file status was not deleted.
1267
if self.other_tree.is_executable(file_id) is None:
1271
if winner == "this":
1272
if file_status == "modified":
1273
executability = self.this_tree.is_executable(file_id)
1274
if executability is not None:
1275
trans_id = self.tt.get_trans_id(file_id)
1276
self.tt.set_executability(executability, trans_id)
1278
assert winner == "other"
1279
if file_id in self.other_tree:
1280
executability = self.other_tree.is_executable(file_id)
1281
elif file_id in self.this_tree:
1282
executability = self.this_tree.is_executable(file_id)
1283
elif file_id in self.base_tree:
1284
executability = self.base_tree.is_executable(file_id)
1285
if executability is not None:
1286
trans_id = self.tt.get_trans_id(file_id)
1287
self.tt.set_executability(executability, trans_id)
1289
def cook_conflicts(self):
1290
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1292
fp = FinalPaths(self.tt)
1293
for conflict in self._raw_conflicts:
1294
conflict_type = conflict[0]
1295
if conflict_type in ('name conflict', 'parent conflict'):
1296
trans_id = conflict[1]
1297
conflict_args = conflict[2:]
1298
if trans_id not in name_conflicts:
1299
name_conflicts[trans_id] = {}
1300
unique_add(name_conflicts[trans_id], conflict_type,
1302
if conflict_type == 'contents conflict':
1303
for trans_id in conflict[1]:
1304
file_id = self.tt.final_file_id(trans_id)
1305
if file_id is not None:
1307
path = fp.get_path(trans_id)
1308
for suffix in ('.BASE', '.THIS', '.OTHER'):
1309
if path.endswith(suffix):
1310
path = path[:-len(suffix)]
1312
self.cooked_conflicts.append((conflict_type, file_id, path))
1313
if conflict_type == 'text conflict':
1314
trans_id = conflict[1]
1315
path = fp.get_path(trans_id)
1316
file_id = self.tt.final_file_id(trans_id)
1317
self.cooked_conflicts.append((conflict_type, file_id, path))
1319
for trans_id, conflicts in name_conflicts.iteritems():
1321
this_parent, other_parent = conflicts['parent conflict']
1322
assert this_parent != other_parent
1324
this_parent = other_parent = \
1325
self.tt.final_file_id(self.tt.final_parent(trans_id))
1327
this_name, other_name = conflicts['name conflict']
1328
assert this_name != other_name
1330
this_name = other_name = self.tt.final_name(trans_id)
1331
other_path = fp.get_path(trans_id)
1332
if this_parent is not None:
1333
this_parent_path = \
1334
fp.get_path(self.tt.get_trans_id(this_parent))
1335
this_path = os.path.join(this_parent_path, this_name)
1337
this_path = "<deleted>"
1338
file_id = self.tt.final_file_id(trans_id)
1339
self.cooked_conflicts.append(('path conflict', file_id, this_path,
1343
class WeaveMerger(Merge3Merger):
1344
supports_reprocess = False
1345
supports_show_base = False
1347
def __init__(self, working_tree, this_tree, base_tree, other_tree):
1348
self.this_revision_tree = self._get_revision_tree(this_tree)
1349
self.other_revision_tree = self._get_revision_tree(other_tree)
1350
super(WeaveMerger, self).__init__(working_tree, this_tree,
1351
base_tree, other_tree)
1353
def _get_revision_tree(self, tree):
1354
if getattr(tree, 'get_weave', False) is False:
1355
# If we have a WorkingTree, try using the basis
1356
return tree.branch.basis_tree()
1360
def _check_file(self, file_id):
1361
"""Check that the revision tree's version of the file matches."""
1362
for tree, rt in ((self.this_tree, self.this_revision_tree),
1363
(self.other_tree, self.other_revision_tree)):
1366
if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
1367
raise WorkingTreeNotRevision(self.this_tree)
1369
def _merged_lines(self, file_id):
1370
"""Generate the merged lines.
1371
There is no distinction between lines that are meant to contain <<<<<<<
1374
weave = self.this_revision_tree.get_weave(file_id)
1375
this_revision_id = self.this_revision_tree.inventory[file_id].revision
1376
other_revision_id = \
1377
self.other_revision_tree.inventory[file_id].revision
1378
this_i = weave.lookup(this_revision_id)
1379
other_i = weave.lookup(other_revision_id)
1380
plan = weave.plan_merge(this_i, other_i)
1381
return weave.weave_merge(plan)
1383
def text_merge(self, file_id, trans_id):
1384
self._check_file(file_id)
1385
lines = self._merged_lines(file_id)
1386
conflicts = '<<<<<<<\n' in lines
1387
self.tt.create_file(lines, trans_id)
1389
self._raw_conflicts.append(('text conflict', trans_id))
1390
name = self.tt.final_name(trans_id)
1391
parent_id = self.tt.final_parent(trans_id)
1392
file_group = self._dump_conflicts(name, parent_id, file_id,
1394
file_group.append(trans_id)
1397
class Diff3Merger(Merge3Merger):
1398
"""Use good ol' diff3 to do text merges"""
1399
def dump_file(self, temp_dir, name, tree, file_id):
1400
out_path = pathjoin(temp_dir, name)
1401
out_file = file(out_path, "wb")
1402
in_file = tree.get_file(file_id)
1403
for line in in_file:
1404
out_file.write(line)
1407
def text_merge(self, file_id, trans_id):
1409
temp_dir = mkdtemp(prefix="bzr-")
1411
new_file = os.path.join(temp_dir, "new")
1412
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1413
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1414
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1415
status = bzrlib.patch.diff3(new_file, this, base, other)
1416
if status not in (0, 1):
1417
raise BzrError("Unhandled diff3 exit code")
1418
self.tt.create_file(file(new_file, "rb"), trans_id)
1420
name = self.tt.final_name(trans_id)
1421
parent_id = self.tt.final_parent(trans_id)
1422
self._dump_conflicts(name, parent_id, file_id)
1423
self._raw_conflicts.append(('text conflict', trans_id))