56
51
# TODO: Report back as changes are merged in
59
def transform_tree(from_tree, to_tree, interesting_ids=None):
60
from_tree.lock_tree_write()
61
operation = cleanup.OperationWithCleanups(merge_inner)
62
operation.add_cleanup(from_tree.unlock)
63
operation.run_simple(from_tree.branch, to_tree, from_tree,
64
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
54
def transform_tree(from_tree, to_tree, interesting_files=None):
55
with from_tree.lock_tree_write():
56
merge_inner(from_tree.branch, to_tree, from_tree,
57
ignore_zero=True, this_tree=from_tree,
58
interesting_files=interesting_files)
67
61
class MergeHooks(hooks.Hooks):
69
63
def __init__(self):
70
64
hooks.Hooks.__init__(self, "breezy.merge", "Merger.hooks")
71
65
self.add_hook('merge_file_content',
72
"Called with a breezy.merge.Merger object to create a per file "
73
"merge object when starting a merge. "
74
"Should return either None or a subclass of "
75
"``breezy.merge.AbstractPerFileMerger``. "
76
"Such objects will then be called per file "
77
"that needs to be merged (including when one "
78
"side has deleted the file and the other has changed it). "
79
"See the AbstractPerFileMerger API docs for details on how it is "
66
"Called with a breezy.merge.Merger object to create a per file "
67
"merge object when starting a merge. "
68
"Should return either None or a subclass of "
69
"``breezy.merge.AbstractPerFileMerger``. "
70
"Such objects will then be called per file "
71
"that needs to be merged (including when one "
72
"side has deleted the file and the other has changed it). "
73
"See the AbstractPerFileMerger API docs for details on how it is "
82
76
self.add_hook('pre_merge',
83
'Called before a merge. '
84
'Receives a Merger object as the single argument.',
77
'Called before a merge. '
78
'Receives a Merger object as the single argument.',
86
80
self.add_hook('post_merge',
87
'Called after a merge. '
88
'Receives a Merger object as the single argument. '
89
'The return value is ignored.',
81
'Called after a merge. '
82
'Receives a Merger object as the single argument. '
83
'The return value is ignored.',
93
87
class AbstractPerFileMerger(object):
94
88
"""PerFileMerger objects are used by plugins extending merge for breezy.
96
90
See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
98
92
:ivar merger: The Merge3Merger performing the merge.
131
125
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
127
def merge_contents(self, params):
148
128
"""Merge the contents of a single file."""
149
129
# Check whether this custom merge logic should be used.
240
220
There are some fields hooks can access:
242
:ivar file_id: the file ID of the file being merged
222
:ivar base_path: Path in base tree
223
:ivar other_path: Path in other tree
224
:ivar this_path: Path in this tree
243
225
:ivar trans_id: the transform ID for the merge of this file
244
:ivar this_kind: kind of file_id in 'this' tree
245
:ivar other_kind: kind of file_id in 'other' tree
226
:ivar this_kind: kind of file in 'this' tree
227
:ivar other_kind: kind of file in 'other' tree
246
228
:ivar winner: one of 'this', 'other', 'conflict'
249
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
231
def __init__(self, merger, paths, trans_id, this_kind, other_kind,
251
233
self._merger = merger
252
self.file_id = file_id
235
self.base_path, self.other_path, self.this_path = paths
253
236
self.trans_id = trans_id
254
237
self.this_kind = this_kind
255
238
self.other_kind = other_kind
262
245
@decorators.cachedproperty
263
246
def base_lines(self):
264
247
"""The lines of the 'base' version of the file."""
265
return self._merger.get_lines(self._merger.base_tree, self.file_id)
248
return self._merger.get_lines(self._merger.base_tree, self.base_path)
267
250
@decorators.cachedproperty
268
251
def this_lines(self):
269
252
"""The lines of the 'this' version of the file."""
270
return self._merger.get_lines(self._merger.this_tree, self.file_id)
253
return self._merger.get_lines(self._merger.this_tree, self.this_path)
272
255
@decorators.cachedproperty
273
256
def other_lines(self):
274
257
"""The lines of the 'other' version of the file."""
275
return self._merger.get_lines(self._merger.other_tree, self.file_id)
258
return self._merger.get_lines(self._merger.other_tree, self.other_path)
278
261
class Merger(object):
456
437
def set_pending(self):
457
438
if (not self.base_is_ancestor or not self.base_is_other_ancestor
458
or self.other_rev_id is None):
439
or self.other_rev_id is None):
460
441
self._add_parent()
462
443
def _add_parent(self):
463
444
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
464
445
new_parent_trees = []
465
operation = cleanup.OperationWithCleanups(
466
self.this_tree.set_parent_trees)
467
for revision_id in new_parents:
469
tree = self.revision_tree(revision_id)
470
except errors.NoSuchRevision:
474
operation.add_cleanup(tree.unlock)
475
new_parent_trees.append((revision_id, tree))
476
operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
446
with contextlib.ExitStack() as stack:
447
for revision_id in new_parents:
449
tree = self.revision_tree(revision_id)
450
except errors.NoSuchRevision:
453
stack.enter_context(tree.lock_read())
454
new_parent_trees.append((revision_id, tree))
455
self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
478
457
def set_other(self, other_revision, possible_transports=None):
479
458
"""Set the revision and tree to merge from.
650
629
for hook in Merger.hooks['post_merge']:
652
631
if self.recurse == 'down':
653
for relpath, file_id in self.this_tree.iter_references():
654
sub_tree = self.this_tree.get_nested_tree(relpath, file_id)
632
for relpath in self.this_tree.iter_references():
633
sub_tree = self.this_tree.get_nested_tree(relpath)
655
634
other_revision = self.other_tree.get_reference_revision(
657
if other_revision == sub_tree.last_revision():
636
if other_revision == sub_tree.last_revision():
659
638
sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
660
639
sub_merge.merge_type = self.merge_type
661
other_branch = self.other_branch.reference_parent(file_id,
640
other_branch = self.other_tree.reference_parent(relpath)
663
641
sub_merge.set_other_revision(other_revision, other_branch)
664
base_tree_path = self.base_tree.id2path(file_id)
642
base_tree_path = _mod_tree.find_previous_path(
643
self.this_tree, self.base_tree, relpath)
665
644
base_revision = self.base_tree.get_reference_revision(
666
base_tree_path, file_id)
667
646
sub_merge.base_tree = \
668
647
sub_tree.branch.repository.revision_tree(base_revision)
669
648
sub_merge.base_rev_id = base_revision
673
652
def do_merge(self):
674
operation = cleanup.OperationWithCleanups(self._do_merge_to)
675
self.this_tree.lock_tree_write()
676
operation.add_cleanup(self.this_tree.unlock)
677
if self.base_tree is not None:
678
self.base_tree.lock_read()
679
operation.add_cleanup(self.base_tree.unlock)
680
if self.other_tree is not None:
681
self.other_tree.lock_read()
682
operation.add_cleanup(self.other_tree.unlock)
683
merge = operation.run_simple()
653
with contextlib.ExitStack() as stack:
654
stack.enter_context(self.this_tree.lock_tree_write())
655
if self.base_tree is not None:
656
stack.enter_context(self.base_tree.lock_read())
657
if self.other_tree is not None:
658
stack.enter_context(self.other_tree.lock_read())
659
merge = self._do_merge_to()
684
660
if len(merge.cooked_conflicts) == 0:
685
661
if not self.ignore_zero and not trace.is_quiet():
686
662
trace.note(gettext("All changes applied successfully."))
718
698
supports_reverse_cherrypick = True
719
699
winner_idx = {"this": 2, "other": 1, "conflict": 1}
720
700
supports_lca_trees = True
701
requires_file_merge_plan = False
722
703
def __init__(self, working_tree, this_tree, base_tree, other_tree,
723
interesting_ids=None, reprocess=False, show_base=False,
704
reprocess=False, show_base=False,
724
705
change_reporter=None, interesting_files=None, do_merge=True,
725
706
cherrypick=False, lca_trees=None, this_branch=None,
726
707
other_branch=None):
733
714
:param this_branch: The branch associated with this_tree. Defaults to
734
715
this_tree.branch if not supplied.
735
716
: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
717
:param: reprocess If True, perform conflict-reduction processing.
740
718
:param show_base: If True, show the base revision in text conflicts.
741
719
(incompatible with reprocess)
742
720
:param change_reporter: An object that should report changes made
743
721
:param interesting_files: The tree-relative paths of files that should
744
722
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
723
the contents of those directories will also be included. If not
724
specified, all files may participate in the
749
726
:param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
750
727
if the ancestry was found to include a criss-cross merge.
751
728
Otherwise should be None.
753
730
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
731
if this_branch is None:
758
732
this_branch = this_tree.branch
759
self.interesting_ids = interesting_ids
760
733
self.interesting_files = interesting_files
761
734
self.working_tree = working_tree
762
735
self.this_tree = this_tree
782
755
def do_merge(self):
783
operation = cleanup.OperationWithCleanups(self._do_merge)
784
self.working_tree.lock_tree_write()
785
operation.add_cleanup(self.working_tree.unlock)
786
self.this_tree.lock_read()
787
operation.add_cleanup(self.this_tree.unlock)
788
self.base_tree.lock_read()
789
operation.add_cleanup(self.base_tree.unlock)
790
self.other_tree.lock_read()
791
operation.add_cleanup(self.other_tree.unlock)
794
def _do_merge(self, operation):
795
self.tt = transform.TreeTransform(self.working_tree, None)
796
operation.add_cleanup(self.tt.finalize)
797
self._compute_transform()
798
results = self.tt.apply(no_conflicts=True)
799
self.write_modified(results)
801
self.working_tree.add_conflicts(self.cooked_conflicts)
802
except errors.UnsupportedOperation:
756
with contextlib.ExitStack() as stack:
757
stack.enter_context(self.working_tree.lock_tree_write())
758
stack.enter_context(self.this_tree.lock_read())
759
stack.enter_context(self.base_tree.lock_read())
760
stack.enter_context(self.other_tree.lock_read())
761
self.tt = self.working_tree.get_transform()
762
stack.enter_context(self.tt)
763
self._compute_transform()
764
results = self.tt.apply(no_conflicts=True)
765
self.write_modified(results)
767
self.working_tree.add_conflicts(self.cooked_conflicts)
768
except errors.UnsupportedOperation:
805
771
def make_preview_transform(self):
806
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
807
self.base_tree.lock_read()
808
operation.add_cleanup(self.base_tree.unlock)
809
self.other_tree.lock_read()
810
operation.add_cleanup(self.other_tree.unlock)
811
return operation.run_simple()
813
def _make_preview_transform(self):
814
self.tt = transform.TransformPreview(self.working_tree)
815
self._compute_transform()
772
with self.base_tree.lock_read(), self.other_tree.lock_read():
773
self.tt = transform.TransformPreview(self.working_tree)
774
self._compute_transform()
818
777
def _compute_transform(self):
819
778
if self._lca_trees is None:
827
786
# One hook for each registered one plus our default merger
828
787
hooks = [factory(self) for factory in factories] + [self]
829
788
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,
789
with ui.ui_factory.nested_progress_bar() as child_pb:
790
for num, (file_id, changed, paths3, parents3, names3,
833
791
executable3) in enumerate(entries):
792
trans_id = self.tt.trans_id_file_id(file_id)
834
794
# Try merging each entry
835
795
child_pb.update(gettext('Preparing file merge'),
836
796
num, len(entries))
837
self._merge_names(file_id, parents3, names3, resolver=resolver)
797
self._merge_names(trans_id, file_id, paths3, parents3,
798
names3, resolver=resolver)
839
file_status = self._do_merge_contents(file_id)
800
file_status = self._do_merge_contents(paths3, trans_id, file_id)
841
802
file_status = 'unmodified'
842
self._merge_executable(file_id,
843
executable3, file_status, resolver=resolver)
803
self._merge_executable(paths3, trans_id, executable3,
804
file_status, resolver=resolver)
846
805
self.tt.fixup_new_roots()
847
806
self._finish_computing_transform()
852
811
This is the second half of _compute_transform.
854
child_pb = ui.ui_factory.nested_progress_bar()
856
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
813
with ui.ui_factory.nested_progress_bar() as child_pb:
814
fs_conflicts = transform.resolve_conflicts(
857
816
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
860
817
if self.change_reporter is not None:
861
818
from breezy import delta
862
819
delta.report_changes(
863
820
self.tt.iter_changes(), self.change_reporter)
864
821
self.cook_conflicts(fs_conflicts)
865
822
for conflict in self.cooked_conflicts:
866
trace.warning(unicode(conflict))
823
trace.warning('%s', conflict.describe())
868
825
def _entries3(self):
869
826
"""Gather data about files modified between three trees.
878
835
iterator = self.other_tree.iter_changes(self.base_tree,
879
specific_files=self.interesting_files,
880
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))
884
for (file_id, paths, changed, versioned, parents, names, kind,
885
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
836
specific_files=self.interesting_files,
837
extra_trees=[self.this_tree])
838
this_interesting_files = self.this_tree.find_related_paths_across_trees(
839
self.interesting_files, trees=[self.other_tree])
840
this_entries = dict(self.this_tree.iter_entries_by_dir(
841
specific_files=this_interesting_files))
842
for change in iterator:
843
if change.path[0] is not None:
844
this_path = _mod_tree.find_previous_path(
845
self.base_tree, self.this_tree, change.path[0])
847
this_path = _mod_tree.find_previous_path(
848
self.other_tree, self.this_tree, change.path[1])
849
this_entry = this_entries.get(this_path)
850
if this_entry is not None:
851
this_name = this_entry.name
852
this_parent = this_entry.parent_id
853
this_executable = this_entry.executable
896
856
this_parent = None
897
857
this_executable = None
898
parents3 = parents + (this_parent,)
899
names3 = names + (this_name,)
900
executable3 = executable + (this_executable,)
901
result.append((file_id, changed, parents3, names3, executable3))
858
parents3 = change.parent_id + (this_parent,)
859
names3 = change.name + (this_name,)
860
paths3 = change.path + (this_path, )
861
executable3 = change.executable + (this_executable,)
863
(change.file_id, change.changed_content, paths3,
864
parents3, names3, executable3))
904
867
def _entries_lca(self):
910
873
For the multi-valued entries, the format will be (BASE, [lca1, lca2])
912
:return: [(file_id, changed, parents, names, executable)], where:
875
:return: [(file_id, changed, paths, parents, names, executable)], where:
914
877
* file_id: Simple file_id of the entry
915
878
* changed: Boolean, True if the kind or contents changed else False
879
* paths: ((base, [path, in, lcas]), path_other, path_this)
916
880
* parents: ((base, [parent_id, in, lcas]), parent_id_other,
918
882
* names: ((base, [name, in, lcas]), name_in_other, name_in_this)
923
887
lookup_trees = [self.this_tree, self.base_tree]
924
888
lookup_trees.extend(self._lca_trees)
925
889
# I think we should include the lca trees as well
926
interesting_ids = self.other_tree.paths2ids(self.interesting_files,
890
interesting_files = self.other_tree.find_related_paths_across_trees(
891
self.interesting_files, lookup_trees)
929
interesting_ids = self.interesting_ids
893
interesting_files = None
931
walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
895
from .multiwalker import MultiWalker
896
walker = MultiWalker(self.other_tree, self._lca_trees)
933
base_inventory = self.base_tree.root_inventory
934
this_inventory = self.this_tree.root_inventory
935
for path, file_id, other_ie, lca_values in walker.iter_all():
898
for other_path, file_id, other_ie, lca_values in walker.iter_all():
936
899
# Is this modified at all from any of the other trees?
937
900
if other_ie is None:
938
901
other_ie = _none_entry
939
if interesting_ids is not None and file_id not in interesting_ids:
903
if interesting_files is not None and other_path not in interesting_files:
942
906
# If other_revision is found in any of the lcas, that means this
945
909
# we know that the ancestry is linear, and that OTHER did not
946
910
# modify anything
947
911
# See doc/developers/lca_merge_resolution.txt for details
948
other_revision = other_ie.revision
949
if other_revision is not None:
950
# We can't use this shortcut when other_revision is None,
951
# because it may be None because things are WorkingTrees, and
952
# not because it is *actually* None.
953
is_unmodified = False
954
for lca_path, ie in lca_values:
955
if ie is not None and ie.revision == other_revision:
912
# We can't use this shortcut when other_revision is None,
913
# because it may be None because things are WorkingTrees, and
914
# not because it is *actually* None.
915
is_unmodified = False
916
for lca_path, ie in lca_values:
917
if ie is not None and other_ie.is_unmodified(ie):
962
925
for lca_path, lca_ie in lca_values:
963
926
if lca_ie is None:
964
927
lca_entries.append(_none_entry)
928
lca_paths.append(None)
966
930
lca_entries.append(lca_ie)
931
lca_paths.append(lca_path)
968
if base_inventory.has_id(file_id):
969
base_ie = base_inventory[file_id]
934
base_path = self.base_tree.id2path(file_id)
935
except errors.NoSuchId:
971
937
base_ie = _none_entry
939
base_ie = next(self.base_tree.iter_entries_by_dir(specific_files=[base_path]))[1]
973
if this_inventory.has_id(file_id):
974
this_ie = this_inventory[file_id]
942
this_path = self.this_tree.id2path(file_id)
943
except errors.NoSuchId:
976
944
this_ie = _none_entry
947
this_ie = next(self.this_tree.iter_entries_by_dir(specific_files=[this_path]))[1]
979
950
lca_parent_ids = []
1005
976
content_changed = False
1006
977
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)
978
def get_sha1(tree, path):
982
return tree.get_file_sha1(path)
983
except errors.NoSuchFile:
985
base_sha1 = get_sha1(self.base_tree, base_path)
986
lca_sha1s = [get_sha1(tree, lca_path)
988
in zip(self._lca_trees, lca_paths)]
989
this_sha1 = get_sha1(self.this_tree, this_path)
990
other_sha1 = get_sha1(self.other_tree, other_path)
1016
991
sha1_winner = self._lca_multi_way(
1017
992
(base_sha1, lca_sha1s), other_sha1, this_sha1,
1018
993
allow_overriding_lca=False)
1020
995
(base_ie.executable, lca_executable),
1021
996
other_ie.executable, this_ie.executable)
1022
997
if (parent_id_winner == 'this' and name_winner == 'this'
1023
and sha1_winner == 'this' and exec_winner == 'this'):
998
and sha1_winner == 'this' and exec_winner == 'this'):
1024
999
# No kind, parent, name, exec, or content change for
1025
1000
# OTHER, so this node is not considered interesting
1027
1002
if sha1_winner == 'this':
1028
1003
content_changed = False
1029
1004
elif other_ie.kind == 'symlink':
1030
def get_target(ie, tree):
1005
def get_target(ie, tree, path):
1031
1006
if ie.kind != 'symlink':
1033
path = tree.id2path(file_id)
1034
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)
1008
return tree.get_symlink_target(path)
1009
base_target = get_target(base_ie, self.base_tree, base_path)
1010
lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
1011
in zip(lca_entries, self._lca_trees, lca_paths)]
1012
this_target = get_target(
1013
this_ie, self.this_tree, this_path)
1014
other_target = get_target(
1015
other_ie, self.other_tree, other_path)
1040
1016
target_winner = self._lca_multi_way(
1041
1017
(base_target, lca_targets),
1042
1018
other_target, this_target)
1043
1019
if (parent_id_winner == 'this' and name_winner == 'this'
1044
and target_winner == 'this'):
1020
and target_winner == 'this'):
1045
1021
# No kind, parent, name, or symlink target change
1046
1022
# not interesting
1061
1037
# If we have gotten this far, that means something has changed
1062
1038
result.append((file_id, content_changed,
1039
((base_path, lca_paths),
1040
other_path, this_path),
1063
1041
((base_ie.parent_id, lca_parent_ids),
1064
1042
other_ie.parent_id, this_ie.parent_id),
1065
1043
((base_ie.name, lca_names),
1066
1044
other_ie.name, this_ie.name),
1067
1045
((base_ie.executable, lca_executable),
1068
1046
other_ie.executable, this_ie.executable)
1072
1050
def write_modified(self, results):
1051
if not self.working_tree.supports_merge_modified():
1073
1053
modified_hashes = {}
1074
1054
for path in results.modified_paths:
1075
1055
wt_relpath = self.working_tree.relpath(path)
1076
file_id = self.working_tree.path2id(wt_relpath)
1056
if not self.working_tree.is_versioned(wt_relpath):
1079
hash = self.working_tree.get_file_sha1(wt_relpath, file_id)
1058
hash = self.working_tree.get_file_sha1(wt_relpath)
1080
1059
if hash is None:
1082
modified_hashes[file_id] = hash
1061
modified_hashes[wt_relpath] = hash
1083
1062
self.working_tree.set_merge_modified(modified_hashes)
1086
def parent(entry, file_id):
1087
1066
"""Determine the parent for a file_id (used as a key method)"""
1088
1067
if entry is None:
1090
1069
return entry.parent_id
1093
def name(entry, file_id):
1094
1073
"""Determine the name for a file_id (used as a key method)"""
1095
1074
if entry is None:
1097
1076
return entry.name
1100
def contents_sha1(tree, file_id):
1079
def contents_sha1(tree, path):
1101
1080
"""Determine the sha1 of the file contents (used as a key method)."""
1103
path = tree.id2path(file_id)
1104
except errors.NoSuchId:
1082
return tree.get_file_sha1(path)
1083
except errors.NoSuchFile:
1106
return tree.get_file_sha1(path, file_id)
1109
def executable(tree, file_id):
1087
def executable(tree, path):
1110
1088
"""Determine the executability of a file-id (used as a key method)."""
1112
path = tree.id2path(file_id)
1113
except errors.NoSuchId:
1090
if tree.kind(path) != "file":
1092
except errors.NoSuchFile:
1115
if tree.kind(path, file_id) != "file":
1117
return tree.is_executable(path, file_id)
1094
return tree.is_executable(path)
1120
def kind(tree, path, file_id):
1097
def kind(tree, path):
1121
1098
"""Determine the kind of a file-id (used as a key method)."""
1123
path = tree.id2path(file_id)
1124
except errors.NoSuchId:
1100
return tree.kind(path)
1101
except errors.NoSuchFile:
1126
return tree.kind(path, file_id)
1129
1105
def _three_way(base, other, this):
1191
1167
# At this point, the lcas disagree, and the tip disagree
1192
1168
return 'conflict'
1194
def merge_names(self, file_id):
1195
def get_entry(tree):
1197
return tree.root_inventory[file_id]
1198
except errors.NoSuchId:
1200
this_entry = get_entry(self.this_tree)
1201
other_entry = get_entry(self.other_tree)
1202
base_entry = get_entry(self.base_tree)
1203
entries = (base_entry, other_entry, this_entry)
1206
for entry in entries:
1209
parents.append(None)
1211
names.append(entry.name)
1212
parents.append(entry.parent_id)
1213
return self._merge_names(file_id, parents, names,
1214
resolver=self._three_way)
1216
def _merge_names(self, file_id, parents, names, resolver):
1217
"""Perform a merge on file_id names and parents"""
1170
def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
1171
"""Perform a merge on file names and parents"""
1218
1172
base_name, other_name, this_name = names
1219
1173
base_parent, other_parent, this_parent = parents
1174
unused_base_path, other_path, this_path = paths
1221
1176
name_winner = resolver(*names)
1255
1209
parent_trans_id = transform.ROOT_PARENT
1257
1211
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1258
self.tt.adjust_path(name, parent_trans_id,
1259
self.tt.trans_id_file_id(file_id))
1212
self.tt.adjust_path(name, parent_trans_id, trans_id)
1261
def _do_merge_contents(self, file_id):
1214
def _do_merge_contents(self, paths, trans_id, file_id):
1262
1215
"""Performs a merge on file_id contents."""
1263
def contents_pair(tree):
1265
path = tree.id2path(file_id)
1266
except errors.NoSuchId:
1216
def contents_pair(tree, path):
1267
1218
return (None, None)
1269
kind = tree.kind(path, file_id)
1220
kind = tree.kind(path)
1270
1221
except errors.NoSuchFile:
1271
1222
return (None, None)
1272
1223
if kind == "file":
1273
contents = tree.get_file_sha1(path, file_id)
1224
contents = tree.get_file_sha1(path)
1274
1225
elif kind == "symlink":
1275
contents = tree.get_symlink_target(path, file_id)
1226
contents = tree.get_symlink_target(path)
1277
1228
contents = None
1278
1229
return kind, contents
1231
base_path, other_path, this_path = paths
1280
1232
# See SPOT run. run, SPOT, run.
1281
1233
# 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)
1235
other_pair = contents_pair(self.other_tree, other_path)
1236
this_pair = contents_pair(self.this_tree, this_path)
1285
1237
if self._lca_trees:
1286
this_pair = contents_pair(self.this_tree)
1287
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1238
(base_path, lca_paths) = base_path
1239
base_pair = contents_pair(self.base_tree, base_path)
1240
lca_pairs = [contents_pair(tree, path)
1241
for tree, path in zip(self._lca_trees, lca_paths)]
1288
1242
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1289
1243
this_pair, allow_overriding_lca=False)
1245
base_pair = contents_pair(self.base_tree, base_path)
1291
1246
if base_pair == other_pair:
1292
1247
winner = 'this'
1294
1249
# We delayed evaluating this_pair as long as we can to avoid
1295
1250
# unnecessary sha1 calculation
1296
this_pair = contents_pair(self.this_tree)
1251
this_pair = contents_pair(self.this_tree, this_path)
1297
1252
winner = self._three_way(base_pair, other_pair, this_pair)
1298
1253
if winner == 'this':
1299
1254
# No interesting changes introduced by OTHER
1300
1255
return "unmodified"
1301
1256
# We have a hypothetical conflict, but if we have files, then we
1302
1257
# can try to merge the content
1303
trans_id = self.tt.trans_id_file_id(file_id)
1304
params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
1258
params = MergeFileHookParams(
1259
self, (base_path, other_path, this_path), trans_id, this_pair[0],
1305
1260
other_pair[0], winner)
1306
1261
hooks = self.active_hooks
1307
1262
hook_status = 'not_applicable'
1335
1287
self.tt.version_file(file_id, trans_id)
1336
1288
transform.create_from_tree(
1337
1289
self.tt, trans_id, self.other_tree,
1338
self.other_tree.id2path(file_id), file_id=file_id,
1339
filter_tree_path=self._get_filter_tree_path(file_id))
1291
filter_tree_path=self._get_filter_tree_path(other_path))
1340
1292
inhibit_content_conflict = True
1341
elif params.other_kind is None: # file_id is not in OTHER
1293
elif params.other_kind is None: # file_id is not in OTHER
1342
1294
# 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:
1295
if self.other_tree.is_versioned(this_path):
1346
1296
# Two entries for the same path again, but here, the other
1347
1297
# entry will also be merged. We simply inhibit the
1348
1298
# 'content' conflict creation because we know OTHER will
1356
1306
self.tt.unversion_file(trans_id)
1357
1307
# This is a contents conflict, because none of the available
1358
1308
# functions could merge it.
1359
file_group = self._dump_conflicts(name, parent_id, file_id,
1309
file_group = self._dump_conflicts(
1310
name, (base_path, other_path, this_path), parent_id,
1311
file_id, set_version=True)
1361
1312
self._raw_conflicts.append(('contents conflict', file_group))
1362
1313
elif hook_status == 'success':
1363
1314
self.tt.create_file(lines, trans_id)
1389
1341
def _default_other_winner_merge(self, merge_hook_params):
1390
1342
"""Replace this contents with other."""
1391
file_id = merge_hook_params.file_id
1392
1343
trans_id = merge_hook_params.trans_id
1393
if self.other_tree.has_id(file_id):
1344
if merge_hook_params.other_path is not None:
1394
1345
# OTHER changed the file
1395
1346
transform.create_from_tree(
1396
1347
self.tt, trans_id, self.other_tree,
1397
self.other_tree.id2path(file_id), file_id=file_id,
1398
filter_tree_path=self._get_filter_tree_path(file_id))
1348
merge_hook_params.other_path,
1349
filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
1399
1350
return 'done', None
1400
elif self.this_tree.has_id(file_id):
1351
elif merge_hook_params.this_path is not None:
1401
1352
# OTHER deleted the file
1402
1353
return 'delete', None
1404
1355
raise AssertionError(
1405
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1356
'winner is OTHER, but file %r not in THIS or OTHER tree'
1357
% (merge_hook_params.base_path,))
1408
1359
def merge_contents(self, merge_hook_params):
1409
1360
"""Fallback merge logic after user installed hooks."""
1410
1361
# This function is used in merge hooks as the fallback instance.
1411
# Perhaps making this function and the functions it calls be a
1362
# Perhaps making this function and the functions it calls be a
1412
1363
# a separate class would be better.
1413
1364
if merge_hook_params.winner == 'other':
1414
1365
# OTHER is a straight winner, so replace this contents with other
1418
1369
# BASE is a file, or both converted to files, so at least we
1419
1370
# have agreement that output should be a file.
1421
self.text_merge(merge_hook_params.file_id,
1422
merge_hook_params.trans_id)
1372
self.text_merge(merge_hook_params.trans_id,
1373
merge_hook_params.paths)
1423
1374
except errors.BinaryFile:
1424
1375
return 'not_applicable', None
1425
1376
return 'done', None
1427
1378
return 'not_applicable', None
1429
def get_lines(self, tree, file_id):
1380
def get_lines(self, tree, path):
1430
1381
"""Return the lines in a file, or an empty list."""
1432
path = tree.id2path(file_id)
1433
except errors.NoSuchId:
1385
kind = tree.kind(path)
1386
except errors.NoSuchFile:
1436
return tree.get_file_lines(path, file_id)
1391
return tree.get_file_lines(path)
1438
def text_merge(self, file_id, trans_id):
1439
"""Perform a three-way text merge on a file_id"""
1393
def text_merge(self, trans_id, paths):
1394
"""Perform a three-way text merge on a file"""
1440
1395
# it's possible that we got here with base as a different type.
1441
1396
# 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)
1397
base_path, other_path, this_path = paths
1398
base_lines = self.get_lines(self.base_tree, base_path)
1399
other_lines = self.get_lines(self.other_tree, other_path)
1400
this_lines = self.get_lines(self.this_tree, this_path)
1453
1401
m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1454
1402
is_cherrypick=self.cherrypick)
1455
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1403
start_marker = b"!START OF MERGE CONFLICT!" + b"I HOPE THIS IS UNIQUE"
1456
1404
if self.show_base is True:
1457
base_marker = '|' * 7
1405
base_marker = b'|' * 7
1459
1407
base_marker = None
1461
1409
def iter_merge3(retval):
1462
1410
retval["text_conflicts"] = False
1463
for line in m3.merge_lines(name_a = "TREE",
1464
name_b = "MERGE-SOURCE",
1465
name_base = "BASE-REVISION",
1411
for line in m3.merge_lines(name_a=b"TREE",
1412
name_b=b"MERGE-SOURCE",
1413
name_base=b"BASE-REVISION",
1466
1414
start_marker=start_marker,
1467
1415
base_marker=base_marker,
1468
1416
reprocess=self.reprocess):
1469
1417
if line.startswith(start_marker):
1470
1418
retval["text_conflicts"] = True
1471
yield line.replace(start_marker, '<' * 7)
1419
yield line.replace(start_marker, b'<' * 7)
1478
1426
self._raw_conflicts.append(('text conflict', trans_id))
1479
1427
name = self.tt.final_name(trans_id)
1480
1428
parent_id = self.tt.final_parent(trans_id)
1481
file_group = self._dump_conflicts(name, parent_id, file_id,
1429
file_id = self.tt.final_file_id(trans_id)
1430
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1482
1431
this_lines, base_lines,
1484
1433
file_group.append(trans_id)
1487
def _get_filter_tree_path(self, file_id):
1435
def _get_filter_tree_path(self, path):
1488
1436
if self.this_tree.supports_content_filtering():
1489
1437
# We get the path from the working tree if it exists.
1490
1438
# That fails though when OTHER is adding a file, so
1491
1439
# we fall back to the other tree to find the path if
1492
1440
# it doesn't exist locally.
1494
return self.this_tree.id2path(file_id)
1495
except errors.NoSuchId:
1496
return self.other_tree.id2path(file_id)
1497
# Skip the id2path lookup for older formats
1441
filter_path = _mod_tree.find_previous_path(
1442
self.other_tree, self.working_tree, path)
1443
if filter_path is None:
1446
# Skip the lookup for older formats
1500
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1449
def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1501
1450
base_lines=None, other_lines=None, set_version=False,
1502
1451
no_base=False):
1503
1452
"""Emit conflict files.
1505
1454
determined automatically. If set_version is true, the .OTHER, .THIS
1506
1455
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)]
1457
base_path, other_path, this_path = paths
1458
data = [('OTHER', self.other_tree, other_path, other_lines),
1459
('THIS', self.this_tree, this_path, this_lines)]
1510
1460
if not no_base:
1511
data.append(('BASE', self.base_tree, base_lines))
1461
data.append(('BASE', self.base_tree, base_path, base_lines))
1513
1463
# We need to use the actual path in the working tree of the file here,
1514
1464
# ignoring the conflict suffixes
1526
1476
versioned = False
1527
1477
file_group = []
1528
for suffix, tree, lines in data:
1530
path = tree.id2path(file_id)
1531
except errors.NoSuchId:
1478
for suffix, tree, path, lines in data:
1479
if path is not None:
1534
1480
trans_id = self._conflict_file(
1535
name, parent_id, path, tree, file_id, suffix, lines,
1481
name, parent_id, path, tree, suffix, lines,
1537
1483
file_group.append(trans_id)
1538
1484
if set_version and not versioned:
1539
1485
self.tt.version_file(file_id, trans_id)
1540
1486
versioned = True
1541
1487
return file_group
1543
def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1489
def _conflict_file(self, name, parent_id, path, tree, suffix,
1544
1490
lines=None, filter_tree_path=None):
1545
1491
"""Emit a single conflict file."""
1546
1492
name = name + '.' + suffix
1547
1493
trans_id = self.tt.create_path(name, parent_id)
1548
1494
transform.create_from_tree(
1549
self.tt, trans_id, tree, path,
1550
file_id=file_id, bytes=lines,
1551
filter_tree_path=filter_tree_path)
1495
self.tt, trans_id, tree, path,
1497
filter_tree_path=filter_tree_path)
1552
1498
return trans_id
1554
def merge_executable(self, file_id, file_status):
1555
"""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,
1559
resolver=self._three_way)
1561
def _merge_executable(self, file_id, executable, file_status,
1500
def _merge_executable(self, paths, trans_id, executable, file_status,
1563
1502
"""Perform a merge on the execute bit."""
1564
1503
base_executable, other_executable, this_executable = executable
1504
base_path, other_path, this_path = paths
1565
1505
if file_status == "deleted":
1567
1507
winner = resolver(*executable)
1568
1508
if winner == "conflict":
1569
# There must be a None in here, if we have a conflict, but we
1570
# need executability since file status was not deleted.
1571
if self.executable(self.other_tree, file_id) is None:
1509
# There must be a None in here, if we have a conflict, but we
1510
# need executability since file status was not deleted.
1511
if other_path is None:
1572
1512
winner = "this"
1574
1514
winner = "other"
1575
1515
if winner == 'this' and file_status != "modified":
1577
trans_id = self.tt.trans_id_file_id(file_id)
1578
1517
if self.tt.final_kind(trans_id) != "file":
1580
1519
if winner == "this":
1581
1520
executability = this_executable
1583
if self.other_tree.has_id(file_id):
1522
if other_path is not None:
1584
1523
executability = other_executable
1585
elif self.this_tree.has_id(file_id):
1524
elif this_path is not None:
1586
1525
executability = this_executable
1587
elif self.base_tree_has_id(file_id):
1526
elif base_path is not None:
1588
1527
executability = base_executable
1589
1528
if executability is not None:
1590
trans_id = self.tt.trans_id_file_id(file_id)
1591
1529
self.tt.set_executability(executability, trans_id)
1593
1531
def cook_conflicts(self, fs_conflicts):
1599
1537
conflict_type = conflict[0]
1600
1538
if conflict_type == 'path conflict':
1601
1539
(trans_id, file_id,
1602
this_parent, this_name,
1603
other_parent, other_name) = conflict[1:]
1540
this_parent, this_name,
1541
other_parent, other_name) = conflict[1:]
1604
1542
if this_parent is None or this_name is None:
1605
1543
this_path = '<deleted>'
1607
parent_path = fp.get_path(
1545
parent_path = fp.get_path(
1608
1546
self.tt.trans_id_file_id(this_parent))
1609
1547
this_path = osutils.pathjoin(parent_path, this_name)
1610
1548
if other_parent is None or other_name is None:
1611
1549
other_path = '<deleted>'
1613
if other_parent == self.other_tree.get_root_id():
1551
if other_parent == self.other_tree.path2id(''):
1614
1552
# The tree transform doesn't know about the other root,
1615
1553
# so we special case here to avoid a NoFinalPath
1617
1555
parent_path = ''
1619
parent_path = fp.get_path(
1557
parent_path = fp.get_path(
1620
1558
self.tt.trans_id_file_id(other_parent))
1621
1559
other_path = osutils.pathjoin(parent_path, other_name)
1622
1560
c = _mod_conflicts.Conflict.factory(
1667
1605
supports_show_base = False
1668
1606
supports_reverse_cherrypick = False
1669
1607
history_based = True
1608
requires_file_merge_plan = True
1671
def _generate_merge_plan(self, file_id, base):
1672
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1610
def _generate_merge_plan(self, this_path, base):
1611
return self.this_tree.plan_file_merge(this_path, self.other_tree,
1675
def _merged_lines(self, file_id):
1614
def _merged_lines(self, this_path):
1676
1615
"""Generate the merged lines.
1677
1616
There is no distinction between lines that are meant to contain <<<<<<<
1681
1620
base = self.base_tree
1684
plan = self._generate_merge_plan(file_id, base)
1623
plan = self._generate_merge_plan(this_path, base)
1685
1624
if 'merge' in debug.debug_flags:
1686
1625
plan = list(plan)
1687
1626
trans_id = self.tt.trans_id_file_id(file_id)
1688
1627
name = self.tt.final_name(trans_id) + '.plan'
1689
contents = ('%11s|%s' % l for l in plan)
1628
contents = (b'%11s|%s' % l for l in plan)
1690
1629
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1691
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1692
'>>>>>>> MERGE-SOURCE\n')
1630
textmerge = versionedfile.PlanWeaveMerge(plan, b'<<<<<<< TREE\n',
1631
b'>>>>>>> MERGE-SOURCE\n')
1693
1632
lines, conflicts = textmerge.merge_lines(self.reprocess)
1695
1634
base_lines = textmerge.base_from_plan()
1697
1636
base_lines = None
1698
1637
return lines, base_lines
1700
def text_merge(self, file_id, trans_id):
1639
def text_merge(self, trans_id, paths):
1701
1640
"""Perform a (weave) text merge for a given file and file-id.
1702
1641
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1703
1642
and a conflict will be noted.
1705
lines, base_lines = self._merged_lines(file_id)
1644
base_path, other_path, this_path = paths
1645
lines, base_lines = self._merged_lines(this_path)
1706
1646
lines = list(lines)
1707
1647
# Note we're checking whether the OUTPUT is binary in this case,
1708
1648
# because we don't want to get into weave merge guts.
1722
1663
class LCAMerger(WeaveMerger):
1724
def _generate_merge_plan(self, file_id, base):
1725
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1665
requires_file_merge_plan = True
1667
def _generate_merge_plan(self, this_path, base):
1668
return self.this_tree.plan_file_lca_merge(this_path, self.other_tree,
1728
1672
class Diff3Merger(Merge3Merger):
1729
1673
"""Three-way merger using external diff3 for text merging"""
1731
def dump_file(self, temp_dir, name, tree, file_id):
1675
requires_file_merge_plan = False
1677
def dump_file(self, temp_dir, name, tree, path):
1732
1678
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)
1679
with open(out_path, "wb") as out_file:
1680
in_file = tree.get_file(path)
1736
1681
for line in in_file:
1737
1682
out_file.write(line)
1740
1683
return out_path
1742
def text_merge(self, file_id, trans_id):
1685
def text_merge(self, trans_id, paths):
1743
1686
"""Perform a diff3 merge using a specified file-id and trans-id.
1744
1687
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1745
1688
will be dumped, and a will be conflict noted.
1747
1690
import breezy.patch
1691
base_path, other_path, this_path = paths
1748
1692
temp_dir = osutils.mkdtemp(prefix="bzr-")
1750
1694
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)
1695
this = self.dump_file(
1696
temp_dir, "this", self.this_tree, this_path)
1697
base = self.dump_file(
1698
temp_dir, "base", self.base_tree, base_path)
1699
other = self.dump_file(
1700
temp_dir, "other", self.other_tree, other_path)
1754
1701
status = breezy.patch.diff3(new_file, this, base, other)
1755
1702
if status not in (0, 1):
1756
1703
raise errors.BzrError("Unhandled diff3 exit code")
1757
f = open(new_file, 'rb')
1704
with open(new_file, 'rb') as f:
1759
1705
self.tt.create_file(f, trans_id)
1762
1706
if status == 1:
1763
1707
name = self.tt.final_name(trans_id)
1764
1708
parent_id = self.tt.final_parent(trans_id)
1765
self._dump_conflicts(name, parent_id, file_id)
1709
file_id = self.tt.final_file_id(trans_id)
1710
self._dump_conflicts(name, paths, parent_id, file_id)
1766
1711
self._raw_conflicts.append(('text conflict', trans_id))
1768
1713
osutils.rmtree(temp_dir)
1819
1764
self.merge_type = Merge3Merger
1820
1765
self.show_base = False
1821
1766
self.reprocess = False
1822
self.interesting_ids = None
1767
self.interesting_files = None
1823
1768
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1824
target_subdir=self._target_subdir,
1825
source_subpath=self._source_subpath)
1769
target_subdir=self._target_subdir,
1770
source_subpath=self._source_subpath)
1826
1771
if self._source_subpath != '':
1827
1772
# If this isn't a partial merge make sure the revisions will be
1829
1774
self._maybe_fetch(self.other_branch, self.this_branch,
1832
1777
def set_pending(self):
1833
1778
if self._source_subpath != '':
1878
1823
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1880
1825
def _compute_transform(self):
1881
child_pb = ui.ui_factory.nested_progress_bar()
1826
with ui.ui_factory.nested_progress_bar() as child_pb:
1883
1827
entries = self._entries_to_incorporate()
1884
1828
entries = list(entries)
1885
for num, (entry, parent_id, path) in enumerate(entries):
1886
child_pb.update(gettext('Preparing file merge'), num, len(entries))
1829
for num, (entry, parent_id, relpath) in enumerate(entries):
1830
child_pb.update(gettext('Preparing file merge'),
1887
1832
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1833
path = osutils.pathjoin(self._source_subpath, relpath)
1888
1834
trans_id = transform.new_by_entry(path, self.tt, entry,
1889
parent_trans_id, self.other_tree)
1835
parent_trans_id, self.other_tree)
1892
1836
self._finish_computing_transform()
1894
1838
def _entries_to_incorporate(self):
1895
1839
"""Yields pairs of (inventory_entry, new_parent)."""
1896
other_inv = self.other_tree.root_inventory
1897
subdir_id = other_inv.path2id(self._source_subpath)
1840
subdir_id = self.other_tree.path2id(self._source_subpath)
1898
1841
if subdir_id is None:
1899
1842
# XXX: The error would be clearer if it gave the URL of the source
1900
1843
# branch, but we don't have a reference to that here.
1901
1844
raise PathNotInTree(self._source_subpath, "Source tree")
1902
subdir = other_inv[subdir_id]
1845
subdir = next(self.other_tree.iter_entries_by_dir(
1846
specific_files=[self._source_subpath]))[1]
1903
1847
parent_in_target = osutils.dirname(self._target_subdir)
1904
1848
target_id = self.this_tree.path2id(parent_in_target)
1905
1849
if target_id is None: