/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-24 17:48:04 UTC
  • mfrom: (6921 work)
  • mto: This revision was merged to the branch mainline in revision 6923.
  • Revision ID: jelmer@jelmer.uk-20180324174804-xf22o05byoj12x1q
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,
 
1359
                file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1360
1360
                                                  set_version=True)
1361
1361
                self._raw_conflicts.append(('contents conflict', file_group))
1362
1362
        elif hook_status == 'success':
1368
1368
            self._raw_conflicts.append(('text conflict', trans_id))
1369
1369
            name = self.tt.final_name(trans_id)
1370
1370
            parent_id = self.tt.final_parent(trans_id)
1371
 
            self._dump_conflicts(name, parent_id, file_id)
 
1371
            self._dump_conflicts(name, paths, parent_id, file_id)
1372
1372
        elif hook_status == 'delete':
1373
1373
            self.tt.unversion_file(trans_id)
1374
1374
            result = "deleted"
1378
1378
            pass
1379
1379
        else:
1380
1380
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1381
 
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1381
        if not this_path and result == "modified":
1382
1382
            self.tt.version_file(file_id, trans_id)
1383
1383
        if not keep_this:
1384
1384
            # The merge has been performed and produced a new content, so the
1390
1390
        """Replace this contents with other."""
1391
1391
        file_id = merge_hook_params.file_id
1392
1392
        trans_id = merge_hook_params.trans_id
1393
 
        if self.other_tree.has_id(file_id):
 
1393
        if merge_hook_params.other_path is not None:
1394
1394
            # OTHER changed the file
1395
1395
            transform.create_from_tree(
1396
1396
                self.tt, trans_id, self.other_tree,
1397
 
                self.other_tree.id2path(file_id), file_id=file_id,
 
1397
                merge_hook_params.other_path, file_id=file_id,
1398
1398
                filter_tree_path=self._get_filter_tree_path(file_id))
1399
1399
            return 'done', None
1400
 
        elif self.this_tree.has_id(file_id):
 
1400
        elif merge_hook_params.this_path is not None:
1401
1401
            # OTHER deleted the file
1402
1402
            return 'delete', None
1403
1403
        else:
1408
1408
    def merge_contents(self, merge_hook_params):
1409
1409
        """Fallback merge logic after user installed hooks."""
1410
1410
        # This function is used in merge hooks as the fallback instance.
1411
 
        # Perhaps making this function and the functions it calls be a 
 
1411
        # Perhaps making this function and the functions it calls be a
1412
1412
        # a separate class would be better.
1413
1413
        if merge_hook_params.winner == 'other':
1414
1414
            # OTHER is a straight winner, so replace this contents with other
1418
1418
            # BASE is a file, or both converted to files, so at least we
1419
1419
            # have agreement that output should be a file.
1420
1420
            try:
1421
 
                self.text_merge(merge_hook_params.file_id,
1422
 
                    merge_hook_params.trans_id)
 
1421
                self.text_merge(merge_hook_params.trans_id,
 
1422
                                merge_hook_params.paths, merge_hook_params.file_id)
1423
1423
            except errors.BinaryFile:
1424
1424
                return 'not_applicable', None
1425
1425
            return 'done', None
1426
1426
        else:
1427
1427
            return 'not_applicable', None
1428
1428
 
1429
 
    def get_lines(self, tree, file_id):
 
1429
    def get_lines(self, tree, path, file_id=None):
1430
1430
        """Return the lines in a file, or an empty list."""
 
1431
        if path is None:
 
1432
            return []
1431
1433
        try:
1432
 
            path = tree.id2path(file_id)
1433
 
        except errors.NoSuchId:
 
1434
            kind = tree.kind(path, file_id)
 
1435
        except errors.NoSuchFile:
1434
1436
            return []
1435
1437
        else:
 
1438
            if kind != 'file':
 
1439
                return []
1436
1440
            return tree.get_file_lines(path, file_id)
1437
1441
 
1438
 
    def text_merge(self, file_id, trans_id):
 
1442
    def text_merge(self, trans_id, paths, file_id):
