/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: 2018-03-25 00:39:16 UTC
  • mfrom: (6926 work)
  • mto: This revision was merged to the branch mainline in revision 6928.
  • Revision ID: jelmer@jelmer.uk-20180325003916-mqa5uj4uq55hrjq9
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
56
56
# TODO: Report back as changes are merged in
57
57
 
58
58
 
59
 
def transform_tree(from_tree, to_tree, interesting_ids=None):
 
59
def transform_tree(from_tree, to_tree, interesting_files=None):
60
60
    from_tree.lock_tree_write()
61
61
    operation = cleanup.OperationWithCleanups(merge_inner)
62
62
    operation.add_cleanup(from_tree.unlock)
63
63
    operation.run_simple(from_tree.branch, to_tree, from_tree,
64
 
        ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
 
64
        ignore_zero=True, this_tree=from_tree,
 
65
        interesting_files=interesting_files)
65
66
 
66
67
 
67
68
class MergeHooks(hooks.Hooks):
94
95
    """PerFileMerger objects are used by plugins extending merge for breezy.
95
96
 
96
97
    See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
97
 
    
 
98
 
98
99
    :ivar merger: The Merge3Merger performing the merge.
99
100
    """
100
101
 
104
105
 
105
106
    def merge_contents(self, merge_params):
106
107
        """Attempt to merge the contents of a single file.
107
 
        
 
108
 
108
109
        :param merge_params: A breezy.merge.MergeFileHookParams
109
110
        :return: A tuple of (status, chunks), where status is one of
110
111
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
130
131
        """
131
132
        raise NotImplementedError(self.file_matches)
132
133
 
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.
136
 
        """
137
 
        return osutils.basename(tree.id2path(params.file_id))
138
 
 
139
 
    def get_filepath(self, params, tree):
140
 
        """Calculate the path to the file in a tree.
141
 
 
142
 
        :param params: A MergeFileHookParams describing the file to merge
143
 
        :param tree: a Tree, e.g. self.merger.this_tree.
144
 
        """
145
 
        return tree.id2path(params.file_id)
146
 
 
147
134
    def merge_contents(self, params):
148
135
        """Merge the contents of a single file."""
149
136
        # Check whether this custom merge logic should be used.
174
161
    classes should implement ``merge_text``.
175
162
 
176
163
    See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
177
 
    
 
164
 
178
165
    :ivar affected_files: The configured file paths to merge.
179
166
 
180
167
    :cvar name_prefix: The prefix to use when looking up configuration
181
168
        details. <name_prefix>_merge_files describes the files targeted by the
182
169
        hook for example.
183
 
        
 
170
 
184
171
    :cvar default_files: The default file paths to merge when no configuration
185
172
        is present.
186
173
    """
216
203
                affected_files = self.default_files
217
204
            self.affected_files = affected_files
218
205
        if affected_files:
219
 
            filepath = self.get_filepath(params, self.merger.this_tree)
 
206
            filepath = params.this_path
220
207
            if filepath in affected_files:
221
208
                return True
222
209
        return False
240
227
    There are some fields hooks can access:
241
228
 
242
229
    :ivar file_id: the file ID of the file being merged
 
230
    :ivar base_path: Path in base tree
 
231
    :ivar other_path: Path in other tree
 
232
    :ivar this_path: Path in this tree
243
233
    :ivar trans_id: the transform ID for the merge of this file
244
234
    :ivar this_kind: kind of file_id in 'this' tree
245
235
    :ivar other_kind: kind of file_id in 'other' tree
246
236
    :ivar winner: one of 'this', 'other', 'conflict'
247
237
    """
248
238
 
249
 
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
239
    def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
250
240
            winner):
251
241
        self._merger = merger
252
242
        self.file_id = file_id
 
243
        self.paths = paths
 
244
        self.base_path, self.other_path, self.this_path = paths
253
245
        self.trans_id = trans_id
254
246
        self.this_kind = this_kind
255
247
        self.other_kind = other_kind
262
254
    @decorators.cachedproperty
263
255
    def base_lines(self):
264
256
        """The lines of the 'base' version of the file."""
265
 
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
257
        return self._merger.get_lines(self._merger.base_tree, self.base_path, self.file_id)
266
258
 
267
259
    @decorators.cachedproperty
268
260
    def this_lines(self):
269
261
        """The lines of the 'this' version of the file."""
270
 
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
262
        return self._merger.get_lines(self._merger.this_tree, self.this_path, self.file_id)
271
263
 
272
264
    @decorators.cachedproperty
273
265
    def other_lines(self):
274
266
        """The lines of the 'other' version of the file."""
275
 
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
267
        return self._merger.get_lines(self._merger.other_tree, self.other_path, self.file_id)
276
268
 
277
269
 
278
270
class Merger(object):
295
287
        self.base_tree = base_tree
296
288
        self.ignore_zero = False
297
289
        self.backup_files = False
298
 
        self.interesting_ids = None
299
290
        self.interesting_files = None
300
291
        self.show_base = False
301
292
        self.reprocess = False
610
601
    def make_merger(self):
611
602
        kwargs = {'working_tree': self.this_tree, 'this_tree': self.this_tree,
612
603
                  'other_tree': self.other_tree,
613
 
                  'interesting_ids': self.interesting_ids,
614
604
                  'interesting_files': self.interesting_files,
615
605
                  'this_branch': self.this_branch,
616
606
                  'other_branch': self.other_branch,
661
651
                other_branch = self.other_branch.reference_parent(file_id,
662
652
                                                                  relpath)
663
653
                sub_merge.set_other_revision(other_revision, other_branch)
664
 
                base_tree_path = self.base_tree.id2path(file_id)
 
654
                base_tree_path = _mod_tree.find_previous_path(
 
655
                    self.this_tree, self.base_tree, relpath)
665
656
                base_revision = self.base_tree.get_reference_revision(
666
657
                    base_tree_path, file_id)
667
658
                sub_merge.base_tree = \
718
709
    supports_reverse_cherrypick = True
719
710
    winner_idx = {"this": 2, "other": 1, "conflict": 1}
720
711
    supports_lca_trees = True
 
712
    requires_file_merge_plan = False
721
713
 
722
714
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
723
 
                 interesting_ids=None, reprocess=False, show_base=False,
 
715
                 reprocess=False, show_base=False,
724
716
                 change_reporter=None, interesting_files=None, do_merge=True,
725
717
                 cherrypick=False, lca_trees=None, this_branch=None,
726
718
                 other_branch=None):
733
725
        :param this_branch: The branch associated with this_tree.  Defaults to
734
726
            this_tree.branch if not supplied.
735
727
        :param other_branch: The branch associated with other_tree, if any.
736
 
        :param interesting_ids: The file_ids of files that should be
737
 
            participate in the merge.  May not be combined with
738
 
            interesting_files.
739
728
        :param: reprocess If True, perform conflict-reduction processing.
740
729
        :param show_base: If True, show the base revision in text conflicts.
741
730
            (incompatible with reprocess)
742
731
        :param change_reporter: An object that should report changes made
743
732
        :param interesting_files: The tree-relative paths of files that should
744
733
            participate in the merge.  If these paths refer to directories,
745
 
            the contents of those directories will also be included.  May not
746
 
            be combined with interesting_ids.  If neither interesting_files nor
747
 
            interesting_ids is specified, all files may participate in the
 
734
            the contents of those directories will also be included.  If not
 
735
            specified, all files may participate in the
748
736
            merge.
749
737
        :param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
750
738
            if the ancestry was found to include a criss-cross merge.
751
739
            Otherwise should be None.
752
740
        """
753
741
        object.__init__(self)
754
 
        if interesting_files is not None and interesting_ids is not None:
755
 
            raise ValueError(
756
 
                'specify either interesting_ids or interesting_files')
757
742
        if this_branch is None:
758
743
            this_branch = this_tree.branch
759
 
        self.interesting_ids = interesting_ids
760
744
        self.interesting_files = interesting_files
761
745
        self.working_tree = working_tree
762
746
        self.this_tree = this_tree
827
811
        # One hook for each registered one plus our default merger
828
812
        hooks = [factory(self) for factory in factories] + [self]
829
813
        self.active_hooks = [hook for hook in hooks if hook is not None]
830
 
        child_pb = ui.ui_factory.nested_progress_bar()
831
 
        try:
