224
224
There are some fields hooks can access:
226
:ivar file_id: the file ID of the file being merged
226
227
:ivar base_path: Path in base tree
227
228
:ivar other_path: Path in other tree
228
229
:ivar this_path: Path in this tree
229
230
:ivar trans_id: the transform ID for the merge of this file
230
:ivar this_kind: kind of file in 'this' tree
231
:ivar other_kind: kind of file in 'other' tree
231
:ivar this_kind: kind of file_id in 'this' tree
232
:ivar other_kind: kind of file_id in 'other' tree
232
233
:ivar winner: one of 'this', 'other', 'conflict'
235
def __init__(self, merger, paths, trans_id, this_kind, other_kind,
236
def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
237
238
self._merger = merger
239
self.file_id = file_id
238
240
self.paths = paths
239
241
self.base_path, self.other_path, self.this_path = paths
240
242
self.trans_id = trans_id
447
449
def _add_parent(self):
448
450
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
449
451
new_parent_trees = []
450
with cleanup.ExitStack() as stack:
451
for revision_id in new_parents:
453
tree = self.revision_tree(revision_id)
454
except errors.NoSuchRevision:
457
stack.enter_context(tree.lock_read())
458
new_parent_trees.append((revision_id, tree))
459
self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
452
operation = cleanup.OperationWithCleanups(
453
self.this_tree.set_parent_trees)
454
for revision_id in new_parents:
456
tree = self.revision_tree(revision_id)
457
except errors.NoSuchRevision:
461
operation.add_cleanup(tree.unlock)
462
new_parent_trees.append((revision_id, tree))
463
operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
461
465
def set_other(self, other_revision, possible_transports=None):
462
466
"""Set the revision and tree to merge from.
633
637
for hook in Merger.hooks['post_merge']:
635
639
if self.recurse == 'down':
636
for relpath in self.this_tree.iter_references():
640
for relpath, file_id in self.this_tree.iter_references():
637
641
sub_tree = self.this_tree.get_nested_tree(relpath)
638
642
other_revision = self.other_tree.get_reference_revision(
642
646
sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
643
647
sub_merge.merge_type = self.merge_type
644
other_branch = self.other_tree.reference_parent(relpath)
648
other_branch = self.other_branch.reference_parent(
645
650
sub_merge.set_other_revision(other_revision, other_branch)
646
651
base_tree_path = _mod_tree.find_previous_path(
647
652
self.this_tree, self.base_tree, relpath)
656
661
def do_merge(self):
657
with cleanup.ExitStack() as stack:
658
stack.enter_context(self.this_tree.lock_tree_write())
659
if self.base_tree is not None:
660
stack.enter_context(self.base_tree.lock_read())
661
if self.other_tree is not None:
662
stack.enter_context(self.other_tree.lock_read())
663
merge = self._do_merge_to()
662
operation = cleanup.OperationWithCleanups(self._do_merge_to)
663
self.this_tree.lock_tree_write()
664
operation.add_cleanup(self.this_tree.unlock)
665
if self.base_tree is not None:
666
self.base_tree.lock_read()
667
operation.add_cleanup(self.base_tree.unlock)
668
if self.other_tree is not None:
669
self.other_tree.lock_read()
670
operation.add_cleanup(self.other_tree.unlock)
671
merge = operation.run_simple()
664
672
if len(merge.cooked_conflicts) == 0:
665
673
if not self.ignore_zero and not trace.is_quiet():
666
674
trace.note(gettext("All changes applied successfully."))
759
764
def do_merge(self):
760
with cleanup.ExitStack() as stack:
761
stack.enter_context(self.working_tree.lock_tree_write())
762
stack.enter_context(self.this_tree.lock_read())
763
stack.enter_context(self.base_tree.lock_read())
764
stack.enter_context(self.other_tree.lock_read())
765
self.tt = self.working_tree.get_transform()
766
stack.enter_context(self.tt)
767
self._compute_transform()
768
results = self.tt.apply(no_conflicts=True)
769
self.write_modified(results)
771
self.working_tree.add_conflicts(self.cooked_conflicts)
772
except errors.UnsupportedOperation:
765
operation = cleanup.OperationWithCleanups(self._do_merge)
766
self.working_tree.lock_tree_write()
767
operation.add_cleanup(self.working_tree.unlock)
768
self.this_tree.lock_read()
769
operation.add_cleanup(self.this_tree.unlock)
770
self.base_tree.lock_read()
771
operation.add_cleanup(self.base_tree.unlock)
772
self.other_tree.lock_read()
773
operation.add_cleanup(self.other_tree.unlock)
776
def _do_merge(self, operation):
777
self.tt = transform.TreeTransform(self.working_tree, None)
778
operation.add_cleanup(self.tt.finalize)
779
self._compute_transform()
780
results = self.tt.apply(no_conflicts=True)
781
self.write_modified(results)
783
self.working_tree.add_conflicts(self.cooked_conflicts)
784
except errors.UnsupportedOperation:
775
787
def make_preview_transform(self):
776
788
with self.base_tree.lock_read(), self.other_tree.lock_read():
793
805
with ui.ui_factory.nested_progress_bar() as child_pb:
794
806
for num, (file_id, changed, paths3, parents3, names3,
795
807
executable3) in enumerate(entries):
796
trans_id = self.tt.trans_id_file_id(file_id)
798
808
# Try merging each entry
799
809
child_pb.update(gettext('Preparing file merge'),
800
810
num, len(entries))
801
self._merge_names(trans_id, file_id, paths3, parents3,
811
self._merge_names(file_id, paths3, parents3,
802
812
names3, resolver=resolver)
804
file_status = self._do_merge_contents(paths3, trans_id, file_id)
814
file_status = self._do_merge_contents(paths3, file_id)
806
816
file_status = 'unmodified'
807
self._merge_executable(paths3, trans_id, executable3,
817
self._merge_executable(paths3, file_id, executable3,
808
818
file_status, resolver=resolver)
809
819
self.tt.fixup_new_roots()
810
820
self._finish_computing_transform()
815
825
This is the second half of _compute_transform.
817
827
with ui.ui_factory.nested_progress_bar() as child_pb:
818
fs_conflicts = transform.resolve_conflicts(
820
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
828
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
829
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
821
830
if self.change_reporter is not None:
822
831
from breezy import delta
823
832
delta.report_changes(
843
852
self.interesting_files, trees=[self.other_tree])
844
853
this_entries = dict(self.this_tree.iter_entries_by_dir(
845
854
specific_files=this_interesting_files))
846
for change in iterator:
847
if change.path[0] is not None:
855
for (file_id, paths, changed, versioned, parents, names, kind,
856
executable) in iterator:
857
if paths[0] is not None:
848
858
this_path = _mod_tree.find_previous_path(
849
self.base_tree, self.this_tree, change.path[0])
859
self.base_tree, self.this_tree, paths[0])
851
861
this_path = _mod_tree.find_previous_path(
852
self.other_tree, self.this_tree, change.path[1])
862
self.other_tree, self.this_tree, paths[1])
853
863
this_entry = this_entries.get(this_path)
854
864
if this_entry is not None:
855
865
this_name = this_entry.name
860
870
this_parent = None
861
871
this_executable = None
862
parents3 = change.parent_id + (this_parent,)
863
names3 = change.name + (this_name,)
864
paths3 = change.path + (this_path, )
865
executable3 = change.executable + (this_executable,)
867
(change.file_id, change.changed_content, paths3,
868
parents3, names3, executable3))
872
parents3 = parents + (this_parent,)
873
names3 = names + (this_name,)
874
paths3 = paths + (this_path, )
875
executable3 = executable + (this_executable,)
876
result.append((file_id, changed, paths3,
877
parents3, names3, executable3))
871
880
def _entries_lca(self):
913
922
# we know that the ancestry is linear, and that OTHER did not
914
923
# modify anything
915
924
# See doc/developers/lca_merge_resolution.txt for details
916
# We can't use this shortcut when other_revision is None,
917
# because it may be None because things are WorkingTrees, and
918
# not because it is *actually* None.
919
is_unmodified = False
920
for lca_path, ie in lca_values:
921
if ie is not None and other_ie.is_unmodified(ie):
925
other_revision = other_ie.revision
926
if other_revision is not None:
927
# We can't use this shortcut when other_revision is None,
928
# because it may be None because things are WorkingTrees, and
929
# not because it is *actually* None.
930
is_unmodified = False
931
for lca_path, ie in lca_values:
932
if ie is not None and ie.revision == other_revision:
1057
1068
modified_hashes = {}
1058
1069
for path in results.modified_paths:
1059
1070
wt_relpath = self.working_tree.relpath(path)
1060
if not self.working_tree.is_versioned(wt_relpath):
1071
file_id = self.working_tree.path2id(wt_relpath)
1062
1074
hash = self.working_tree.get_file_sha1(wt_relpath)
1063
1075
if hash is None:
1065
modified_hashes[wt_relpath] = hash
1077
modified_hashes[file_id] = hash
1066
1078
self.working_tree.set_merge_modified(modified_hashes)
1171
1183
# At this point, the lcas disagree, and the tip disagree
1172
1184
return 'conflict'
1174
def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
1175
"""Perform a merge on file names and parents"""
1186
def _merge_names(self, file_id, paths, parents, names, resolver):
1187
"""Perform a merge on file_id names and parents"""
1176
1188
base_name, other_name, this_name = names
1177
1189
base_parent, other_parent, this_parent = parents
1178
1190
unused_base_path, other_path, this_path = paths
1191
1203
# Creating helpers (.OTHER or .THIS) here cause problems down the
1192
1204
# road if a ContentConflict needs to be created so we should not do
1206
trans_id = self.tt.trans_id_file_id(file_id)
1194
1207
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1195
1208
this_parent, this_name,
1196
1209
other_parent, other_name))
1213
1226
parent_trans_id = transform.ROOT_PARENT
1215
1228
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1216
self.tt.adjust_path(name, parent_trans_id, trans_id)
1229
self.tt.adjust_path(name, parent_trans_id,
1230
self.tt.trans_id_file_id(file_id))
1218
def _do_merge_contents(self, paths, trans_id, file_id):
1232
def _do_merge_contents(self, paths, file_id):
1219
1233
"""Performs a merge on file_id contents."""
1220
1234
def contents_pair(tree, path):
1221
1235
if path is None:
1259
1273
return "unmodified"
1260
1274
# We have a hypothetical conflict, but if we have files, then we
1261
1275
# can try to merge the content
1276
trans_id = self.tt.trans_id_file_id(file_id)
1262
1277
params = MergeFileHookParams(
1263
self, (base_path, other_path, this_path), trans_id, this_pair[0],
1278
self, file_id, (base_path, other_path,
1279
this_path), trans_id, this_pair[0],
1264
1280
other_pair[0], winner)
1265
1281
hooks = self.active_hooks
1266
1282
hook_status = 'not_applicable'
1291
1307
self.tt.version_file(file_id, trans_id)
1292
1308
transform.create_from_tree(
1293
1309
self.tt, trans_id, self.other_tree,
1295
filter_tree_path=self._get_filter_tree_path(other_path))
1310
other_path, file_id=file_id,
1311
filter_tree_path=self._get_filter_tree_path(file_id))
1296
1312
inhibit_content_conflict = True
1297
1313
elif params.other_kind is None: # file_id is not in OTHER
1298
1314
# Is the name used for a different file_id ?
1345
1361
def _default_other_winner_merge(self, merge_hook_params):
1346
1362
"""Replace this contents with other."""
1363
file_id = merge_hook_params.file_id
1347
1364
trans_id = merge_hook_params.trans_id
1348
1365
if merge_hook_params.other_path is not None:
1349
1366
# OTHER changed the file
1350
1367
transform.create_from_tree(
1351
1368
self.tt, trans_id, self.other_tree,
1352
merge_hook_params.other_path,
1353
filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
1369
merge_hook_params.other_path, file_id=file_id,
1370
filter_tree_path=self._get_filter_tree_path(file_id))
1354
1371
return 'done', None
1355
1372
elif merge_hook_params.this_path is not None:
1356
1373
# OTHER deleted the file
1357
1374
return 'delete', None
1359
1376
raise AssertionError(
1360
'winner is OTHER, but file %r not in THIS or OTHER tree'
1361
% (merge_hook_params.base_path,))
1377
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1363
1380
def merge_contents(self, merge_hook_params):
1364
1381
"""Fallback merge logic after user installed hooks."""
1374
1391
# have agreement that output should be a file.
1376
1393
self.text_merge(merge_hook_params.trans_id,
1377
merge_hook_params.paths)
1394
merge_hook_params.paths, merge_hook_params.file_id)
1378
1395
except errors.BinaryFile:
1379
1396
return 'not_applicable', None
1380
1397
return 'done', None
1395
1412
return tree.get_file_lines(path)
1397
def text_merge(self, trans_id, paths):
1398
"""Perform a three-way text merge on a file"""
1414
def text_merge(self, trans_id, paths, file_id):
1415
"""Perform a three-way text merge on a file_id"""
1399
1416
# it's possible that we got here with base as a different type.
1400
1417
# if so, we just want two-way text conflicts.
1401
1418
base_path, other_path, this_path = paths
1430
1447
self._raw_conflicts.append(('text conflict', trans_id))
1431
1448
name = self.tt.final_name(trans_id)
1432
1449
parent_id = self.tt.final_parent(trans_id)
1433
file_id = self.tt.final_file_id(trans_id)
1434
1450
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1435
1451
this_lines, base_lines,
1437
1453
file_group.append(trans_id)
1439
def _get_filter_tree_path(self, path):
1455
def _get_filter_tree_path(self, file_id):
1440
1456
if self.this_tree.supports_content_filtering():
1441
1457
# We get the path from the working tree if it exists.
1442
1458
# That fails though when OTHER is adding a file, so
1443
1459
# we fall back to the other tree to find the path if
1444
1460
# it doesn't exist locally.
1445
filter_path = _mod_tree.find_previous_path(
1446
self.other_tree, self.working_tree, path)
1447
if filter_path is None:
1450
# Skip the lookup for older formats
1462
return self.this_tree.id2path(file_id)
1463
except errors.NoSuchId:
1464
return self.other_tree.id2path(file_id)
1465
# Skip the id2path lookup for older formats
1453
1468
def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1482
1497
for suffix, tree, path, lines in data:
1483
1498
if path is not None:
1484
1499
trans_id = self._conflict_file(
1485
name, parent_id, path, tree, suffix, lines,
1500
name, parent_id, path, tree, file_id, suffix, lines,
1486
1501
filter_tree_path)
1487
1502
file_group.append(trans_id)
1488
1503
if set_version and not versioned:
1490
1505
versioned = True
1491
1506
return file_group
1493
def _conflict_file(self, name, parent_id, path, tree, suffix,
1508
def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1494
1509
lines=None, filter_tree_path=None):
1495
1510
"""Emit a single conflict file."""
1496
1511
name = name + '.' + suffix
1497
1512
trans_id = self.tt.create_path(name, parent_id)
1498
1513
transform.create_from_tree(
1499
1514
self.tt, trans_id, tree, path,
1515
file_id=file_id, chunks=lines,
1501
1516
filter_tree_path=filter_tree_path)
1502
1517
return trans_id
1504
def _merge_executable(self, paths, trans_id, executable, file_status,
1519
def merge_executable(self, paths, file_id, file_status):
1520
"""Perform a merge on the execute bit."""
1521
executable = [self.executable(t, p, file_id)
1522
for t, p in zip([self.base_tree, self.other_tree, self.this_tree], paths)]
1523
self._merge_executable(paths, file_id, executable, file_status,
1524
resolver=self._three_way)
1526
def _merge_executable(self, paths, file_id, executable, file_status,
1506
1528
"""Perform a merge on the execute bit."""
1507
1529
base_executable, other_executable, this_executable = executable
1530
1553
elif base_path is not None:
1531
1554
executability = base_executable
1532
1555
if executability is not None:
1556
trans_id = self.tt.trans_id_file_id(file_id)
1533
1557
self.tt.set_executability(executability, trans_id)
1535
1559
def cook_conflicts(self, fs_conflicts):
1552
1576
if other_parent is None or other_name is None:
1553
1577
other_path = '<deleted>'
1555
if other_parent == self.other_tree.path2id(''):
1579
if other_parent == self.other_tree.get_root_id():
1556
1580
# The tree transform doesn't know about the other root,
1557
1581
# so we special case here to avoid a NoFinalPath
1611
1635
history_based = True
1612
1636
requires_file_merge_plan = True
1614
def _generate_merge_plan(self, this_path, base):
1615
return self.this_tree.plan_file_merge(this_path, self.other_tree,
1638
def _generate_merge_plan(self, file_id, base):
1639
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1618
def _merged_lines(self, this_path):
1642
def _merged_lines(self, file_id):
1619
1643
"""Generate the merged lines.
1620
1644
There is no distinction between lines that are meant to contain <<<<<<<
1624
1648
base = self.base_tree
1627
plan = self._generate_merge_plan(this_path, base)
1651
plan = self._generate_merge_plan(file_id, base)
1628
1652
if 'merge' in debug.debug_flags:
1629
1653
plan = list(plan)
1630
1654
trans_id = self.tt.trans_id_file_id(file_id)
1640
1664
base_lines = None
1641
1665
return lines, base_lines
1643
def text_merge(self, trans_id, paths):
1667
def text_merge(self, trans_id, paths, file_id):
1644
1668
"""Perform a (weave) text merge for a given file and file-id.
1645
1669
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1646
1670
and a conflict will be noted.
1648
1672
base_path, other_path, this_path = paths
1649
lines, base_lines = self._merged_lines(this_path)
1673
lines, base_lines = self._merged_lines(file_id)
1650
1674
lines = list(lines)
1651
1675
# Note we're checking whether the OUTPUT is binary in this case,
1652
1676
# because we don't want to get into weave merge guts.
1657
1681
self._raw_conflicts.append(('text conflict', trans_id))
1658
1682
name = self.tt.final_name(trans_id)
1659
1683
parent_id = self.tt.final_parent(trans_id)
1660
file_id = self.tt.final_file_id(trans_id)
1661
1684
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1663
1686
base_lines=base_lines)
1669
1692
requires_file_merge_plan = True
1671
def _generate_merge_plan(self, this_path, base):
1672
return self.this_tree.plan_file_lca_merge(this_path, self.other_tree,
1694
def _generate_merge_plan(self, file_id, base):
1695
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1686
1709
out_file.write(line)
1687
1710
return out_path
1689
def text_merge(self, trans_id, paths):
1712
def text_merge(self, trans_id, paths, file_id):
1690
1713
"""Perform a diff3 merge using a specified file-id and trans-id.
1691
1714
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1692
1715
will be dumped, and a will be conflict noted.
1710
1733
if status == 1:
1711
1734
name = self.tt.final_name(trans_id)
1712
1735
parent_id = self.tt.final_parent(trans_id)
1713
file_id = self.tt.final_file_id(trans_id)
1714
1736
self._dump_conflicts(name, paths, parent_id, file_id)
1715
1737
self._raw_conflicts.append(('text conflict', trans_id))
1855
1877
name_in_target = osutils.basename(self._target_subdir)
1856
1878
merge_into_root = subdir.copy()
1857
1879
merge_into_root.name = name_in_target
1859
self.this_tree.id2path(merge_into_root.file_id)
1860
except errors.NoSuchId:
1880
if self.this_tree.has_id(merge_into_root.file_id):
1863
1881
# Give the root a new file-id.
1864
1882
# This can happen fairly easily if the directory we are
1865
1883
# incorporating is the root, and both trees have 'TREE_ROOT' as
2007
2025
for record in self.vf.get_record_stream(keys, 'unordered', True):
2008
2026
if record.storage_kind == 'absent':
2009
2027
raise errors.RevisionNotPresent(record.key, self.vf)
2010
result[record.key[-1]] = record.get_bytes_as('lines')
2028
result[record.key[-1]] = osutils.chunks_to_lines(
2029
record.get_bytes_as('chunked'))
2013
2032
def plan_merge(self):