1439
1443
        """Perform a three-way text merge on a file_id"""
1440
1444
        # it's possible that we got here with base as a different type.
1441
1445
        # if so, we just want two-way text conflicts.
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)
 
1446
        base_path, other_path, this_path = paths
 
1447
        base_lines = self.get_lines(self.base_tree, base_path, file_id)
 
1448
        other_lines = self.get_lines(self.other_tree, other_path, file_id)
 
1449
        this_lines = self.get_lines(self.this_tree, this_path, file_id)
1453
1450
        m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1454
1451
                           is_cherrypick=self.cherrypick)
1455
1452
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1478
1475
            self._raw_conflicts.append(('text conflict', trans_id))
1479
1476
            name = self.tt.final_name(trans_id)
1480
1477
            parent_id = self.tt.final_parent(trans_id)
1481
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1478
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1482
1479
                                              this_lines, base_lines,
1483
1480
                                              other_lines)
1484
1481
            file_group.append(trans_id)
1485
1482
 
1486
 
 
1487
1483
    def _get_filter_tree_path(self, file_id):
1488
1484
        if self.this_tree.supports_content_filtering():
1489
1485
            # We get the path from the working tree if it exists.
1497
1493
        # Skip the id2path lookup for older formats
1498
1494
        return None
1499
1495
 
1500
 
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
 
1496
    def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1501
1497
                        base_lines=None, other_lines=None, set_version=False,
1502
1498
                        no_base=False):
1503
1499
        """Emit conflict files.
1505
1501
        determined automatically.  If set_version is true, the .OTHER, .THIS
1506
1502
        or .BASE (in that order) will be created as versioned files.
1507
1503
        """
1508
 
        data = [('OTHER', self.other_tree, other_lines),
1509
 
                ('THIS', self.this_tree, this_lines)]
 
1504
        base_path, other_path, this_path = paths
 
1505
        data = [('OTHER', self.other_tree, other_path, other_lines),
 
1506
                ('THIS', self.this_tree, this_path, this_lines)]
1510
1507
        if not no_base:
1511
 
            data.append(('BASE', self.base_tree, base_lines))
 
1508
            data.append(('BASE', self.base_tree, base_path, base_lines))
1512
1509
 
1513
1510
        # We need to use the actual path in the working tree of the file here,
1514
1511
        # ignoring the conflict suffixes
1525
1522
 
1526
1523
        versioned = False
1527
1524
        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:
 
1525
        for suffix, tree, path, lines in data:
 
1526
            if path is not None:
1534
1527
                trans_id = self._conflict_file(
1535
1528
                        name, parent_id, path, tree, file_id, suffix, lines,
1536
1529
                        filter_tree_path)
1551
1544
                filter_tree_path=filter_tree_path)
1552
1545
        return trans_id
1553
1546
 
1554
 
    def merge_executable(self, file_id, file_status):
 
1547
    def merge_executable(self, paths, file_id, file_status):
1555
1548
        """Perform a merge on the execute bit."""
1556
 
        executable = [self.executable(t, file_id) for t in (self.base_tree,
1557
 
                      self.other_tree, self.this_tree)]
