/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/merge.py

  • Committer: Jelmer Vernooij
  • Date: 2019-03-05 07:32:38 UTC
  • mto: (7290.1.21 work)
  • mto: This revision was merged to the branch mainline in revision 7311.
  • Revision ID: jelmer@jelmer.uk-20190305073238-zlqn981opwnqsmzi
Add appveyor configuration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
import contextlib
 
17
from __future__ import absolute_import
18
18
 
19
19
from .lazy_import import lazy_import
20
20
lazy_import(globals(), """
21
 
import patiencediff
22
 
 
23
21
from breezy import (
24
22
    branch as _mod_branch,
 
23
    cleanup,
25
24
    conflicts as _mod_conflicts,
26
25
    debug,
27
26
    graph as _mod_graph,
28
27
    merge3,
29
28
    osutils,
 
29
    patiencediff,
30
30
    revision as _mod_revision,
31
31
    textfile,
32
32
    trace,
 
33
    transform,
33
34
    tree as _mod_tree,
34
35
    tsort,
35
36
    ui,
36
37
    workingtree,
37
38
    )
38
39
from breezy.bzr import (
39
 
    conflicts as _mod_bzr_conflicts,
40
40
    generate_ids,
41
41
    versionedfile,
42
42
    )
43
43
from breezy.i18n import gettext
44
44
""")
45
 
from breezy.bzr.conflicts import Conflict as BzrConflict
46
45
from . import (
47
46
    decorators,
48
47
    errors,
49
48
    hooks,
50
49
    registry,
51
 
    transform,
 
50
    )
 
51
from .sixish import (
 
52
    viewitems,
52
53
    )
53
54
# TODO: Report back as changes are merged in
54
55
 
221
222
 
222
223
    There are some fields hooks can access:
223
224
 
 
225
    :ivar file_id: the file ID of the file being merged
224
226
    :ivar base_path: Path in base tree
225
227
    :ivar other_path: Path in other tree
226
228
    :ivar this_path: Path in this tree
227
229
    :ivar trans_id: the transform ID for the merge of this file
228
 
    :ivar this_kind: kind of file in 'this' tree
229
 
    :ivar other_kind: kind of file in 'other' tree
 
230
    :ivar this_kind: kind of file_id in 'this' tree
 
231
    :ivar other_kind: kind of file_id in 'other' tree
230
232
    :ivar winner: one of 'this', 'other', 'conflict'
231
233
    """
232
234
 
233
 
    def __init__(self, merger, paths, trans_id, this_kind, other_kind,
 
235
    def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
234
236
                 winner):
235
237
        self._merger = merger
 
238
        self.file_id = file_id
236
239
        self.paths = paths
237
240
        self.base_path, self.other_path, self.this_path = paths
238
241
        self.trans_id = trans_id
445
448
    def _add_parent(self):
446
449
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
447
450
        new_parent_trees = []
448
 
        with contextlib.ExitStack() as stack:
449
 
            for revision_id in new_parents:
450
 
                try:
451
 
                    tree = self.revision_tree(revision_id)
452
 
                except errors.NoSuchRevision:
453
 
                    tree = None
454
 
                else:
455
 
                    stack.enter_context(tree.lock_read())
456
 
                new_parent_trees.append((revision_id, tree))
457
 
            self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
 
451
        operation = cleanup.OperationWithCleanups(
 
452
            self.this_tree.set_parent_trees)
 
453
        for revision_id in new_parents:
 
454
            try:
 
455
                tree = self.revision_tree(revision_id)
 
456
            except errors.NoSuchRevision:
 
457
                tree = None
 
458
            else:
 
459
                tree.lock_read()
 
460
                operation.add_cleanup(tree.unlock)
 
461
            new_parent_trees.append((revision_id, tree))
 
462
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
458
463
 
459
464
    def set_other(self, other_revision, possible_transports=None):
460
465
        """Set the revision and tree to merge from.
631
636
        for hook in Merger.hooks['post_merge']:
632
637
            hook(merge)
633
638
        if self.recurse == 'down':
634
 
            for relpath in self.this_tree.iter_references():
 
639
            for relpath, file_id in self.this_tree.iter_references():
635
640
                sub_tree = self.this_tree.get_nested_tree(relpath)
636
641
                other_revision = self.other_tree.get_reference_revision(
637
642
                    relpath)
639
644
                    continue
640
645
                sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
641
646
                sub_merge.merge_type = self.merge_type
642
 
                other_branch = self.other_tree.reference_parent(relpath)
 
647
                other_branch = self.other_branch.reference_parent(
 
648
                    relpath, file_id)
643
649
                sub_merge.set_other_revision(other_revision, other_branch)
644
650
                base_tree_path = _mod_tree.find_previous_path(
645
651
                    self.this_tree, self.base_tree, relpath)
652
658
        return merge
653
659
 
654
660
    def do_merge(self):
655
 
        with contextlib.ExitStack() as stack:
656
 
            stack.enter_context(self.this_tree.lock_tree_write())
657
 
            if self.base_tree is not None:
658
 
                stack.enter_context(self.base_tree.lock_read())
659
 
            if self.other_tree is not None:
660
 
                stack.enter_context(self.other_tree.lock_read())
661
 
            merge = self._do_merge_to()
 
661
        operation = cleanup.OperationWithCleanups(self._do_merge_to)
 
662
        self.this_tree.lock_tree_write()
 
663
        operation.add_cleanup(self.this_tree.unlock)
 
664
        if self.base_tree is not None:
 
665
            self.base_tree.lock_read()
 
666
            operation.add_cleanup(self.base_tree.unlock)
 
667
        if self.other_tree is not None:
 
668
            self.other_tree.lock_read()
 
669
            operation.add_cleanup(self.other_tree.unlock)
 
670
        merge = operation.run_simple()
662
671
        if len(merge.cooked_conflicts) == 0:
663
672
            if not self.ignore_zero and not trace.is_quiet():
664
673
                trace.note(gettext("All changes applied successfully."))
683
692
    symlink_target = None
684
693
    text_sha1 = None
685
694
 
686
 
    def is_unmodified(self, other):
687
 
        return other is self
688
 
 
689
695
 
690
696
_none_entry = _InventoryNoneEntry()
691
697
 
755
761
            self.do_merge()
756
762
 
757
763
    def do_merge(self):
758
 
        with contextlib.ExitStack() as stack:
759
 
            stack.enter_context(self.working_tree.lock_tree_write())
760
 
            stack.enter_context(self.this_tree.lock_read())
761
 
            stack.enter_context(self.base_tree.lock_read())
762
 
            stack.enter_context(self.other_tree.lock_read())
763
 
            self.tt = self.working_tree.transform()
764
 
            stack.enter_context(self.tt)
765
 
            self._compute_transform()
766
 
            results = self.tt.apply(no_conflicts=True)
767
 
            self.write_modified(results)
768
 
            try:
769
 
                self.working_tree.add_conflicts(self.cooked_conflicts)
770
 
            except errors.UnsupportedOperation:
771
 
                pass
 
764
        operation = cleanup.OperationWithCleanups(self._do_merge)
 
765
        self.working_tree.lock_tree_write()
 
766
        operation.add_cleanup(self.working_tree.unlock)
 
767
        self.this_tree.lock_read()
 
768
        operation.add_cleanup(self.this_tree.unlock)
 
769
        self.base_tree.lock_read()
 
770
        operation.add_cleanup(self.base_tree.unlock)
 
771
        self.other_tree.lock_read()
 
772
        operation.add_cleanup(self.other_tree.unlock)
 
773
        operation.run()
 
774
 
 
775
    def _do_merge(self, operation):
 
776
        self.tt = transform.TreeTransform(self.working_tree, None)
 
777
        operation.add_cleanup(self.tt.finalize)
 
778
        self._compute_transform()
 
779
        results = self.tt.apply(no_conflicts=True)
 
780
        self.write_modified(results)
 
781
        try:
 
782
            self.working_tree.add_conflicts(self.cooked_conflicts)
 
783
        except errors.UnsupportedOperation:
 
784
            pass
772
785
 
773
786
    def make_preview_transform(self):
774
787
        with self.base_tree.lock_read(), self.other_tree.lock_read():
775
 
            self.tt = self.working_tree.preview_transform()
 
788
            self.tt = transform.TransformPreview(self.working_tree)
776
789
            self._compute_transform()
777
790
            return self.tt
778
791
 
779
792
    def _compute_transform(self):
780
793
        if self._lca_trees is None:
781
 
            entries = list(self._entries3())
 
794
            entries = self._entries3()
782
795
            resolver = self._three_way
783
796
        else:
784
 
            entries = list(self._entries_lca())
 
797
            entries = self._entries_lca()
785
798
            resolver = self._lca_multi_way
786
799
        # Prepare merge hooks
787
800
        factories = Merger.hooks['merge_file_content']
790
803
        self.active_hooks = [hook for hook in hooks if hook is not None]
791
804
        with ui.ui_factory.nested_progress_bar() as child_pb:
792
805
            for num, (file_id, changed, paths3, parents3, names3,
793
 
                      executable3, copied) in enumerate(entries):
794
 
                if copied:
795
 
                    # Treat copies as simple adds for now
796
 
                    paths3 = (None, paths3[1], None)
797
 
                    parents3 = (None, parents3[1], None)
798
 
                    names3 = (None, names3[1], None)
799
 
                    executable3 = (None, executable3[1], None)
800
 
                    changed = True
801
 
                    copied = False
802
 
                trans_id = self.tt.trans_id_file_id(file_id)
 
806
                      executable3) in enumerate(entries):
803
807
                # Try merging each entry
804
808
                child_pb.update(gettext('Preparing file merge'),
805
809
                                num, len(entries))
806
 
                self._merge_names(trans_id, file_id, paths3, parents3,
 
810
                self._merge_names(file_id, paths3, parents3,
807
811
                                  names3, resolver=resolver)
808
812
                if changed:
809
 
                    file_status = self._do_merge_contents(paths3, trans_id, file_id)
 
813
                    file_status = self._do_merge_contents(paths3, file_id)
810
814
                else:
811
815
                    file_status = 'unmodified'
812
 
                self._merge_executable(paths3, trans_id, executable3,
 
816
                self._merge_executable(paths3, file_id, executable3,
813
817
                                       file_status, resolver=resolver)
814
818
        self.tt.fixup_new_roots()
815
819
        self._finish_computing_transform()
820
824
        This is the second half of _compute_transform.
821
825
        """
822
826
        with ui.ui_factory.nested_progress_bar() as child_pb:
823
 
            fs_conflicts = transform.resolve_conflicts(
824
 
                self.tt, child_pb,
825
 
                lambda t, c: transform.conflict_pass(t, c, self.other_tree))
 
827
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
 
828
                                                       lambda t, c: transform.conflict_pass(t, c, self.other_tree))
826
829
        if self.change_reporter is not None:
827
830
            from breezy import delta
828
831
            delta.report_changes(
840
843
        other and this.  names3 is a tuple of names for base, other and this.
841
844
        executable3 is a tuple of execute-bit values for base, other and this.
842
845
        """
 
846
        result = []
843
847
        iterator = self.other_tree.iter_changes(self.base_tree,
844
848
                                                specific_files=self.interesting_files,
845
849
                                                extra_trees=[self.this_tree])
847
851
            self.interesting_files, trees=[self.other_tree])
848
852
        this_entries = dict(self.this_tree.iter_entries_by_dir(
849
853
                            specific_files=this_interesting_files))
850
 
        for change in iterator:
851
 
            if change.path[0] is not None:
 
854
        for (file_id, paths, changed, versioned, parents, names, kind,
 
855
             executable) in iterator:
 
856
            if paths[0] is not None:
852
857
                this_path = _mod_tree.find_previous_path(
853
 
                    self.base_tree, self.this_tree, change.path[0])
 
858
                    self.base_tree, self.this_tree, paths[0])
854
859
            else:
855
860
                this_path = _mod_tree.find_previous_path(
856
 
                    self.other_tree, self.this_tree, change.path[1])
 
861
                    self.other_tree, self.this_tree, paths[1])
857
862
            this_entry = this_entries.get(this_path)
858
863
            if this_entry is not None:
859
864
                this_name = this_entry.name
863
868
                this_name = None
864
869
                this_parent = None
865
870
                this_executable = None
866
 
            parents3 = change.parent_id + (this_parent,)
867
 
            names3 = change.name + (this_name,)
868
 
            paths3 = change.path + (this_path, )
869
 
            executable3 = change.executable + (this_executable,)
870
 
            yield (
871
 
                (change.file_id, change.changed_content, paths3,
872
 
                 parents3, names3, executable3, change.copied))
 
871
            parents3 = parents + (this_parent,)
 
872
            names3 = names + (this_name,)
 
873
            paths3 = paths + (this_path, )
 
874
            executable3 = executable + (this_executable,)
 
875
            result.append((file_id, changed, paths3,
 
876
                           parents3, names3, executable3))
 
877
        return result
873
878
 
874
879
    def _entries_lca(self):
875
880
        """Gather data about files modified between multiple trees.
879
884
 
880
885
        For the multi-valued entries, the format will be (BASE, [lca1, lca2])
881
886
 
882
 
        :return: [(file_id, changed, paths, parents, names, executable, copied)], where:
 
887
        :return: [(file_id, changed, paths, parents, names, executable)], where:
883
888
 
884
889
            * file_id: Simple file_id of the entry
885
890
            * changed: Boolean, True if the kind or contents changed else False
898
903
                self.interesting_files, lookup_trees)
899
904
        else:
900
905
            interesting_files = None
901
 
        from .multiwalker import MultiWalker
902
 
        walker = MultiWalker(self.other_tree, self._lca_trees)
 
906
        result = []
 
907
        walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
903
908
 
904
 
        for other_path, file_id, other_ie, lca_values in walker.iter_all():
 
909
        base_inventory = self.base_tree.root_inventory
 
910
        this_inventory = self.this_tree.root_inventory
 
911
        for path, file_id, other_ie, lca_values in walker.iter_all():
905
912
            # Is this modified at all from any of the other trees?
906
913
            if other_ie is None:
907
914
                other_ie = _none_entry
908
915
                other_path = None
 
916
            else:
 
917
                other_path = self.other_tree.id2path(file_id)
909
918
            if interesting_files is not None and other_path not in interesting_files:
910
919
                continue
911
920
 
915
924
            # we know that the ancestry is linear, and that OTHER did not
916
925
            # modify anything
917
926
            # See doc/developers/lca_merge_resolution.txt for details
918
 
            # We can't use this shortcut when other_revision is None,
919
 
            # because it may be None because things are WorkingTrees, and
920
 
            # not because it is *actually* None.
921
 
            is_unmodified = False
922
 
            for lca_path, ie in lca_values:
923
 
                if ie is not None and other_ie.is_unmodified(ie):
924
 
                    is_unmodified = True
925
 
                    break
926
 
            if is_unmodified:
927
 
                continue
 
927
            other_revision = other_ie.revision
 
928
            if other_revision is not None:
 
929
                # We can't use this shortcut when other_revision is None,
 
930
                # because it may be None because things are WorkingTrees, and
 
931
                # not because it is *actually* None.
 
932
                is_unmodified = False
 
933
                for lca_path, ie in lca_values:
 
934
                    if ie is not None and ie.revision == other_revision:
 
935
                        is_unmodified = True
 
936
                        break
 
937
                if is_unmodified:
 
938
                    continue
928
939
 
929
940
            lca_entries = []
930
941
            lca_paths = []
937
948
                    lca_paths.append(lca_path)
938
949
 
939
950
            try:
 
951
                base_ie = base_inventory.get_entry(file_id)
 
952
            except errors.NoSuchId:
 
953
                base_ie = _none_entry
 
954
                base_path = None
 
955
            else:
940
956
                base_path = self.base_tree.id2path(file_id)
941
 
            except errors.NoSuchId:
942
 
                base_path = None
943
 
                base_ie = _none_entry
944
 
            else:
945
 
                base_ie = next(self.base_tree.iter_entries_by_dir(specific_files=[base_path]))[1]
946
957
 
947
958
            try:
948
 
                this_path = self.this_tree.id2path(file_id)
 
959
                this_ie = this_inventory.get_entry(file_id)
949
960
            except errors.NoSuchId:
950
961
                this_ie = _none_entry
951
962
                this_path = None
952
963
            else:
953
 
                this_ie = next(self.this_tree.iter_entries_by_dir(specific_files=[this_path]))[1]
 
964
                this_path = self.this_tree.id2path(file_id)
954
965
 
955
966
            lca_kinds = []
956
967
            lca_parent_ids = []
1041
1052
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
1042
1053
 
1043
1054
            # If we have gotten this far, that means something has changed
1044
 
            yield (file_id, content_changed,
 
1055
            result.append((file_id, content_changed,
1045
1056
                           ((base_path, lca_paths),
1046
1057
                            other_path, this_path),
1047
1058
                           ((base_ie.parent_id, lca_parent_ids),
1049
1060
                           ((base_ie.name, lca_names),
1050
1061
                            other_ie.name, this_ie.name),
1051
1062
                           ((base_ie.executable, lca_executable),
1052
 
                            other_ie.executable, this_ie.executable),
1053
 
                           # Copy detection is not yet supported, so nothing is
1054
 
                           # a copy:
1055
 
                           False
1056
 
                           )
 
1063
                            other_ie.executable, this_ie.executable)
 
1064
                           ))
 
1065
        return result
1057
1066
 
1058
1067
    def write_modified(self, results):
1059
1068
        if not self.working_tree.supports_merge_modified():
1061
1070
        modified_hashes = {}
1062
1071
        for path in results.modified_paths:
1063
1072
            wt_relpath = self.working_tree.relpath(path)
1064
 
            if not self.working_tree.is_versioned(wt_relpath):
 
1073
            file_id = self.working_tree.path2id(wt_relpath)
 
1074
            if file_id is None:
1065
1075
                continue
1066
1076
            hash = self.working_tree.get_file_sha1(wt_relpath)
1067
1077
            if hash is None:
1068
1078
                continue
1069
 
            modified_hashes[wt_relpath] = hash
 
1079
            modified_hashes[file_id] = hash
1070
1080
        self.working_tree.set_merge_modified(modified_hashes)
1071
1081
 
1072
1082
    @staticmethod
1175
1185
        # At this point, the lcas disagree, and the tip disagree
1176
1186
        return 'conflict'
1177
1187
 
1178
 
    def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
1179
 
        """Perform a merge on file names and parents"""
 
1188
    def _merge_names(self, file_id, paths, parents, names, resolver):
 
1189
        """Perform a merge on file_id names and parents"""
1180
1190
        base_name, other_name, this_name = names
1181
1191
        base_parent, other_parent, this_parent = parents
1182
1192
        unused_base_path, other_path, this_path = paths
1195
1205
            # Creating helpers (.OTHER or .THIS) here cause problems down the
1196
1206
            # road if a ContentConflict needs to be created so we should not do
1197
1207
            # that
 
1208
            trans_id = self.tt.trans_id_file_id(file_id)
1198
1209
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1199
1210
                                        this_parent, this_name,
1200
1211
                                        other_parent, other_name))
1217
1228
                parent_trans_id = transform.ROOT_PARENT
1218
1229
            else:
1219
1230
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1220
 
            self.tt.adjust_path(name, parent_trans_id, trans_id)
 
1231
            self.tt.adjust_path(name, parent_trans_id,
 
1232
                                self.tt.trans_id_file_id(file_id))
1221
1233
 
1222
 
    def _do_merge_contents(self, paths, trans_id, file_id):
 
1234
    def _do_merge_contents(self, paths, file_id):
1223
1235
        """Performs a merge on file_id contents."""
1224
1236
        def contents_pair(tree, path):
1225
1237
            if path is None:
1263
1275
            return "unmodified"
1264
1276
        # We have a hypothetical conflict, but if we have files, then we
1265
1277
        # can try to merge the content
 
1278
        trans_id = self.tt.trans_id_file_id(file_id)
1266
1279
        params = MergeFileHookParams(
1267
 
            self, (base_path, other_path, this_path), trans_id, this_pair[0],
 
1280
            self, file_id, (base_path, other_path,
 
1281
                            this_path), trans_id, this_pair[0],
1268
1282
            other_pair[0], winner)
1269
1283
        hooks = self.active_hooks
1270
1284
        hook_status = 'not_applicable'
1292
1306
                    keep_this = True
1293
1307
                    # versioning the merged file will trigger a duplicate
1294
1308
                    # conflict
1295
 
                    self.tt.version_file(trans_id, file_id=file_id)
 
1309
                    self.tt.version_file(file_id, trans_id)
1296
1310
                    transform.create_from_tree(
1297
1311
                        self.tt, trans_id, self.other_tree,
1298
 
                        other_path,
1299
 
                        filter_tree_path=self._get_filter_tree_path(other_path))
 
1312
                        other_path, file_id=file_id,
 
1313
                        filter_tree_path=self._get_filter_tree_path(file_id))
1300
1314
                    inhibit_content_conflict = True
1301
1315
            elif params.other_kind is None:  # file_id is not in OTHER
1302
1316
                # Is the name used for a different file_id ?
1315
1329
                # This is a contents conflict, because none of the available
1316
1330
                # functions could merge it.
1317
1331
                file_group = self._dump_conflicts(
1318
 
                    name, (base_path, other_path, this_path), parent_id)
1319
 
                for tid in file_group:
1320
 
                    self.tt.version_file(tid, file_id=file_id)
1321
 
                    break
 
1332
                    name, (base_path, other_path, this_path), parent_id,
 
1333
                    file_id, set_version=True)
1322
1334
                self._raw_conflicts.append(('contents conflict', file_group))
1323
1335
        elif hook_status == 'success':
1324
1336
            self.tt.create_file(lines, trans_id)
1330
1342
            name = self.tt.final_name(trans_id)
1331
1343
            parent_id = self.tt.final_parent(trans_id)
1332
1344
            self._dump_conflicts(
1333
 
                name, (base_path, other_path, this_path), parent_id)
 
1345
                name, (base_path, other_path, this_path), parent_id, file_id)
1334
1346
        elif hook_status == 'delete':
1335
1347
            self.tt.unversion_file(trans_id)
1336
1348
            result = "deleted"
1341
1353
        else:
1342
1354
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1343
1355
        if not this_path and result == "modified":
1344
 
            self.tt.version_file(trans_id, file_id=file_id)
 
1356
            self.tt.version_file(file_id, trans_id)
1345
1357
        if not keep_this:
1346
1358
            # The merge has been performed and produced a new content, so the
1347
1359
            # old contents should not be retained.
1350
1362
 
1351
1363
    def _default_other_winner_merge(self, merge_hook_params):
1352
1364
        """Replace this contents with other."""
 
1365
        file_id = merge_hook_params.file_id
1353
1366
        trans_id = merge_hook_params.trans_id
1354
1367
        if merge_hook_params.other_path is not None:
1355
1368
            # OTHER changed the file
1356
1369
            transform.create_from_tree(
1357
1370
                self.tt, trans_id, self.other_tree,
1358
 
                merge_hook_params.other_path,
1359
 
                filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
 
1371
                merge_hook_params.other_path, file_id=file_id,
 
1372
                filter_tree_path=self._get_filter_tree_path(file_id))
1360
1373
            return 'done', None
1361
1374
        elif merge_hook_params.this_path is not None:
1362
1375
            # OTHER deleted the file
1363
1376
            return 'delete', None
1364
1377
        else:
1365
1378
            raise AssertionError(
1366
 
                'winner is OTHER, but file %r not in THIS or OTHER tree'
1367
 
                % (merge_hook_params.base_path,))
 
1379
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1380
                % (file_id,))
1368
1381
 
1369
1382
    def merge_contents(self, merge_hook_params):
1370
1383
        """Fallback merge logic after user installed hooks."""
1380
1393
            # have agreement that output should be a file.
1381
1394
            try:
1382
1395
                self.text_merge(merge_hook_params.trans_id,
1383
 
                                merge_hook_params.paths)
 
1396
                                merge_hook_params.paths, merge_hook_params.file_id)
1384
1397
            except errors.BinaryFile:
1385
1398
                return 'not_applicable', None
1386
1399
            return 'done', None
1400
1413
                return []
1401
1414
            return tree.get_file_lines(path)
1402
1415
 
1403
 
    def text_merge(self, trans_id, paths):
1404
 
        """Perform a three-way text merge on a file"""
 
1416
    def text_merge(self, trans_id, paths, file_id):
 
1417
        """Perform a three-way text merge on a file_id"""
1405
1418
        # it's possible that we got here with base as a different type.
1406
1419
        # if so, we just want two-way text conflicts.
1407
1420
        base_path, other_path, this_path = paths
1436
1449
            self._raw_conflicts.append(('text conflict', trans_id))
1437
1450
            name = self.tt.final_name(trans_id)
1438
1451
            parent_id = self.tt.final_parent(trans_id)
1439
 
            file_group = self._dump_conflicts(
1440
 
                name, paths, parent_id,
1441
 
                lines=(base_lines, other_lines, this_lines))
 
1452
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
 
1453
                                              this_lines, base_lines,
 
1454
                                              other_lines)
1442
1455
            file_group.append(trans_id)
1443
1456
 
1444
 
    def _get_filter_tree_path(self, path):
 
1457
    def _get_filter_tree_path(self, file_id):
1445
1458
        if self.this_tree.supports_content_filtering():
1446
1459
            # We get the path from the working tree if it exists.
1447
1460
            # That fails though when OTHER is adding a file, so
1448
1461
            # we fall back to the other tree to find the path if
1449
1462
            # it doesn't exist locally.
1450
 
            filter_path = _mod_tree.find_previous_path(
1451
 
                self.other_tree, self.working_tree, path)
1452
 
            if filter_path is None:
1453
 
                filter_path = path
1454
 
            return filter_path
1455
 
        # Skip the lookup for older formats
 
1463
            try:
 
1464
                return self.this_tree.id2path(file_id)
 
1465
            except errors.NoSuchId:
 
1466
                return self.other_tree.id2path(file_id)
 
1467
        # Skip the id2path lookup for older formats
1456
1468
        return None
1457
1469
 
1458
 
    def _dump_conflicts(self, name, paths, parent_id, lines=None,
 
1470
    def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
 
1471
                        base_lines=None, other_lines=None, set_version=False,
1459
1472
                        no_base=False):
1460
1473
        """Emit conflict files.