832
 
            for num, (file_id, changed, parents3, names3,
 
814
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
815
            for num, (file_id, changed, paths3, parents3, names3,
833
816
                      executable3) in enumerate(entries):
834
817
                # Try merging each entry
835
818
                child_pb.update(gettext('Preparing file merge'),
836
819
                                num, len(entries))
837
 
                self._merge_names(file_id, parents3, names3, resolver=resolver)
 
820
                self._merge_names(file_id, paths3, parents3, names3, resolver=resolver)
838
821
                if changed:
839
 
                    file_status = self._do_merge_contents(file_id)
 
822
                    file_status = self._do_merge_contents(paths3, file_id)
840
823
                else:
841
824
                    file_status = 'unmodified'
842
 
                self._merge_executable(file_id,
843
 
                    executable3, file_status, resolver=resolver)
844
 
        finally:
845
 
            child_pb.finished()
 
825
                self._merge_executable(paths3, file_id, executable3,
 
826
                        file_status, resolver=resolver)
846
827
        self.tt.fixup_new_roots()
847
828
        self._finish_computing_transform()
848
829
 
851
832
 
852
833
        This is the second half of _compute_transform.
853
834
        """
854
 
        child_pb = ui.ui_factory.nested_progress_bar()
855
 
        try:
 
835
        with ui.ui_factory.nested_progress_bar() as child_pb:
856
836
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
857
837
                lambda t, c: transform.conflict_pass(t, c, self.other_tree))
858
 
        finally:
859
 
            child_pb.finished()
860
838
        if self.change_reporter is not None:
861
839
            from breezy import delta
862
840
            delta.report_changes(
878
856
        iterator = self.other_tree.iter_changes(self.base_tree,
879
857
                specific_files=self.interesting_files,
880
858
                extra_trees=[self.this_tree])
881
 
        this_entries = dict((e.file_id, e) for p, e in
882
 
                            self.this_tree.iter_entries_by_dir(
883
 
                            self.interesting_ids))
 
859
        this_interesting_files = self.this_tree.find_related_paths_across_trees(
 
860
                self.interesting_files, trees=[self.other_tree])
 
861
        this_entries = dict(self.this_tree.iter_entries_by_dir(
 
862
                            specific_files=this_interesting_files))
884
863
        for (file_id, paths, changed, versioned, parents, names, kind,
885
864
             executable) in iterator:
886
 
            if (self.interesting_ids is not None and
887
 
                file_id not in self.interesting_ids):
888
 
                continue
889
 
            entry = this_entries.get(file_id)
890
 
            if entry is not None:
891
 
                this_name = entry.name
892
 
                this_parent = entry.parent_id
893
 
                this_executable = entry.executable
 
865
            if paths[0] is not None:
 
866
                this_path = _mod_tree.find_previous_path(
 
867
                        self.base_tree, self.this_tree, paths[0])
 
868
            else:
 
869
                this_path = _mod_tree.find_previous_path(
 
870
                        self.other_tree, self.this_tree, paths[1])
 
871
            this_entry = this_entries.get(this_path)
 
872
            if this_entry is not None:
 
873
                this_name = this_entry.name
 
874
                this_parent = this_entry.parent_id
 
875
                this_executable = this_entry.executable
894
876
            else:
895
877
                this_name = None
896
878
                this_parent = None
897
879
                this_executable = None
898
880
            parents3 = parents + (this_parent,)
899
881
            names3 = names + (this_name,)
 
882
            paths3 = paths + (this_path, )
900
883
            executable3 = executable + (this_executable,)
901
 
            result.append((file_id, changed, parents3, names3, executable3))
 
884
            result.append((file_id, changed, paths3, parents3, names3, executable3))
902
885
        return result
903
886
 
904
887
    def _entries_lca(self):
909
892
 
910
893
        For the multi-valued entries, the format will be (BASE, [lca1, lca2])
911
894
 
912
 
        :return: [(file_id, changed, parents, names, executable)], where:
 
895
        :return: [(file_id, changed, paths, parents, names, executable)], where:
913
896
 
914
897
            * file_id: Simple file_id of the entry
915
898
            * changed: Boolean, True if the kind or contents changed else False
 
899
            * paths: ((base, [path, in, lcas]), path_other, path_this)
916
900
            * parents: ((base, [parent_id, in, lcas]), parent_id_other,
917
901
                        parent_id_this)
918
902
            * names:   ((base, [name, in, lcas]), name_in_other, name_in_this)
923
907
            lookup_trees = [self.this_tree, self.base_tree]
924
908
            lookup_trees.extend(self._lca_trees)
925
909
            # I think we should include the lca trees as well
926
 
            interesting_ids = self.other_tree.paths2ids(self.interesting_files,
927
 
                                                        lookup_trees)
 
910
            interesting_files = self.other_tree.find_related_paths_across_trees(
 
911
                    self.interesting_files, lookup_trees)
928
912
        else:
929
 
            interesting_ids = self.interesting_ids
 
913
            interesting_files = None
930
914
        result = []
931
915
        walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
932
916
 
936
920
            # Is this modified at all from any of the other trees?
937
921
            if other_ie is None:
938
922
                other_ie = _none_entry
939
 
            if interesting_ids is not None and file_id not in interesting_ids:
 
923
                other_path = None
 
924
            else:
 
925
                other_path = self.other_tree.id2path(file_id)
 
926
            if interesting_files is not None and other_path not in interesting_files:
940
927
                continue
941
928
 
942
929
            # If other_revision is found in any of the lcas, that means this
959
946
                    continue
960
947
 
961
948
            lca_entries = []
 
949
            lca_paths = []
962
950
            for lca_path, lca_ie in lca_values:
963
951
                if lca_ie is None:
964
952
                    lca_entries.append(_none_entry)
 
953
                    lca_paths.append(None)
965
954
                else:
966
955
                    lca_entries.append(lca_ie)
 
956
                    lca_paths.append(path)
967
957
 
968
 
            if base_inventory.has_id(file_id):
 
958
            try:
969
959
                base_ie = base_inventory[file_id]
970
 
            else:
 
960
            except errors.NoSuchId:
971
961
                base_ie = _none_entry
 
962
                base_path = None
 
963
            else:
 
964
                base_path = self.base_tree.id2path(file_id)
972
965
 
973
 
            if this_inventory.has_id(file_id):
 
966
            try:
974
967
                this_ie = this_inventory[file_id]
975
 
            else:
 
968
            except errors.NoSuchId:
976
969
                this_ie = _none_entry
 
970
                this_path = None
 
971
            else:
 
972
                this_path = self.this_tree.id2path(file_id)
977
973
 
978
974
            lca_kinds = []
979
975
            lca_parent_ids = []
1004
1000
                        continue
1005
1001
                    content_changed = False
1006
1002
                elif other_ie.kind is None or other_ie.kind == 'file':
1007
 
                    def get_sha1(ie, tree):
1008
 
                        if ie.kind != 'file':
1009
 
                            return None
1010
 
                        return tree.get_file_sha1(tree.id2path(file_id), file_id)
1011
 
                    base_sha1 = get_sha1(base_ie, self.base_tree)
1012
 
                    lca_sha1s = [get_sha1(ie, tree) for ie, tree
1013
 
                                 in zip(lca_entries, self._lca_trees)]
1014
 
                    this_sha1 = get_sha1(this_ie, self.this_tree)
1015
 
                    other_sha1 = get_sha1(other_ie, self.other_tree)
 
1003
                    def get_sha1(tree, path):
 
1004
                        if path is None:
 
1005
                            return None
 
1006
                        try:
 
1007
                            return tree.get_file_sha1(path, file_id)
 
1008
                        except errors.NoSuchFile:
 
1009
                            return None
 
1010
                    base_sha1 = get_sha1(self.base_tree, base_path)
 
1011
                    lca_sha1s = [get_sha1(tree, lca_path) for tree, lca_path
 
1012
                                 in zip(self._lca_trees, lca_paths)]
 
1013
                    this_sha1 = get_sha1(self.this_tree, this_path)
 
1014
                    other_sha1 = get_sha1(self.other_tree, other_path)
1016
1015
                    sha1_winner = self._lca_multi_way(
1017
1016
                        (base_sha1, lca_sha1s), other_sha1, this_sha1,
1018
1017
                        allow_overriding_lca=False)
1027
1026
                    if sha1_winner == 'this':
1028
1027
                        content_changed = False
1029
1028
                elif other_ie.kind == 'symlink':
1030
 
                    def get_target(ie, tree):
 
1029
                    def get_target(ie, tree, path):
1031
1030
                        if ie.kind != 'symlink':
1032
1031
                            return None
1033
 
                        path = tree.id2path(file_id)
1034
1032
                        return tree.get_symlink_target(path, file_id)
1035
 
                    base_target = get_target(base_ie, self.base_tree)
1036
 
                    lca_targets = [get_target(ie, tree) for ie, tree
1037
 
                                   in zip(lca_entries, self._lca_trees)]
1038
 
                    this_target = get_target(this_ie, self.this_tree)
1039
 
                    other_target = get_target(other_ie, self.other_tree)
 
1033
                    base_target = get_target(base_ie, self.base_tree, base_path)
 
1034
                    lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
 
1035
                                   in zip(lca_entries, self._lca_trees, lca_paths)]
 
1036
                    this_target = get_target(this_ie, self.this_tree, this_path)
 
1037
                    other_target = get_target(other_ie, self.other_tree, other_path)
1040
1038
                    target_winner = self._lca_multi_way(
1041
1039
                        (base_target, lca_targets),
1042
1040
                        other_target, this_target)
1060
1058
 
1061
1059
            # If we have gotten this far, that means something has changed
1062
1060
            result.append((file_id, content_changed,
 
1061
                           ((base_path, lca_paths),
 
1062
                            other_path, this_path),
1063
1063
                           ((base_ie.parent_id, lca_parent_ids),
1064
1064
                            other_ie.parent_id, this_ie.parent_id),
1065
1065
                           ((base_ie.name, lca_names),
1070
1070
        return result
1071
1071
 
1072
1072
    def write_modified(self, results):
 
1073
        if not self.working_tree.supports_merge_modified():
 
1074
            return
1073
1075
        modified_hashes = {}
1074
1076
        for path in results.modified_paths:
1075
1077
            wt_relpath = self.working_tree.relpath(path)
1097
1099
        return entry.name
1098
1100
 
1099
1101
    @staticmethod
1100
 
    def contents_sha1(tree, file_id):
 
1102
    def contents_sha1(tree, path, file_id=None):
1101
1103
        """Determine the sha1 of the file contents (used as a key method)."""
1102
1104
        try:
1103
 
            path = tree.id2path(file_id)
1104
 
        except errors.NoSuchId:
 
1105
            return tree.get_file_sha1(path, file_id)
 
1106
        except errors.NoSuchFile:
1105
1107
            return None
1106
 
        return tree.get_file_sha1(path, file_id)
1107
1108
 
1108
1109
    @staticmethod
1109
 
    def executable(tree, file_id):
 
1110
    def executable(tree, path, file_id=None):
1110
1111
        """Determine the executability of a file-id (used as a key method)."""
1111
1112
        try:
1112
 
            path = tree.id2path(file_id)
1113
 
        except errors.NoSuchId:
 
1113
            if tree.kind(path, file_id) != "file":
 
1114
                return False
 
1115
        except errors.NoSuchFile:
1114
1116
            return None
1115
 
        if tree.kind(path, file_id) != "file":
1116
 
            return False
1117
1117
        return tree.is_executable(path, file_id)
1118
1118
 
1119
1119
    @staticmethod
1120
 
    def kind(tree, path, file_id):
 
1120
    def kind(tree, path, file_id=None):
1121
1121
        """Determine the kind of a file-id (used as a key method)."""
1122
1122
        try:
1123
 
            path = tree.id2path(file_id)
1124
 
        except errors.NoSuchId:
 
1123
            return tree.kind(path, file_id)
 
1124
        except errors.NoSuchFile:
1125
1125
            return None
1126
 
        return tree.kind(path, file_id)
1127
1126
 
1128
1127
    @staticmethod
1129
1128
    def _three_way(base, other, this):
1210
1209
            else:
1211
1210
                names.append(entry.name)
1212
1211
                parents.append(entry.parent_id)
1213
 
        return self._merge_names(file_id, parents, names,
 
1212
        return self._merge_names(file_id, paths, parents, names,
1214
1213
                                 resolver=self._three_way)
1215
1214
 
1216
 
    def _merge_names(self, file_id, parents, names, resolver):
 
1215
    def _merge_names(self, file_id, paths, parents, names, resolver):
1217
1216
        """Perform a merge on file_id names and parents"""
1218
1217
        base_name, other_name, this_name = names
1219
1218
        base_parent, other_parent, this_parent = parents
 
1219
        unused_base_path, other_path, this_path = paths
1220
1220
 
1221
1221
        name_winner = resolver(*names)
1222
1222
 
1236
1236
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1237
1237
                                        this_parent, this_name,
1238
1238
                                        other_parent, other_name))
1239
 
        if not self.other_tree.has_id(file_id):
 
1239
        if other_path is None:
1240
1240
            # it doesn't matter whether the result was 'other' or
1241
1241
            # 'conflict'-- if it has no file id, we leave it alone.
1242
1242
            return
1258
1258
            self.tt.adjust_path(name, parent_trans_id,
1259
1259
                                self.tt.trans_id_file_id(file_id))
1260
1260
 
1261
 
    def _do_merge_contents(self, file_id):
 
1261
    def _do_merge_contents(self, paths, file_id):
1262
1262
        """Performs a merge on file_id contents."""
1263
 
        def contents_pair(tree):
1264
 
            try:
1265
 
                path = tree.id2path(file_id)
1266
 
            except errors.NoSuchId:
 
1263
        def contents_pair(tree, path):
 
1264
            if path is None:
1267
1265
                return (None, None)
1268
1266
            try:
1269
1267
                kind = tree.kind(path, file_id)
1277
1275
                contents = None
1278
1276
            return kind, contents
1279
1277
 
 
1278
        base_path, other_path, this_path = paths
1280
1279
        # See SPOT run.  run, SPOT, run.
1281
1280
        # So we're not QUITE repeating ourselves; we do tricky things with
1282
1281
        # file kind...
1283
 
        base_pair = contents_pair(self.base_tree)
1284
 
        other_pair = contents_pair(self.other_tree)
 
1282
        other_pair = contents_pair(self.other_tree, other_path)
 
1283
        this_pair = contents_pair(self.this_tree, this_path)
1285
1284
        if self._lca_trees:
1286
 
            this_pair = contents_pair(self.this_tree)
1287
 
            lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
 
1285
            (base_path, lca_paths) = base_path
 
1286
            base_pair = contents_pair(self.base_tree, base_path)
 
1287
            lca_pairs = [contents_pair(tree, path)
 
1288
                         for tree, path in zip(self._lca_trees, lca_paths)]
1288
1289
            winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1289
1290
                                         this_pair, allow_overriding_lca=False)
1290
1291
        else:
 
1292
            base_pair = contents_pair(self.base_tree, base_path)
1291
1293
            if base_pair == other_pair:
1292
1294
                winner = 'this'
1293
1295
            else:
1294
1296
                # We delayed evaluating this_pair as long as we can to avoid
1295
1297
                # unnecessary sha1 calculation
1296
 
                this_pair = contents_pair(self.this_tree)
 
1298
                this_pair = contents_pair(self.this_tree, this_path)
1297
1299
                winner = self._three_way(base_pair, other_pair, this_pair)
1298
1300
        if winner == 'this':
1299
1301
            # No interesting changes introduced by OTHER
1301
1303
        # We have a hypothetical conflict, but if we have files, then we
1302
1304
        # can try to merge the content
1303
1305
        trans_id = self.tt.trans_id_file_id(file_id)
1304
 
        params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
 
1306
        params = MergeFileHookParams(
 
1307
            self, file_id, (base_path, other_path,
 
1308
            this_path), trans_id, this_pair[0],
1305
1309
            other_pair[0], winner)
1306
1310
        hooks = self.active_hooks
1307
1311
        hook_status = 'not_applicable'
1325
1329
            inhibit_content_conflict = False
1326
1330
            if params.this_kind is None: # file_id is not in THIS
1327
1331
                # Is the name used for a different file_id ?
1328
 
                dupe_path = self.other_tree.id2path(file_id)
1329
 
                this_id = self.this_tree.path2id(dupe_path)
1330
 
                if this_id is not None:
 
1332
                if self.this_tree.is_versioned(other_path):
1331
1333
                    # Two entries for the same path
1332
1334
                    keep_this = True
1333
1335
                    # versioning the merged file will trigger a duplicate
1335
1337
                    self.tt.version_file(file_id, trans_id)
1336
1338
                    transform.create_from_tree(
1337
1339
                        self.tt, trans_id, self.other_tree,
1338
 
                        self.other_tree.id2path(file_id), file_id=file_id,
 
1340
                        other_path, file_id=file_id,
1339
1341
                        filter_tree_path=self._get_filter_tree_path(file_id))
1340
1342
                    inhibit_content_conflict = True
1341
1343
            elif params.other_kind is None: # file_id is not in OTHER
1342
1344
                # Is the name used for a different file_id ?
1343
 
                dupe_path = self.this_tree.id2path(file_id)
1344
 
                other_id = self.other_tree.path2id(dupe_path)
1345
 
                if other_id is not None:
 
1345
                if self.other_tree.is_versioned(this_path):
1346
1346
                    # Two entries for the same path again, but here, the other
1347
1347
                    # entry will also be merged.  We simply inhibit the
1348
1348
                    # 'content' conflict creation because we know OTHER will
1356
1356
                    self.tt.unversion_file(trans_id)
1357
1357
                # This is a contents conflict, because none of the available
1358
1358
                # functions could merge it.
1359
 
                file_group = self._dump_conflicts(name, parent_id, file_id,
1360
 
                                                  set_version=True)
 
1359
                file_group = self._dump_conflicts(
 
1360
                        name, (base_path, other_path, this_path), parent_id,
 
1361
                        file_id, set_version=True)
1361
1362
                self._raw_conflicts.append(('contents conflict', file_group))
1362
1363
        elif hook_status == 'success':
1363
1364
            self.tt.create_file(lines, trans_id)
1368
1369
            self._raw_conflicts.append(('text conflict', trans_id))
1369
1370
            name = self.tt.final_name(trans_id)
1370
1371
            parent_id = self.tt.final_parent(trans_id)
1371
 
            self._dump_conflicts(name, parent_id, file_id)
 
1372
            self._dump_conflicts(
 
1373
                name, (base_path, other_path, this_path), parent_id, file_id)
1372
1374
        elif hook_status == 'delete':
1373
1375
            self.tt.unversion_file(trans_id)
1374
1376
            result = "deleted"
1378
1380
            pass
1379
1381
        else:
1380
1382
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1381
 
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1383
        if not this_path and result == "modified":
1382
1384
            self.tt.version_file(file_id, trans_id)
1383
1385
        if not keep_this:
1384
1386
            # The merge has been performed and produced a new content, so the
1390
1392
        """Replace this contents with other."""
1391
1393
        file_id = merge_hook_params.file_id
1392
1394
        trans_id = merge_hook_params.trans_id
1393
 
        if self.other_tree.has_id(file_id):
 
1395
        if merge_hook_params.other_path is not None:
1394
1396
            # OTHER changed the file
1395
1397
            transform.create_from_tree(
1396
1398
                self.tt, trans_id, self.other_tree,
1397
 
                self.other_tree.id2path(file_id), file_id=file_id,
 
1399
                merge_hook_params.other_path, file_id=file_id,
1398
1400
                filter_tree_path=self._get_filter_tree_path(file_id))
1399
1401
            return 'done', None
1400
 
        elif self.this_tree.has_id(file_id):
 
1402
        elif merge_hook_params.this_path is not None:
1401
1403
            # OTHER deleted the file
1402
1404
            return 'delete', None
1403
1405
        else:
1408
1410
    def merge_contents(self, merge_hook_params):
1409
1411
        """Fallback merge logic after user installed hooks."""
1410
1412
        # This function is used in merge hooks as the fallback instance.
1411
 
        # Perhaps making this function and the functions it calls be a 
 
1413
        # Perhaps making this function and the functions it calls be a
1412
1414
        # a separate class would be better.
1413
1415
        if merge_hook_params.winner == 'other':
1414
1416
            # OTHER is a straight winner, so replace this contents with other
1418
1420
            # BASE is a file, or both converted to files, so at least we
1419
1421
            # have agreement that output should be a file.
1420
1422
            try:
1421
 
                self.text_merge(merge_hook_params.file_id,
1422
 
                    merge_hook_params.trans_id)
 
1423
                self.text_merge(merge_hook_params.trans_id,
 
1424
                                merge_hook_params.paths, merge_hook_params.file_id)
1423
1425
            except errors.BinaryFile:
1424
1426
                return 'not_applicable', None
1425
1427
            return 'done', None
1426
1428
        else:
1427
1429
            return 'not_applicable', None
1428
1430
 
1429
 
    def get_lines(self, tree, file_id):
 
1431
    def get_lines(self, tree, path, file_id=None):
1430
1432
        """Return the lines in a file, or an empty list."""
 
1433
        if path is None:
 
1434
            return []
1431
1435
        try:
1432
 
            path = tree.id2path(file_id)
1433
 
        except errors.NoSuchId:
 
1436
            kind = tree.kind(path, file_id)
 
1437
        except errors.NoSuchFile:
1434
1438
            return []
1435
1439
        else:
 
1440
            if kind != 'file':
 
1441
                return []
1436
1442
            return tree.get_file_lines(path, file_id)
1437
1443
 
1438
 
    def text_merge(self, file_id, trans_id):
 
1444
    def text_merge(self, trans_id, paths, file_id):
1439
1445
        """Perform a three-way text merge on a file_id"""
1440
1446
        # it's possible that we got here with base as a different type.
1441
1447
        # if so, we just want two-way text conflicts.
1442
 
        try:
1443
 
            base_path = self.base_tree.id2path(file_id)
1444
 
        except errors.NoSuchId:
1445
 
            base_lines = []
1446
 
        else:
1447
 
            if self.base_tree.kind(base_path, file_id) == "file":
1448
 
                base_lines = self.get_lines(self.base_tree, file_id)
1449
 
            else:
1450
 
                base_lines = []
1451
 
        other_lines = self.get_lines(self.other_tree, file_id)
1452
 
        this_lines = self.get_lines(self.this_tree, file_id)
 
1448
        base_path, other_path, this_path = paths
 
1449
        base_lines = self.get_lines(self.base_tree, base_path, file_id)
 
1450
        other_lines = self.get_lines(self.other_tree, other_path, file_id)
 
1451
        this_lines = self.get_lines(self.this_tree, this_path, file_id)
1453
1452
        m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1454
1453
                           is_cherrypick=self.cherrypick)
1455
1454
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1478
1477
            self._raw_conflicts.append(('text conflict', trans_id))
1479
1478
            name = self.tt.final_name(trans_id)
1480
1479
            parent_id = self.tt.final_parent(trans_id)
1481
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1480
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1482
1481
                                              this_lines, base_lines,
1483
1482
                                              other_lines)
1484
1483
            file_group.append(trans_id)
1485
1484
 
1486
 
 
1487
1485
    def _get_filter_tree_path(self, file_id):
1488
1486
        if self.this_tree.supports_content_filtering():
1489
1487
            # We get the path from the working tree if it exists.
1497
1495
        # Skip the id2path lookup for older formats
1498
1496
        return None
1499
1497
 
1500
 
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
 
1498
    def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1501
1499
                        base_lines=None, other_lines=None, set_version=False,
1502
1500
                        no_base=False):
1503
1501
        """Emit conflict files.
1505
1503
        determined automatically.  If set_version is true, the .OTHER, .THIS
1506
1504
        or .BASE (in that order) will be created as versioned files.
1507
1505
        """
1508
 
        data = [('OTHER', self.other_tree, other_lines),
1509
 
                ('THIS', self.this_tree, this_lines)]
 
1506
        base_path, other_path, this_path = paths
 
1507
        data = [('OTHER', self.other_tree, other_path, other_lines),
 
1508
                ('THIS', self.this_tree, this_path, this_lines)]
1510
1509
        if not no_base:
1511
 
            data.append(('BASE', self.base_tree, base_lines))
 
1510
            data.append(('BASE', self.base_tree, base_path, base_lines))
1512
1511
 
1513
1512
        # We need to use the actual path in the working tree of the file here,
1514
1513
        # ignoring the conflict suffixes
1525
1524
 
1526
1525
        versioned = False
1527
1526
        file_group = []
1528
 
        for suffix, tree, lines in data:
1529
 
            try:
1530
 
                path = tree.id2path(file_id)
1531
 
            except errors.NoSuchId:
1532
 
                pass
1533
 
            else:
 
1527
        for suffix, tree, path, lines in data:
 
1528
            if path is not None:
1534
1529
                trans_id = self._conflict_file(
1535
1530
                        name, parent_id, path, tree, file_id, suffix, lines,
1536
1531
                        filter_tree_path)
1551
1546
                filter_tree_path=filter_tree_path)
1552
1547
        return trans_id
1553
1548
 
1554
 
    def merge_executable(self, file_id, file_status):
 
1549
    def merge_executable(self, paths, file_id, file_status):
1555
1550
        """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,
 
1551
        executable = [self.executable(t, p, file_id) for t, p in zip([self.base_tree,
 
1552
                      self.other_tree, self.this_tree], paths)]
 
1553
        self._merge_executable(paths, file_id, executable, file_status,
1559
1554
                               resolver=self._three_way)
1560
1555
 
1561
 
    def _merge_executable(self, file_id, executable, file_status,
 
1556
    def _merge_executable(self, paths, file_id, executable, file_status,
1562
1557
                          resolver):
1563
1558
        """Perform a merge on the execute bit."""
1564
1559
        base_executable, other_executable, this_executable = executable
 
1560
        base_path, other_path, this_path = paths
1565
1561
        if file_status == "deleted":
1566
1562
            return
1567
1563
        winner = resolver(*executable)
1568
1564
        if winner == "conflict":
1569
1565
        # There must be a None in here, if we have a conflict, but we
1570
1566
        # need executability since file status was not deleted.
1571
 
            if self.executable(self.other_tree, file_id) is None:
 
1567
            if self.executable(self.other_tree, other_path, file_id) is None:
1572
1568
                winner = "this"
1573
1569
            else:
1574
1570
                winner = "other"
1580
1576
        if winner == "this":
1581
1577
            executability = this_executable
1582
1578
        else:
1583
 
            if self.other_tree.has_id(file_id):
 
1579
            if other_path is not None:
1584
1580
                executability = other_executable
1585
 
            elif self.this_tree.has_id(file_id):
 
1581
            elif this_path is not None:
1586
1582
                executability = this_executable
1587
 
            elif self.base_tree_has_id(file_id):
 
1583
            elif base_path is not None:
1588
1584
                executability = base_executable
1589
1585
        if executability is not None:
1590
1586
            trans_id = self.tt.trans_id_file_id(file_id)
1667
1663
    supports_show_base = False
1668
1664
    supports_reverse_cherrypick = False
1669
1665
    history_based = True
 
1666
    requires_file_merge_plan = True
1670
1667
 
1671
1668
    def _generate_merge_plan(self, file_id, base):
1672
1669
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1697
1694
            base_lines = None
1698
1695
        return lines, base_lines
1699
1696
 
1700
 
    def text_merge(self, file_id, trans_id):
 
1697
    def text_merge(self, trans_id, paths, file_id):
1701
1698
        """Perform a (weave) text merge for a given file and file-id.
1702
1699
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1703
1700
        and a conflict will be noted.
1704
1701
        """
 
1702
        base_path, other_path, this_path = paths
1705
1703
        lines, base_lines = self._merged_lines(file_id)
1706
1704
        lines = list(lines)
1707
1705
        # Note we're checking whether the OUTPUT is binary in this case,
1713
1711
            self._raw_conflicts.append(('text conflict', trans_id))
1714
1712
            name = self.tt.final_name(trans_id)
1715
1713
            parent_id = self.tt.final_parent(trans_id)
1716
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1714
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1717
1715
                                              no_base=False,
1718
1716
                                              base_lines=base_lines)
1719
1717
            file_group.append(trans_id)
1721
1719
 
1722
1720
class LCAMerger(WeaveMerger):
1723
1721
 
 
1722
    requires_file_merge_plan = True
 
1723
 
1724
1724
    def _generate_merge_plan(self, file_id, base):
1725
1725
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1726
1726
                                                  base=base)