1558
 
        self._merge_executable(file_id, executable, file_status,
 
1549
        executable = [self.executable(t, p, file_id) for t, p in zip([self.base_tree,
 
1550
                      self.other_tree, self.this_tree], paths)]
 
1551
        self._merge_executable(paths, file_id, executable, file_status,
1559
1552
                               resolver=self._three_way)
1560
1553
 
1561
 
    def _merge_executable(self, file_id, executable, file_status,
 
1554
    def _merge_executable(self, paths, file_id, executable, file_status,
1562
1555
                          resolver):
1563
1556
        """Perform a merge on the execute bit."""
1564
1557
        base_executable, other_executable, this_executable = executable
 
1558
        base_path, other_path, this_path = paths
1565
1559
        if file_status == "deleted":
1566
1560
            return
1567
1561
        winner = resolver(*executable)
1568
1562
        if winner == "conflict":
1569
1563
        # There must be a None in here, if we have a conflict, but we
1570
1564
        # need executability since file status was not deleted.
1571
 
            if self.executable(self.other_tree, file_id) is None:
 
1565
            if self.executable(self.other_tree, other_path, file_id) is None:
1572
1566
                winner = "this"
1573
1567
            else:
1574
1568
                winner = "other"
1580
1574
        if winner == "this":
1581
1575
            executability = this_executable
1582
1576
        else:
1583
 
            if self.other_tree.has_id(file_id):
 
1577
            if other_path is not None:
1584
1578
                executability = other_executable
1585
 
            elif self.this_tree.has_id(file_id):
 
1579
            elif this_path is not None:
1586
1580
                executability = this_executable
1587
 
            elif self.base_tree_has_id(file_id):
 
1581
            elif base_path is not None:
1588
1582
                executability = base_executable
1589
1583
        if executability is not None:
1590
1584
            trans_id = self.tt.trans_id_file_id(file_id)
1667
1661
    supports_show_base = False
1668
1662
    supports_reverse_cherrypick = False
1669
1663
    history_based = True
 
1664
    requires_file_merge_plan = True
1670
1665
 
1671
1666
    def _generate_merge_plan(self, file_id, base):
1672
1667
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1697
1692
            base_lines = None
1698
1693
        return lines, base_lines
1699
1694
 
1700
 
    def text_merge(self, file_id, trans_id):
 
1695
    def text_merge(self, trans_id, paths, file_id):
1701
1696
        """Perform a (weave) text merge for a given file and file-id.
1702
1697
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1703
1698
        and a conflict will be noted.
1704
1699
        """
 
1700
        base_path, other_path, this_path = paths
1705
1701
        lines, base_lines = self._merged_lines(file_id)
1706
1702
        lines = list(lines)
1707
1703
        # Note we're checking whether the OUTPUT is binary in this case,
1713
1709
            self._raw_conflicts.append(('text conflict', trans_id))
1714
1710
            name = self.tt.final_name(trans_id)
1715
1711
            parent_id = self.tt.final_parent(trans_id)
1716
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1712
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1717
1713
                                              no_base=False,
1718
1714
                                              base_lines=base_lines)
1719
1715
            file_group.append(trans_id)
1721
1717
 
1722
1718
class LCAMerger(WeaveMerger):
1723
1719
 
 
1720
    requires_file_merge_plan = True
 
1721
 
1724
1722
    def _generate_merge_plan(self, file_id, base):
1725
1723
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1726
1724
                                                  base=base)
1728
1726
class Diff3Merger(Merge3Merger):
1729
1727
    """Three-way merger using external diff3 for text merging"""
1730
1728
 
1731
 
    def dump_file(self, temp_dir, name, tree, file_id):
 
1729
    requires_file_merge_plan = False
 
1730
 
 
1731
    def dump_file(self, temp_dir, name, tree, path, file_id=None):
1732
1732
        out_path = osutils.pathjoin(temp_dir, name)
1733
 
        out_file = open(out_path, "wb")
1734
 
        try:
1735
 
            in_file = tree.get_file(tree.id2path(file_id), file_id)
 
1733
        with open(out_path, "wb") as out_file:
 
1734
            in_file = tree.get_file(path, file_id=None)
1736
1735
            for line in in_file:
1737
1736
                out_file.write(line)
1738
 
        finally:
1739
 
            out_file.close()
1740
1737
        return out_path
1741
1738
 
1742
 
    def text_merge(self, file_id, trans_id):
 
1739
    def text_merge(self, trans_id, paths, file_id):
1743
1740
        """Perform a diff3 merge using a specified file-id and trans-id.
1744
1741
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1745
1742
        will be dumped, and a will be conflict noted.
1746
1743
        """
1747
1744
        import breezy.patch
 
1745
        base_path, other_path, this_path = paths
1748
1746
        temp_dir = osutils.mkdtemp(prefix="bzr-")
1749
1747
        try:
1750
1748
            new_file = osutils.pathjoin(temp_dir, "new")