1461
1474
        If this_lines, base_lines, or other_lines are omitted, they will be
1463
1476
        or .BASE (in that order) will be created as versioned files.
1464
1477
        """
1465
1478
        base_path, other_path, this_path = paths
1466
 
        if lines:
1467
 
            base_lines, other_lines, this_lines = lines
1468
 
        else:
1469
 
            base_lines = other_lines = this_lines = None
1470
1479
        data = [('OTHER', self.other_tree, other_path, other_lines),
1471
1480
                ('THIS', self.this_tree, this_path, this_lines)]
1472
1481
        if not no_base:
1473
1482
            data.append(('BASE', self.base_tree, base_path, base_lines))
1474
1483
 
1475
1484
        # We need to use the actual path in the working tree of the file here,
1476
 
        if self.this_tree.supports_content_filtering():
1477
 
            filter_tree_path = this_path
 
1485
        # ignoring the conflict suffixes
 
1486
        wt = self.this_tree
 
1487
        if wt.supports_content_filtering():
 
1488
            try:
 
1489
                filter_tree_path = wt.id2path(file_id)
 
1490
            except errors.NoSuchId:
 
1491
                # file has been deleted
 
1492
                filter_tree_path = None
1478
1493
        else:
 
1494
            # Skip the id2path lookup for older formats
1479
1495
            filter_tree_path = None
1480
1496
 
 
1497
        versioned = False
1481
1498
        file_group = []
1482
1499
        for suffix, tree, path, lines in data:
1483
1500
            if path is not None:
1484
1501
                trans_id = self._conflict_file(
1485
 
                    name, parent_id, path, tree, suffix, lines,
 
1502
                    name, parent_id, path, tree, file_id, suffix, lines,
1486
1503
                    filter_tree_path)
1487
1504
                file_group.append(trans_id)
 
1505
                if set_version and not versioned:
 
1506
                    self.tt.version_file(file_id, trans_id)
 
1507
                    versioned = True
1488
1508
        return file_group
1489
1509
 
1490
 
    def _conflict_file(self, name, parent_id, path, tree, suffix,
 
1510
    def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1491
1511
                       lines=None, filter_tree_path=None):
1492
1512
        """Emit a single conflict file."""
1493
1513
        name = name + '.' + suffix
1494
1514
        trans_id = self.tt.create_path(name, parent_id)
1495
1515
        transform.create_from_tree(
1496
1516
            self.tt, trans_id, tree, path,
1497
 
            chunks=lines,
 
1517
            file_id=file_id, chunks=lines,
1498
1518
            filter_tree_path=filter_tree_path)
1499
1519
        return trans_id
1500
1520
 
1501
 
    def _merge_executable(self, paths, trans_id, executable, file_status,
 
1521
    def merge_executable(self, paths, file_id, file_status):
 
1522
        """Perform a merge on the execute bit."""
 
1523
        executable = [self.executable(t, p, file_id)
 
1524
                      for t, p in zip([self.base_tree, self.other_tree, self.this_tree], paths)]
 
1525
        self._merge_executable(paths, file_id, executable, file_status,
 
1526
                               resolver=self._three_way)
 
1527
 
 
1528
    def _merge_executable(self, paths, file_id, executable, file_status,
1502
1529
                          resolver):
1503
1530
        """Perform a merge on the execute bit."""
1504
1531
        base_executable, other_executable, this_executable = executable
1515
1542
                winner = "other"
1516
1543
        if winner == 'this' and file_status != "modified":
1517
1544
            return
 
1545
        trans_id = self.tt.trans_id_file_id(file_id)
1518
1546
        if self.tt.final_kind(trans_id) != "file":
1519
1547
            return
1520
1548
        if winner == "this":
1527
1555
            elif base_path is not None:
1528
1556
                executability = base_executable
1529
1557
        if executability is not None:
 
1558
            trans_id = self.tt.trans_id_file_id(file_id)
1530
1559
            self.tt.set_executability(executability, trans_id)
1531
1560
 
1532
1561
    def cook_conflicts(self, fs_conflicts):
1533
1562
        """Convert all conflicts into a form that doesn't depend on trans_id"""