1728
1728
class Diff3Merger(Merge3Merger):
1729
1729
    """Three-way merger using external diff3 for text merging"""
1730
1730
 
1731
 
    def dump_file(self, temp_dir, name, tree, file_id):
 
1731
    requires_file_merge_plan = False
 
1732
 
 
1733
    def dump_file(self, temp_dir, name, tree, path, file_id=None):
1732
1734
        out_path = osutils.pathjoin(temp_dir, name)
1733
 
        out_file = open(out_path, "wb")
1734
 
        try:
1735
 
            in_file = tree.get_file(tree.id2path(file_id), file_id)
 
1735
        with open(out_path, "wb") as out_file:
 
1736
            in_file = tree.get_file(path, file_id=None)
1736
1737
            for line in in_file:
1737
1738
                out_file.write(line)
1738
 
        finally:
1739
 
            out_file.close()
1740
1739
        return out_path
1741
1740
 
1742
 
    def text_merge(self, file_id, trans_id):
 
1741
    def text_merge(self, trans_id, paths, file_id):
1743
1742
        """Perform a diff3 merge using a specified file-id and trans-id.
1744
1743
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1745
1744
        will be dumped, and a will be conflict noted.
1746
1745
        """
1747
1746
        import breezy.patch
 
1747
        base_path, other_path, this_path = paths