1751
 
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1752
 
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1753
 
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
 
1749
            this = self.dump_file(temp_dir, "this", self.this_tree, this_path, file_id)
 
1750
            base = self.dump_file(temp_dir, "base", self.base_tree, base_path, file_id)
 
1751
            other = self.dump_file(temp_dir, "other", self.other_tree, other_path, file_id)
1754
1752
            status = breezy.patch.diff3(new_file, this, base, other)
1755
1753
            if status not in (0, 1):
1756
1754
                raise errors.BzrError("Unhandled diff3 exit code")
1757
 
            f = open(new_file, 'rb')
1758
 
            try:
 
1755
            with open(new_file, 'rb') as f:
1759
1756
                self.tt.create_file(f, trans_id)
1760
 
            finally:
1761
 
                f.close()
1762
1757
            if status == 1:
1763
1758
                name = self.tt.final_name(trans_id)
1764
1759
                parent_id = self.tt.final_parent(trans_id)
1765
 
                self._dump_conflicts(name, parent_id, file_id)
 
1760
                self._dump_conflicts(name, paths, parent_id, file_id)
1766
1761
                self._raw_conflicts.append(('text conflict', trans_id))
1767
1762
        finally:
1768
1763
            osutils.rmtree(temp_dir)
1819
1814
        self.merge_type = Merge3Merger
1820
1815
        self.show_base = False
1821
1816
        self.reprocess = False
1822
 
        self.interesting_ids = None
 
1817
        self.interesting_files = None
1823
1818
        self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1824
1819
              target_subdir=self._target_subdir,
1825
1820
              source_subpath=self._source_subpath)
1837
1832
 
1838
1833
class _MergeTypeParameterizer(object):
1839
1834
    """Wrap a merge-type class to provide extra parameters.
1840
 
    
 
1835
 
1841
1836
    This is hack used by MergeIntoMerger to pass some extra parameters to its
1842
1837
    merge_type.  Merger.do_merge() sets up its own set of parameters to pass to
1843
1838
    the 'merge_type' member.  It is difficult override do_merge without
1878
1873
        super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1879
1874
 
1880
1875
    def _compute_transform(self):
1881
 
        child_pb = ui.ui_factory.nested_progress_bar()
1882
 
        try:
 
1876
        with ui.ui_factory.nested_progress_bar() as child_pb:
1883
1877
            entries = self._entries_to_incorporate()
1884
1878
            entries = list(entries)
1885
 
            for num, (entry, parent_id, path) in enumerate(entries):
 
1879
            for num, (entry, parent_id, relpath) in enumerate(entries):
1886
1880
                child_pb.update(gettext('Preparing file merge'), num, len(entries))
1887
1881
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
 
1882
                path = osutils.pathjoin(self._source_subpath, relpath)
1888
1883
                trans_id = transform.new_by_entry(path, self.tt, entry,
1889
1884
                    parent_trans_id, self.other_tree)
1890
 
        finally:
1891
 
            child_pb.finished()
1892
1885
        self._finish_computing_transform()
1893
1886
 
1894
1887
    def _entries_to_incorporate(self):
1933
1926
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1934
1927
                backup_files=False,
1935
1928
                merge_type=Merge3Merger,
1936
 
                interesting_ids=None,
1937
1929
                show_base=False,
1938
1930
                reprocess=False,
1939
1931
                other_rev_id=None,
1954
1946
                    change_reporter=change_reporter)
1955
1947
    merger.backup_files = backup_files
1956
1948
    merger.merge_type = merge_type
1957
 
    merger.interesting_ids = interesting_ids
1958
1949
    merger.ignore_zero = ignore_zero
1959
 
    if interesting_files:
1960
 
        if interesting_ids:
1961
 
            raise ValueError('Only supply interesting_ids'
1962
 
                             ' or interesting_files')
1963
 
        merger.interesting_files = interesting_files
 
1950
    merger.interesting_files = interesting_files
1964
1951
    merger.show_base = show_base
1965
1952
    merger.reprocess = reprocess
1966
1953
    merger.other_rev_id = other_rev_id