1534
 
        self.cooked_conflicts = list(self.tt.cook_conflicts(
1535
 
            list(fs_conflicts) + self._raw_conflicts))
 
1563
        content_conflict_file_ids = set()
 
1564
        cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
 
1565
        fp = transform.FinalPaths(self.tt)
 
1566
        for conflict in self._raw_conflicts:
 
1567
            conflict_type = conflict[0]
 
1568
            if conflict_type == 'path conflict':
 
1569
                (trans_id, file_id,
 
1570
                 this_parent, this_name,
 
1571
                 other_parent, other_name) = conflict[1:]
 
1572
                if this_parent is None or this_name is None:
 
1573
                    this_path = '<deleted>'
 
1574
                else:
 
1575
                    parent_path = fp.get_path(
 
1576
                        self.tt.trans_id_file_id(this_parent))
 
1577
                    this_path = osutils.pathjoin(parent_path, this_name)
 
1578
                if other_parent is None or other_name is None:
 
1579
                    other_path = '<deleted>'
 
1580
                else:
 
1581
                    if other_parent == self.other_tree.get_root_id():
 
1582
                        # The tree transform doesn't know about the other root,
 
1583
                        # so we special case here to avoid a NoFinalPath
 
1584
                        # exception
 
1585
                        parent_path = ''
 