1748
1748
        temp_dir = osutils.mkdtemp(prefix="bzr-")
1749
1749
        try:
1750
1750
            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)
 
1751
            this = self.dump_file(temp_dir, "this", self.this_tree, this_path, file_id)
 
1752
            base = self.dump_file(temp_dir, "base", self.base_tree, base_path, file_id)
 
1753
            other = self.dump_file(temp_dir, "other", self.other_tree, other_path, file_id)
1754
1754
            status = breezy.patch.diff3(new_file, this, base, other)
1755
1755
            if status not in (0, 1):
1756
1756
                raise errors.BzrError("Unhandled diff3 exit code")
1757
 
            f = open(new_file, 'rb')
1758
 
            try:
 
1757
            with open(new_file, 'rb') as f:
1759
1758
                self.tt.create_file(f, trans_id)
1760
 
            finally:
1761
 
                f.close()
1762
1759
            if status == 1:
1763
1760
                name = self.tt.final_name(trans_id)
1764
1761
                parent_id = self.tt.final_parent(trans_id)
1765
 
                self._dump_conflicts(name, parent_id, file_id)
 
1762
                self._dump_conflicts(name, paths, parent_id, file_id)
1766
1763
                self._raw_conflicts.append(('text conflict', trans_id))
1767
1764
        finally:
