56
56
# TODO: Report back as changes are merged in
59
def transform_tree(from_tree, to_tree, interesting_ids=None):
59
def transform_tree(from_tree, to_tree, interesting_files=None):
60
60
from_tree.lock_tree_write()
61
61
operation = cleanup.OperationWithCleanups(merge_inner)
62
62
operation.add_cleanup(from_tree.unlock)
63
63
operation.run_simple(from_tree.branch, to_tree, from_tree,
64
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
64
ignore_zero=True, this_tree=from_tree,
65
interesting_files=interesting_files)
67
68
class MergeHooks(hooks.Hooks):
131
132
raise NotImplementedError(self.file_matches)
133
def get_filename(self, params, tree):
134
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
135
self.merger.this_tree) and a MergeFileHookParams.
137
return osutils.basename(tree.id2path(params.file_id))
139
def get_filepath(self, params, tree):
140
"""Calculate the path to the file in a tree.
142
:param params: A MergeFileHookParams describing the file to merge
143
:param tree: a Tree, e.g. self.merger.this_tree.
145
return tree.id2path(params.file_id)
147
134
def merge_contents(self, params):
148
135
"""Merge the contents of a single file."""
149
136
# Check whether this custom merge logic should be used.
240
227
There are some fields hooks can access:
242
229
:ivar file_id: the file ID of the file being merged
230
:ivar base_path: Path in base tree
231
:ivar other_path: Path in other tree
232
:ivar this_path: Path in this tree
243
233
:ivar trans_id: the transform ID for the merge of this file
244
234
:ivar this_kind: kind of file_id in 'this' tree
245
235
:ivar other_kind: kind of file_id in 'other' tree
246
236
:ivar winner: one of 'this', 'other', 'conflict'
249
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
239
def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
251
241
self._merger = merger
252
242
self.file_id = file_id
244
self.base_path, self.other_path, self.this_path = paths
253
245
self.trans_id = trans_id
254
246
self.this_kind = this_kind
255
247
self.other_kind = other_kind
262
254
@decorators.cachedproperty
263
255
def base_lines(self):
264
256
"""The lines of the 'base' version of the file."""
265
return self._merger.get_lines(self._merger.base_tree, self.file_id)
257
return self._merger.get_lines(self._merger.base_tree, self.base_path, self.file_id)
267
259
@decorators.cachedproperty
268
260
def this_lines(self):
269
261
"""The lines of the 'this' version of the file."""
270
return self._merger.get_lines(self._merger.this_tree, self.file_id)
262
return self._merger.get_lines(self._merger.this_tree, self.this_path, self.file_id)
272
264
@decorators.cachedproperty
273
265
def other_lines(self):
274
266
"""The lines of the 'other' version of the file."""
275
return self._merger.get_lines(self._merger.other_tree, self.file_id)
267
return self._merger.get_lines(self._merger.other_tree, self.other_path, self.file_id)
278
270
class Merger(object):
610
601
def make_merger(self):
611
602
kwargs = {'working_tree': self.this_tree, 'this_tree': self.this_tree,
612
603
'other_tree': self.other_tree,
613
'interesting_ids': self.interesting_ids,
614
604
'interesting_files': self.interesting_files,
615
605
'this_branch': self.this_branch,
616
606
'other_branch': self.other_branch,
718
709
supports_reverse_cherrypick = True
719
710
winner_idx = {"this": 2, "other": 1, "conflict": 1}
720
711
supports_lca_trees = True
712
requires_file_merge_plan = False
722
714
def __init__(self, working_tree, this_tree, base_tree, other_tree,
723
interesting_ids=None, reprocess=False, show_base=False,
715
reprocess=False, show_base=False,
724
716
change_reporter=None, interesting_files=None, do_merge=True,
725
717
cherrypick=False, lca_trees=None, this_branch=None,
726
718
other_branch=None):
733
725
:param this_branch: The branch associated with this_tree. Defaults to
734
726
this_tree.branch if not supplied.
735
727
:param other_branch: The branch associated with other_tree, if any.
736
:param interesting_ids: The file_ids of files that should be
737
participate in the merge. May not be combined with
739
728
:param: reprocess If True, perform conflict-reduction processing.
740
729
:param show_base: If True, show the base revision in text conflicts.
741
730
(incompatible with reprocess)
742
731
:param change_reporter: An object that should report changes made
743
732
:param interesting_files: The tree-relative paths of files that should
744
733
participate in the merge. If these paths refer to directories,
745
the contents of those directories will also be included. May not
746
be combined with interesting_ids. If neither interesting_files nor
747
interesting_ids is specified, all files may participate in the
734
the contents of those directories will also be included. If not
735
specified, all files may participate in the
749
737
:param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
750
738
if the ancestry was found to include a criss-cross merge.
751
739
Otherwise should be None.
753
741
object.__init__(self)
754
if interesting_files is not None and interesting_ids is not None:
756
'specify either interesting_ids or interesting_files')
757
742
if this_branch is None:
758
743
this_branch = this_tree.branch
759
self.interesting_ids = interesting_ids
760
744
self.interesting_files = interesting_files
761
745
self.working_tree = working_tree
762
746
self.this_tree = this_tree
827
811
# One hook for each registered one plus our default merger
828
812
hooks = [factory(self) for factory in factories] + [self]
829
813
self.active_hooks = [hook for hook in hooks if hook is not None]
830
child_pb = ui.ui_factory.nested_progress_bar()
832
for num, (file_id, changed, parents3, names3,
814
with ui.ui_factory.nested_progress_bar() as child_pb:
815
for num, (file_id, changed, paths3, parents3, names3,
833
816
executable3) in enumerate(entries):
834
817
# Try merging each entry
835
818
child_pb.update(gettext('Preparing file merge'),
836
819
num, len(entries))
837
self._merge_names(file_id, parents3, names3, resolver=resolver)
820
self._merge_names(file_id, paths3, parents3, names3, resolver=resolver)
839
file_status = self._do_merge_contents(file_id)
822
file_status = self._do_merge_contents(paths3, file_id)
841
824
file_status = 'unmodified'
842
self._merge_executable(file_id,
843
executable3, file_status, resolver=resolver)
825
self._merge_executable(paths3, file_id, executable3,
826
file_status, resolver=resolver)
846
827
self.tt.fixup_new_roots()
847
828
self._finish_computing_transform()
852
833
This is the second half of _compute_transform.
854
child_pb = ui.ui_factory.nested_progress_bar()
835
with ui.ui_factory.nested_progress_bar() as child_pb:
856
836
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
857
837
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
860
838
if self.change_reporter is not None:
861
839
from breezy import delta
862
840
delta.report_changes(
878
856
iterator = self.other_tree.iter_changes(self.base_tree,
879
857
specific_files=self.interesting_files,
880
858
extra_trees=[self.this_tree])
881
this_entries = dict((e.file_id, e) for p, e in
882
self.this_tree.iter_entries_by_dir(
883
self.interesting_ids))
859
this_interesting_files = self.this_tree.find_related_paths_across_trees(
860
self.interesting_files, trees=[self.other_tree])
861
this_entries = dict(self.this_tree.iter_entries_by_dir(
862
specific_files=this_interesting_files))
884
863
for (file_id, paths, changed, versioned, parents, names, kind,
885
864
executable) in iterator:
886
if (self.interesting_ids is not None and
887
file_id not in self.interesting_ids):
889
entry = this_entries.get(file_id)
890
if entry is not None:
891
this_name = entry.name
892
this_parent = entry.parent_id
893
this_executable = entry.executable
865
if paths[0] is not None:
866
this_path = _mod_tree.find_previous_path(
867
self.base_tree, self.this_tree, paths[0])
869
this_path = _mod_tree.find_previous_path(
870
self.other_tree, self.this_tree, paths[1])
871
this_entry = this_entries.get(this_path)
872
if this_entry is not None:
873
this_name = this_entry.name
874
this_parent = this_entry.parent_id
875
this_executable = this_entry.executable
896
878
this_parent = None
897
879
this_executable = None
898
880
parents3 = parents + (this_parent,)
899
881
names3 = names + (this_name,)
882
paths3 = paths + (this_path, )
900
883
executable3 = executable + (this_executable,)
901
result.append((file_id, changed, parents3, names3, executable3))
884
result.append((file_id, changed, paths3, parents3, names3, executable3))
904
887
def _entries_lca(self):
910
893
For the multi-valued entries, the format will be (BASE, [lca1, lca2])
912
:return: [(file_id, changed, parents, names, executable)], where:
895
:return: [(file_id, changed, paths, parents, names, executable)], where:
914
897
* file_id: Simple file_id of the entry
915
898
* changed: Boolean, True if the kind or contents changed else False
899
* paths: ((base, [path, in, lcas]), path_other, path_this)
916
900
* parents: ((base, [parent_id, in, lcas]), parent_id_other,
918
902
* names: ((base, [name, in, lcas]), name_in_other, name_in_this)
923
907
lookup_trees = [self.this_tree, self.base_tree]
924
908
lookup_trees.extend(self._lca_trees)
925
909
# I think we should include the lca trees as well
926
interesting_ids = self.other_tree.paths2ids(self.interesting_files,
910
interesting_files = self.other_tree.find_related_paths_across_trees(
911
self.interesting_files, lookup_trees)
929
interesting_ids = self.interesting_ids
913
interesting_files = None
931
915
walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
962
950
for lca_path, lca_ie in lca_values:
963
951
if lca_ie is None:
964
952
lca_entries.append(_none_entry)
953
lca_paths.append(None)
966
955
lca_entries.append(lca_ie)
956
lca_paths.append(path)
968
if base_inventory.has_id(file_id):
969
959
base_ie = base_inventory[file_id]
960
except errors.NoSuchId:
971
961
base_ie = _none_entry
964
base_path = self.base_tree.id2path(file_id)
973
if this_inventory.has_id(file_id):
974
967
this_ie = this_inventory[file_id]
968
except errors.NoSuchId:
976
969
this_ie = _none_entry
972
this_path = self.this_tree.id2path(file_id)
979
975
lca_parent_ids = []
1005
1001
content_changed = False
1006
1002
elif other_ie.kind is None or other_ie.kind == 'file':
1007
def get_sha1(ie, tree):
1008
if ie.kind != 'file':
1010
return tree.get_file_sha1(tree.id2path(file_id), file_id)
1011
base_sha1 = get_sha1(base_ie, self.base_tree)
1012
lca_sha1s = [get_sha1(ie, tree) for ie, tree
1013
in zip(lca_entries, self._lca_trees)]
1014
this_sha1 = get_sha1(this_ie, self.this_tree)
1015
other_sha1 = get_sha1(other_ie, self.other_tree)
1003
def get_sha1(tree, path):
1007
return tree.get_file_sha1(path, file_id)
1008
except errors.NoSuchFile:
1010
base_sha1 = get_sha1(self.base_tree, base_path)
1011
lca_sha1s = [get_sha1(tree, lca_path) for tree, lca_path
1012
in zip(self._lca_trees, lca_paths)]
1013
this_sha1 = get_sha1(self.this_tree, this_path)
1014
other_sha1 = get_sha1(self.other_tree, other_path)
1016
1015
sha1_winner = self._lca_multi_way(
1017
1016
(base_sha1, lca_sha1s), other_sha1, this_sha1,
1018
1017
allow_overriding_lca=False)
1027
1026
if sha1_winner == 'this':
1028
1027
content_changed = False
1029
1028
elif other_ie.kind == 'symlink':
1030
def get_target(ie, tree):
1029
def get_target(ie, tree, path):
1031
1030
if ie.kind != 'symlink':
1033
path = tree.id2path(file_id)
1034
1032
return tree.get_symlink_target(path, file_id)
1035
base_target = get_target(base_ie, self.base_tree)
1036
lca_targets = [get_target(ie, tree) for ie, tree
1037
in zip(lca_entries, self._lca_trees)]
1038
this_target = get_target(this_ie, self.this_tree)
1039
other_target = get_target(other_ie, self.other_tree)
1033
base_target = get_target(base_ie, self.base_tree, base_path)
1034
lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
1035
in zip(lca_entries, self._lca_trees, lca_paths)]
1036
this_target = get_target(this_ie, self.this_tree, this_path)
1037
other_target = get_target(other_ie, self.other_tree, other_path)
1040
1038
target_winner = self._lca_multi_way(
1041
1039
(base_target, lca_targets),
1042
1040
other_target, this_target)
1061
1059
# If we have gotten this far, that means something has changed
1062
1060
result.append((file_id, content_changed,
1061
((base_path, lca_paths),
1062
other_path, this_path),
1063
1063
((base_ie.parent_id, lca_parent_ids),
1064
1064
other_ie.parent_id, this_ie.parent_id),
1065
1065
((base_ie.name, lca_names),
1097
1099
return entry.name
1100
def contents_sha1(tree, file_id):
1102
def contents_sha1(tree, path, file_id=None):
1101
1103
"""Determine the sha1 of the file contents (used as a key method)."""
1103
path = tree.id2path(file_id)
1104
except errors.NoSuchId:
1105
return tree.get_file_sha1(path, file_id)
1106
except errors.NoSuchFile:
1106
return tree.get_file_sha1(path, file_id)
1109
def executable(tree, file_id):
1110
def executable(tree, path, file_id=None):
1110
1111
"""Determine the executability of a file-id (used as a key method)."""
1112
path = tree.id2path(file_id)
1113
except errors.NoSuchId:
1113
if tree.kind(path, file_id) != "file":
1115
except errors.NoSuchFile:
1115
if tree.kind(path, file_id) != "file":
1117
1117
return tree.is_executable(path, file_id)
1120
def kind(tree, path, file_id):
1120
def kind(tree, path, file_id=None):
1121
1121
"""Determine the kind of a file-id (used as a key method)."""
1123
path = tree.id2path(file_id)
1124
except errors.NoSuchId:
1123
return tree.kind(path, file_id)
1124
except errors.NoSuchFile:
1126
return tree.kind(path, file_id)
1129
1128
def _three_way(base, other, this):
1211
1210
names.append(entry.name)
1212
1211
parents.append(entry.parent_id)
1213
return self._merge_names(file_id, parents, names,
1212
return self._merge_names(file_id, paths, parents, names,
1214
1213
resolver=self._three_way)
1216
def _merge_names(self, file_id, parents, names, resolver):
1215
def _merge_names(self, file_id, paths, parents, names, resolver):
1217
1216
"""Perform a merge on file_id names and parents"""
1218
1217
base_name, other_name, this_name = names
1219
1218
base_parent, other_parent, this_parent = parents
1219
unused_base_path, other_path, this_path = paths
1221
1221
name_winner = resolver(*names)
1258
1258
self.tt.adjust_path(name, parent_trans_id,
1259
1259
self.tt.trans_id_file_id(file_id))
1261
def _do_merge_contents(self, file_id):
1261
def _do_merge_contents(self, paths, file_id):
1262
1262
"""Performs a merge on file_id contents."""
1263
def contents_pair(tree):
1265
path = tree.id2path(file_id)
1266
except errors.NoSuchId:
1263
def contents_pair(tree, path):
1267
1265
return (None, None)
1269
1267
kind = tree.kind(path, file_id)
1277
1275
contents = None
1278
1276
return kind, contents
1278
base_path, other_path, this_path = paths
1280
1279
# See SPOT run. run, SPOT, run.
1281
1280
# So we're not QUITE repeating ourselves; we do tricky things with
1283
base_pair = contents_pair(self.base_tree)
1284
other_pair = contents_pair(self.other_tree)
1282
other_pair = contents_pair(self.other_tree, other_path)
1283
this_pair = contents_pair(self.this_tree, this_path)
1285
1284
if self._lca_trees:
1286
this_pair = contents_pair(self.this_tree)
1287
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1285
(base_path, lca_paths) = base_path
1286
base_pair = contents_pair(self.base_tree, base_path)
1287
lca_pairs = [contents_pair(tree, path)
1288
for tree, path in zip(self._lca_trees, lca_paths)]
1288
1289
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1289
1290
this_pair, allow_overriding_lca=False)
1292
base_pair = contents_pair(self.base_tree, base_path)
1291
1293
if base_pair == other_pair:
1292
1294
winner = 'this'
1294
1296
# We delayed evaluating this_pair as long as we can to avoid
1295
1297
# unnecessary sha1 calculation
1296
this_pair = contents_pair(self.this_tree)
1298
this_pair = contents_pair(self.this_tree, this_path)
1297
1299
winner = self._three_way(base_pair, other_pair, this_pair)
1298
1300
if winner == 'this':
1299
1301
# No interesting changes introduced by OTHER
1301
1303
# We have a hypothetical conflict, but if we have files, then we
1302
1304
# can try to merge the content
1303
1305
trans_id = self.tt.trans_id_file_id(file_id)
1304
params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
1306
params = MergeFileHookParams(
1307
self, file_id, (base_path, other_path,
1308
this_path), trans_id, this_pair[0],
1305
1309
other_pair[0], winner)
1306
1310
hooks = self.active_hooks
1307
1311
hook_status = 'not_applicable'
1335
1337
self.tt.version_file(file_id, trans_id)
1336
1338
transform.create_from_tree(
1337
1339
self.tt, trans_id, self.other_tree,
1338
self.other_tree.id2path(file_id), file_id=file_id,
1340
other_path, file_id=file_id,
1339
1341
filter_tree_path=self._get_filter_tree_path(file_id))
1340
1342
inhibit_content_conflict = True
1341
1343
elif params.other_kind is None: # file_id is not in OTHER
1342
1344
# Is the name used for a different file_id ?
1343
dupe_path = self.this_tree.id2path(file_id)
1344
other_id = self.other_tree.path2id(dupe_path)
1345
if other_id is not None:
1345
if self.other_tree.is_versioned(this_path):
1346
1346
# Two entries for the same path again, but here, the other
1347
1347
# entry will also be merged. We simply inhibit the
1348
1348
# 'content' conflict creation because we know OTHER will
1356
1356
self.tt.unversion_file(trans_id)
1357
1357
# This is a contents conflict, because none of the available
1358
1358
# functions could merge it.
1359
file_group = self._dump_conflicts(name, parent_id, file_id,
1359
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1360
1360
set_version=True)
1361
1361
self._raw_conflicts.append(('contents conflict', file_group))
1362
1362
elif hook_status == 'success':
1368
1368
self._raw_conflicts.append(('text conflict', trans_id))
1369
1369
name = self.tt.final_name(trans_id)
1370
1370
parent_id = self.tt.final_parent(trans_id)
1371
self._dump_conflicts(name, parent_id, file_id)
1371
self._dump_conflicts(name, paths, parent_id, file_id)
1372
1372
elif hook_status == 'delete':
1373
1373
self.tt.unversion_file(trans_id)
1374
1374
result = "deleted"
1390
1390
"""Replace this contents with other."""
1391
1391
file_id = merge_hook_params.file_id
1392
1392
trans_id = merge_hook_params.trans_id
1393
if self.other_tree.has_id(file_id):
1393
if merge_hook_params.other_path is not None:
1394
1394
# OTHER changed the file
1395
1395
transform.create_from_tree(
1396
1396
self.tt, trans_id, self.other_tree,
1397
self.other_tree.id2path(file_id), file_id=file_id,
1397
merge_hook_params.other_path, file_id=file_id,
1398
1398
filter_tree_path=self._get_filter_tree_path(file_id))
1399
1399
return 'done', None
1400
elif self.this_tree.has_id(file_id):
1400
elif merge_hook_params.this_path is not None:
1401
1401
# OTHER deleted the file
1402
1402
return 'delete', None
1408
1408
def merge_contents(self, merge_hook_params):
1409
1409
"""Fallback merge logic after user installed hooks."""
1410
1410
# This function is used in merge hooks as the fallback instance.
1411
# Perhaps making this function and the functions it calls be a
1411
# Perhaps making this function and the functions it calls be a
1412
1412
# a separate class would be better.
1413
1413
if merge_hook_params.winner == 'other':
1414
1414
# OTHER is a straight winner, so replace this contents with other
1418
1418
# BASE is a file, or both converted to files, so at least we
1419
1419
# have agreement that output should be a file.
1421
self.text_merge(merge_hook_params.file_id,
1422
merge_hook_params.trans_id)
1421
self.text_merge(merge_hook_params.trans_id,
1422
merge_hook_params.paths, merge_hook_params.file_id)
1423
1423
except errors.BinaryFile:
1424
1424
return 'not_applicable', None
1425
1425
return 'done', None
1427
1427
return 'not_applicable', None
1429
def get_lines(self, tree, file_id):
1429
def get_lines(self, tree, path, file_id=None):
1430
1430
"""Return the lines in a file, or an empty list."""
1432
path = tree.id2path(file_id)
1433
except errors.NoSuchId:
1434
kind = tree.kind(path, file_id)
1435
except errors.NoSuchFile:
1436
1440
return tree.get_file_lines(path, file_id)
1438
def text_merge(self, file_id, trans_id):
1442
def text_merge(self, trans_id, paths, file_id):
1439
1443
"""Perform a three-way text merge on a file_id"""
1440
1444
# it's possible that we got here with base as a different type.
1441
1445
# if so, we just want two-way text conflicts.
1443
base_path = self.base_tree.id2path(file_id)
1444
except errors.NoSuchId:
1447
if self.base_tree.kind(base_path, file_id) == "file":
1448
base_lines = self.get_lines(self.base_tree, file_id)
1451
other_lines = self.get_lines(self.other_tree, file_id)
1452
this_lines = self.get_lines(self.this_tree, file_id)
1446
base_path, other_path, this_path = paths
1447
base_lines = self.get_lines(self.base_tree, base_path, file_id)
1448
other_lines = self.get_lines(self.other_tree, other_path, file_id)
1449
this_lines = self.get_lines(self.this_tree, this_path, file_id)
1453
1450
m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1454
1451
is_cherrypick=self.cherrypick)
1455
1452
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1478
1475
self._raw_conflicts.append(('text conflict', trans_id))
1479
1476
name = self.tt.final_name(trans_id)
1480
1477
parent_id = self.tt.final_parent(trans_id)
1481
file_group = self._dump_conflicts(name, parent_id, file_id,
1478
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1482
1479
this_lines, base_lines,
1484
1481
file_group.append(trans_id)
1487
1483
def _get_filter_tree_path(self, file_id):
1488
1484
if self.this_tree.supports_content_filtering():
1489
1485
# We get the path from the working tree if it exists.
1497
1493
# Skip the id2path lookup for older formats
1500
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1496
def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1501
1497
base_lines=None, other_lines=None, set_version=False,
1502
1498
no_base=False):
1503
1499
"""Emit conflict files.
1505
1501
determined automatically. If set_version is true, the .OTHER, .THIS
1506
1502
or .BASE (in that order) will be created as versioned files.
1508
data = [('OTHER', self.other_tree, other_lines),
1509
('THIS', self.this_tree, this_lines)]
1504
base_path, other_path, this_path = paths
1505
data = [('OTHER', self.other_tree, other_path, other_lines),
1506
('THIS', self.this_tree, this_path, this_lines)]
1510
1507
if not no_base:
1511
data.append(('BASE', self.base_tree, base_lines))
1508
data.append(('BASE', self.base_tree, base_path, base_lines))
1513
1510
# We need to use the actual path in the working tree of the file here,
1514
1511
# ignoring the conflict suffixes
1526
1523
versioned = False
1527
1524
file_group = []
1528
for suffix, tree, lines in data:
1530
path = tree.id2path(file_id)
1531
except errors.NoSuchId:
1525
for suffix, tree, path, lines in data:
1526
if path is not None:
1534
1527
trans_id = self._conflict_file(
1535
1528
name, parent_id, path, tree, file_id, suffix, lines,
1536
1529
filter_tree_path)
1551
1544
filter_tree_path=filter_tree_path)
1552
1545
return trans_id
1554
def merge_executable(self, file_id, file_status):
1547
def merge_executable(self, paths, file_id, file_status):
1555
1548
"""Perform a merge on the execute bit."""
1556
executable = [self.executable(t, file_id) for t in (self.base_tree,
1557
self.other_tree, self.this_tree)]
1558
self._merge_executable(file_id, executable, file_status,
1549
executable = [self.executable(t, p, file_id) for t, p in zip([self.base_tree,
1550
self.other_tree, self.this_tree], paths)]
1551
self._merge_executable(paths, file_id, executable, file_status,
1559
1552
resolver=self._three_way)
1561
def _merge_executable(self, file_id, executable, file_status,
1554
def _merge_executable(self, paths, file_id, executable, file_status,
1563
1556
"""Perform a merge on the execute bit."""
1564
1557
base_executable, other_executable, this_executable = executable
1558
base_path, other_path, this_path = paths
1565
1559
if file_status == "deleted":
1567
1561
winner = resolver(*executable)
1568
1562
if winner == "conflict":
1569
1563
# There must be a None in here, if we have a conflict, but we
1570
1564
# need executability since file status was not deleted.
1571
if self.executable(self.other_tree, file_id) is None:
1565
if self.executable(self.other_tree, other_path, file_id) is None:
1572
1566
winner = "this"
1574
1568
winner = "other"
1580
1574
if winner == "this":
1581
1575
executability = this_executable
1583
if self.other_tree.has_id(file_id):
1577
if other_path is not None:
1584
1578
executability = other_executable
1585
elif self.this_tree.has_id(file_id):
1579
elif this_path is not None:
1586
1580
executability = this_executable
1587
elif self.base_tree_has_id(file_id):
1581
elif base_path is not None:
1588
1582
executability = base_executable
1589
1583
if executability is not None:
1590
1584
trans_id = self.tt.trans_id_file_id(file_id)
1697
1692
base_lines = None
1698
1693
return lines, base_lines
1700
def text_merge(self, file_id, trans_id):
1695
def text_merge(self, trans_id, paths, file_id):
1701
1696
"""Perform a (weave) text merge for a given file and file-id.
1702
1697
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1703
1698
and a conflict will be noted.
1700
base_path, other_path, this_path = paths
1705
1701
lines, base_lines = self._merged_lines(file_id)
1706
1702
lines = list(lines)
1707
1703
# Note we're checking whether the OUTPUT is binary in this case,
1713
1709
self._raw_conflicts.append(('text conflict', trans_id))
1714
1710
name = self.tt.final_name(trans_id)
1715
1711
parent_id = self.tt.final_parent(trans_id)
1716
file_group = self._dump_conflicts(name, parent_id, file_id,
1712
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1718
1714
base_lines=base_lines)
1719
1715
file_group.append(trans_id)
1728
1726
class Diff3Merger(Merge3Merger):
1729
1727
"""Three-way merger using external diff3 for text merging"""
1731
def dump_file(self, temp_dir, name, tree, file_id):
1729
requires_file_merge_plan = False
1731
def dump_file(self, temp_dir, name, tree, path, file_id=None):
1732
1732
out_path = osutils.pathjoin(temp_dir, name)
1733
out_file = open(out_path, "wb")
1735
in_file = tree.get_file(tree.id2path(file_id), file_id)
1733
with open(out_path, "wb") as out_file:
1734
in_file = tree.get_file(path, file_id=None)
1736
1735
for line in in_file:
1737
1736
out_file.write(line)
1740
1737
return out_path
1742
def text_merge(self, file_id, trans_id):
1739
def text_merge(self, trans_id, paths, file_id):
1743
1740
"""Perform a diff3 merge using a specified file-id and trans-id.
1744
1741
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1745
1742
will be dumped, and a will be conflict noted.
1747
1744
import breezy.patch
1745
base_path, other_path, this_path = paths
1748
1746
temp_dir = osutils.mkdtemp(prefix="bzr-")
1750
1748
new_file = osutils.pathjoin(temp_dir, "new")
1751
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1752
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1753
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1749
this = self.dump_file(temp_dir, "this", self.this_tree, this_path, file_id)
1750
base = self.dump_file(temp_dir, "base", self.base_tree, base_path, file_id)
1751
other = self.dump_file(temp_dir, "other", self.other_tree, other_path, file_id)
1754
1752
status = breezy.patch.diff3(new_file, this, base, other)
1755
1753
if status not in (0, 1):
1756
1754
raise errors.BzrError("Unhandled diff3 exit code")
1757
f = open(new_file, 'rb')
1755
with open(new_file, 'rb') as f:
1759
1756
self.tt.create_file(f, trans_id)
1762
1757
if status == 1:
1763
1758
name = self.tt.final_name(trans_id)
1764
1759
parent_id = self.tt.final_parent(trans_id)
1765
self._dump_conflicts(name, parent_id, file_id)
1760
self._dump_conflicts(name, paths, parent_id, file_id)
1766
1761
self._raw_conflicts.append(('text conflict', trans_id))
1768
1763
osutils.rmtree(temp_dir)
1878
1873
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1880
1875
def _compute_transform(self):
1881
child_pb = ui.ui_factory.nested_progress_bar()
1876
with ui.ui_factory.nested_progress_bar() as child_pb:
1883
1877
entries = self._entries_to_incorporate()
1884
1878
entries = list(entries)
1885
for num, (entry, parent_id, path) in enumerate(entries):
1879
for num, (entry, parent_id, relpath) in enumerate(entries):
1886
1880
child_pb.update(gettext('Preparing file merge'), num, len(entries))
1887
1881
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1882
path = osutils.pathjoin(self._source_subpath, relpath)
1888
1883
trans_id = transform.new_by_entry(path, self.tt, entry,
1889
1884
parent_trans_id, self.other_tree)
1892
1885
self._finish_computing_transform()
1894
1887
def _entries_to_incorporate(self):
1954
1946
change_reporter=change_reporter)
1955
1947
merger.backup_files = backup_files
1956
1948
merger.merge_type = merge_type
1957
merger.interesting_ids = interesting_ids
1958
1949
merger.ignore_zero = ignore_zero
1959
if interesting_files:
1961
raise ValueError('Only supply interesting_ids'
1962
' or interesting_files')
1963
merger.interesting_files = interesting_files
1950
merger.interesting_files = interesting_files
1964
1951
merger.show_base = show_base
1965
1952
merger.reprocess = reprocess
1966
1953
merger.other_rev_id = other_rev_id