1586
                    else:
 
1587
                        parent_path = fp.get_path(
 
1588
                            self.tt.trans_id_file_id(other_parent))
 
1589
                    other_path = osutils.pathjoin(parent_path, other_name)
 
1590
                c = _mod_conflicts.Conflict.factory(
 
1591
                    'path conflict', path=this_path,
 
1592
                    conflict_path=other_path,
 
1593
                    file_id=file_id)
 
1594
            elif conflict_type == 'contents conflict':
 
1595
                for trans_id in conflict[1]:
 
1596
                    file_id = self.tt.final_file_id(trans_id)
 
1597
                    if file_id is not None:
 
1598
                        # Ok we found the relevant file-id
 
1599
                        break
 
1600
                path = fp.get_path(trans_id)
 
1601
                for suffix in ('.BASE', '.THIS', '.OTHER'):
 
1602
                    if path.endswith(suffix):
 
1603
                        # Here is the raw path
 
1604
                        path = path[:-len(suffix)]
 
1605
                        break
 
1606
                c = _mod_conflicts.Conflict.factory(conflict_type,
 
1607
                                                    path=path, file_id=file_id)
 
1608
                content_conflict_file_ids.add(file_id)
 
1609
            elif conflict_type == 'text conflict':
 
1610
                trans_id = conflict[1]
 
1611
                path = fp.get_path(trans_id)
 
1612
                file_id = self.tt.final_file_id(trans_id)
 
1613
                c = _mod_conflicts.Conflict.factory(conflict_type,
 
1614
                                                    path=path, file_id=file_id)
 
1615
            else:
 
1616
                raise AssertionError('bad conflict type: %r' % (conflict,))
 
1617
            cooked_conflicts.append(c)
 
1618
 
 
1619
        self.cooked_conflicts = []
 
1620
        # We want to get rid of path conflicts when a corresponding contents
 
1621
        # conflict exists. This can occur when one branch deletes a file while
 
1622
        # the other renames *and* modifies it. In this case, the content
 
1623
        # conflict is enough.
 
1624
        for c in cooked_conflicts:
 
1625
            if (c.typestring == 'path conflict'
 
1626
                    and c.file_id in content_conflict_file_ids):
 
1627
                continue
 
1628
            self.cooked_conflicts.append(c)
 
1629
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1536
1630
 
1537
1631
 
1538
1632
class WeaveMerger(Merge3Merger):
1543
1637
    history_based = True
1544
1638
    requires_file_merge_plan = True
1545
1639
 
1546
 
    def _generate_merge_plan(self, this_path, base):
1547
 
        return self.this_tree.plan_file_merge(this_path, self.other_tree,
 
1640
    def _generate_merge_plan(self, file_id, base):
 
1641
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1548
1642
                                              base=base)
1549
1643
 
1550
 
    def _merged_lines(self, this_path):
 
1644
    def _merged_lines(self, file_id):
1551
1645
        """Generate the merged lines.
1552
1646
        There is no distinction between lines that are meant to contain <<<<<<<
1553
1647
        and conflicts.
1556
1650
            base = self.base_tree
1557
1651
        else:
1558
1652
            base = None
1559
 
        plan = self._generate_merge_plan(this_path, base)
 
1653
        plan = self._generate_merge_plan(file_id, base)
1560
1654
        if 'merge' in debug.debug_flags:
1561
1655
            plan = list(plan)
1562
1656
            trans_id = self.tt.trans_id_file_id(file_id)
1572
1666
            base_lines = None
1573
1667
        return lines, base_lines
1574
1668
 
1575
 
    def text_merge(self, trans_id, paths):
 
1669
    def text_merge(self, trans_id, paths, file_id):
1576
1670
        """Perform a (weave) text merge for a given file and file-id.
1577
1671
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1578
1672
        and a conflict will be noted.