1768
1765
            osutils.rmtree(temp_dir)
1819
1816
        self.merge_type = Merge3Merger
1820
1817
        self.show_base = False
1821
1818
        self.reprocess = False
1822
 
        self.interesting_ids = None
 
1819
        self.interesting_files = None
1823
1820
        self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1824
1821
              target_subdir=self._target_subdir,
1825
1822
              source_subpath=self._source_subpath)
1837
1834
 
1838
1835
class _MergeTypeParameterizer(object):
1839
1836
    """Wrap a merge-type class to provide extra parameters.
1840
 
    
 
1837
 
1841
1838
    This is hack used by MergeIntoMerger to pass some extra parameters to its
1842
1839
    merge_type.  Merger.do_merge() sets up its own set of parameters to pass to
1843
1840
    the 'merge_type' member.  It is difficult override do_merge without
1878
1875
        super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1879
1876
 
1880
1877
    def _compute_transform(self):
1881
 
        child_pb = ui.ui_factory.nested_progress_bar()
1882
 
        try:
 
1878
        with ui.ui_factory.nested_progress_bar() as child_pb:
1883
1879
            entries = self._entries_to_incorporate()
1884
1880
            entries = list(entries)
1885
 
            for num, (entry, parent_id, path) in enumerate(entries):
 
1881
            for num, (entry, parent_id, relpath) in enumerate(entries):
1886
1882
                child_pb.update(gettext('Preparing file merge'), num, len(entries))
1887
1883
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
 
1884
                path = osutils.pathjoin(self._source_subpath, relpath)
1888
1885
                trans_id = transform.new_by_entry(path, self.tt, entry,
1889
1886
                    parent_trans_id, self.other_tree)
1890
 
        finally:
1891
 
            child_pb.finished()
1892
1887
        self._finish_computing_transform()
1893
1888
 
1894
1889
    def _entries_to_incorporate(self):
1933
1928
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1934
1929
                backup_files=False,
1935
1930
                merge_type=Merge3Merger,
1936
 
                interesting_ids=None,
1937
1931
                show_base=False,
1938
1932
                reprocess=False,
1939
1933
                other_rev_id=None,
1954
1948
                    change_reporter=change_reporter)
1955
1949
    merger.backup_files = backup_files
1956
1950
    merger.merge_type = merge_type
1957
 
    merger.interesting_ids = interesting_ids
1958
1951
    merger.ignore_zero = ignore_zero
1959
 
    if interesting_files:
1960
 
        if interesting_ids:
1961
 
            raise ValueError('Only supply interesting_ids'
1962
 
                             ' or interesting_files')
1963
 
        merger.interesting_files = interesting_files
 
1952
    merger.interesting_files = interesting_files
1964
1953
    merger.show_base = show_base
1965
1954
    merger.reprocess = reprocess
1966
1955
    merger.other_rev_id = other_rev_id