1579
1673
        """
1580
1674
        base_path, other_path, this_path = paths
1581
 
        lines, base_lines = self._merged_lines(this_path)
 
1675
        lines, base_lines = self._merged_lines(file_id)
1582
1676
        lines = list(lines)
1583
1677
        # Note we're checking whether the OUTPUT is binary in this case,
1584
1678
        # because we don't want to get into weave merge guts.
1589
1683
            self._raw_conflicts.append(('text conflict', trans_id))
1590
1684
            name = self.tt.final_name(trans_id)
1591
1685
            parent_id = self.tt.final_parent(trans_id)
1592
 
            file_group = self._dump_conflicts(name, paths, parent_id,
1593
 
                                              (base_lines, None, None),
1594
 
                                              no_base=False)
 
1686
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
 
1687
                                              no_base=False,
 
1688
                                              base_lines=base_lines)
1595
1689
            file_group.append(trans_id)
1596
1690
 
1597
1691
 
1599
1693
 
1600
1694
    requires_file_merge_plan = True
1601
1695
 
1602
 
    def _generate_merge_plan(self, this_path, base):
1603
 
        return self.this_tree.plan_file_lca_merge(this_path, self.other_tree,
 
1696
    def _generate_merge_plan(self, file_id, base):
 
1697
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1604
1698
                                                  base=base)
1605
1699
 
1606
1700
 
1617
1711
                out_file.write(line)
1618
1712
        return out_path
1619
1713
 
1620
 
    def text_merge(self, trans_id, paths):
 
1714
    def text_merge(self, trans_id, paths, file_id):
1621
1715
        """Perform a diff3 merge using a specified file-id and trans-id.
1622
1716
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1623
1717
        will be dumped, and a will be conflict noted.
1641
1735
            if status == 1:
1642
1736
                name = self.tt.final_name(trans_id)
1643
1737
                parent_id = self.tt.final_parent(trans_id)
1644
 
                self._dump_conflicts(name, paths, parent_id)
 
1738
                self._dump_conflicts(name, paths, parent_id, file_id)
1645
1739
                self._raw_conflicts.append(('text conflict', trans_id))
1646
1740
        finally:
1647
1741
            osutils.rmtree(temp_dir)
1771
1865
 
1772
1866
    def _entries_to_incorporate(self):
1773
1867
        """Yields pairs of (inventory_entry, new_parent)."""
1774
 
        subdir_id = self.other_tree.path2id(self._source_subpath)
 
1868
        other_inv = self.other_tree.root_inventory
 
1869
        subdir_id = other_inv.path2id(self._source_subpath)
1775
1870
        if subdir_id is None:
1776
1871
            # XXX: The error would be clearer if it gave the URL of the source
1777
1872
            # branch, but we don't have a reference to that here.
1778
1873
            raise PathNotInTree(self._source_subpath, "Source tree")
1779
 
        subdir = next(self.other_tree.iter_entries_by_dir(
1780
 
            specific_files=[self._source_subpath]))[1]
 
1874
        subdir = other_inv.get_entry(subdir_id)
1781
1875
        parent_in_target = osutils.dirname(self._target_subdir)
1782
1876
        target_id = self.this_tree.path2id(parent_in_target)
1783
1877
        if target_id is None:
1785
1879
        name_in_target = osutils.basename(self._target_subdir)
1786
1880
        merge_into_root = subdir.copy()
1787
1881
        merge_into_root.name = name_in_target
1788
 
        try:
1789
 
            self.this_tree.id2path(merge_into_root.file_id)
1790
 
        except errors.NoSuchId:
1791
 
            pass
1792
 
        else:
 
1882
        if self.this_tree.has_id(merge_into_root.file_id):
1793
1883
            # Give the root a new file-id.
1794
1884
            # This can happen fairly easily if the directory we are
1795
1885
            # incorporating is the root, and both trees have 'TREE_ROOT' as
1803
1893
        if subdir.kind != 'directory':
1804
1894
            # No children, so we are done.
1805
1895
            return
1806
 
        for path, entry in self.other_tree.root_inventory.iter_entries_by_dir(subdir_id):
 
1896
        for path, entry in other_inv.iter_entries_by_dir(subdir_id):
1807
1897
            parent_id = entry.parent_id
1808
1898
            if parent_id == subdir.file_id:
1809
1899
                # The root's parent ID has changed, so make sure children of
1937
2027
        for record in self.vf.get_record_stream(keys, 'unordered', True):
1938
2028
            if record.storage_kind == 'absent':
1939
2029
                raise errors.RevisionNotPresent(record.key, self.vf)
1940
 
            result[record.key[-1]] = record.get_bytes_as('lines')
 
2030
            result[record.key[-1]] = osutils.chunks_to_lines(
 
2031
                record.get_bytes_as('chunked'))
1941
2032
        return result
1942
2033
 
1943
2034
    def plan_merge(self):
2185
2276
        filtered_parent_map = {}
2186
2277
        child_map = {}
2187
2278
        tails = []
2188
 
        for key, parent_keys in parent_map.items():
 
2279
        for key, parent_keys in viewitems(parent_map):
2189
2280
            culled_parent_keys = [p for p in parent_keys if p in parent_map]
2190
2281
            if not culled_parent_keys:
2191
2282
                tails.append(key)