~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Jelmer Vernooij
  • Date: 2010-01-31 12:10:40 UTC
  • mto: This revision was merged to the branch mainline in revision 4993.
  • Revision ID: jelmer@samba.org-20100131121040-64t9ykvo2oz4pknx
rename Repository.deserialise_inventory to Repository._deserialise_inventory.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
import warnings
18
17
 
19
18
from bzrlib import (
20
19
    branch as _mod_branch,
27
26
    merge3,
28
27
    osutils,
29
28
    patiencediff,
 
29
    progress,
30
30
    revision as _mod_revision,
31
31
    textfile,
32
32
    trace,
36
36
    ui,
37
37
    versionedfile
38
38
    )
39
 
from bzrlib.cleanup import OperationWithCleanups
40
39
from bzrlib.symbol_versioning import (
41
40
    deprecated_in,
42
41
    deprecated_method,
46
45
 
47
46
def transform_tree(from_tree, to_tree, interesting_ids=None):
48
47
    from_tree.lock_tree_write()
49
 
    operation = OperationWithCleanups(merge_inner)
50
 
    operation.add_cleanup(from_tree.unlock)
51
 
    operation.run_simple(from_tree.branch, to_tree, from_tree,
52
 
        ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
 
48
    try:
 
49
        merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
 
50
                    interesting_ids=interesting_ids, this_tree=from_tree)
 
51
    finally:
 
52
        from_tree.unlock()
53
53
 
54
54
 
55
55
class MergeHooks(hooks.Hooks):
57
57
    def __init__(self):
58
58
        hooks.Hooks.__init__(self)
59
59
        self.create_hook(hooks.HookPoint('merge_file_content',
60
 
            "Called with a bzrlib.merge.Merger object to create a per file "
61
 
            "merge object when starting a merge. "
62
 
            "Should return either None or a subclass of "
63
 
            "``bzrlib.merge.AbstractPerFileMerger``. "
64
 
            "Such objects will then be called per file "
65
 
            "that needs to be merged (including when one "
66
 
            "side has deleted the file and the other has changed it). "
67
 
            "See the AbstractPerFileMerger API docs for details on how it is "
68
 
            "used by merge.",
 
60
            "Called when file content needs to be merged (including when one "
 
61
            "side has deleted the file and the other has changed it)."
 
62
            "merge_file_content is called with a "
 
63
            "bzrlib.merge.MergeHookParams. The function should return a tuple "
 
64
            "of (status, lines), where status is one of 'not_applicable', "
 
65
            "'success', 'conflicted', or 'delete'.  If status is success or "
 
66
            "conflicted, then lines should be an iterable of strings of the "
 
67
            "new file contents.",
69
68
            (2, 1), None))
70
69
 
71
70
 
72
 
class AbstractPerFileMerger(object):
73
 
    """PerFileMerger objects are used by plugins extending merge for bzrlib.
74
 
 
75
 
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
76
 
    
77
 
    :ivar merger: The Merge3Merger performing the merge.
78
 
    """
79
 
 
80
 
    def __init__(self, merger):
81
 
        """Create a PerFileMerger for use with merger."""
82
 
        self.merger = merger
83
 
 
84
 
    def merge_contents(self, merge_params):
85
 
        """Attempt to merge the contents of a single file.
86
 
        
87
 
        :param merge_params: A bzrlib.merge.MergeHookParams
88
 
        :return : A tuple of (status, chunks), where status is one of
89
 
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
90
 
            is 'success' or 'conflicted', then chunks should be an iterable of
91
 
            strings for the new file contents.
92
 
        """
93
 
        return ('not applicable', None)
94
 
 
95
 
 
96
 
class PerFileMerger(AbstractPerFileMerger):
97
 
    """Merge individual files when self.file_matches returns True.
98
 
 
99
 
    This class is intended to be subclassed.  The file_matches and
100
 
    merge_matching methods should be overridden with concrete implementations.
101
 
    """
102
 
 
103
 
    def file_matches(self, params):
104
 
        """Return True if merge_matching should be called on this file.
105
 
 
106
 
        Only called with merges of plain files with no clear winner.
107
 
 
108
 
        Subclasses must override this.
109
 
        """
110
 
        raise NotImplementedError(self.file_matches)
111
 
 
112
 
    def get_filename(self, params, tree):
113
 
        """Lookup the filename (i.e. basename, not path), given a Tree (e.g.
114
 
        self.merger.this_tree) and a MergeHookParams.
115
 
        """
116
 
        return osutils.basename(tree.id2path(params.file_id))
117
 
 
118
 
    def get_filepath(self, params, tree):
119
 
        """Calculate the path to the file in a tree.
120
 
 
121
 
        :param params: A MergeHookParams describing the file to merge
122
 
        :param tree: a Tree, e.g. self.merger.this_tree.
123
 
        """
124
 
        return tree.id2path(params.file_id)
125
 
 
126
 
    def merge_contents(self, params):
127
 
        """Merge the contents of a single file."""
128
 
        # Check whether this custom merge logic should be used.
129
 
        if (
130
 
            # OTHER is a straight winner, rely on default merge.
131
 
            params.winner == 'other' or
132
 
            # THIS and OTHER aren't both files.
133
 
            not params.is_file_merge() or
134
 
            # The filename doesn't match *.xml
135
 
            not self.file_matches(params)):
136
 
            return 'not_applicable', None
137
 
        return self.merge_matching(params)
138
 
 
139
 
    def merge_matching(self, params):
140
 
        """Merge the contents of a single file that has matched the criteria
141
 
        in PerFileMerger.merge_contents (is a conflict, is a file,
142
 
        self.file_matches is True).
143
 
 
144
 
        Subclasses must override this.
145
 
        """
146
 
        raise NotImplementedError(self.merge_matching)
147
 
 
148
 
 
149
 
class ConfigurableFileMerger(PerFileMerger):
150
 
    """Merge individual files when configured via a .conf file.
151
 
 
152
 
    This is a base class for concrete custom file merging logic. Concrete
153
 
    classes should implement ``merge_text``.
154
 
 
155
 
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
156
 
    
157
 
    :ivar affected_files: The configured file paths to merge.
158
 
 
159
 
    :cvar name_prefix: The prefix to use when looking up configuration
160
 
        details. <name_prefix>_merge_files describes the files targeted by the
161
 
        hook for example.
162
 
        
163
 
    :cvar default_files: The default file paths to merge when no configuration
164
 
        is present.
165
 
    """
166
 
 
167
 
    name_prefix = None
168
 
    default_files = None
169
 
 
170
 
    def __init__(self, merger):
171
 
        super(ConfigurableFileMerger, self).__init__(merger)
172
 
        self.affected_files = None
173
 
        self.default_files = self.__class__.default_files or []
174
 
        self.name_prefix = self.__class__.name_prefix
175
 
        if self.name_prefix is None:
176
 
            raise ValueError("name_prefix must be set.")
177
 
 
178
 
    def file_matches(self, params):
179
 
        """Check whether the file should call the merge hook.
180
 
 
181
 
        <name_prefix>_merge_files configuration variable is a list of files
182
 
        that should use the hook.
183
 
        """
184
 
        affected_files = self.affected_files
185
 
        if affected_files is None:
186
 
            config = self.merger.this_branch.get_config()
187
 
            # Until bzr provides a better policy for caching the config, we
188
 
            # just add the part we're interested in to the params to avoid
189
 
            # reading the config files repeatedly (bazaar.conf, location.conf,
190
 
            # branch.conf).
191
 
            config_key = self.name_prefix + '_merge_files'
192
 
            affected_files = config.get_user_option_as_list(config_key)
193
 
            if affected_files is None:
194
 
                # If nothing was specified in the config, use the default.
195
 
                affected_files = self.default_files
196
 
            self.affected_files = affected_files
197
 
        if affected_files:
198
 
            filepath = self.get_filepath(params, self.merger.this_tree)
199
 
            if filepath in affected_files:
200
 
                return True
201
 
        return False
202
 
 
203
 
    def merge_matching(self, params):
204
 
        return self.merge_text(params)
205
 
 
206
 
    def merge_text(self, params):
207
 
        """Merge the byte contents of a single file.
208
 
 
209
 
        This is called after checking that the merge should be performed in
210
 
        merge_contents, and it should behave as per
211
 
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
212
 
        """
213
 
        raise NotImplementedError(self.merge_text)
214
 
 
215
 
 
216
71
class MergeHookParams(object):
217
72
    """Object holding parameters passed to merge_file_content hooks.
218
73
 
219
 
    There are some fields hooks can access:
 
74
    There are 3 fields hooks can access:
220
75
 
 
76
    :ivar merger: the Merger object
221
77
    :ivar file_id: the file ID of the file being merged
222
78
    :ivar trans_id: the transform ID for the merge of this file
223
79
    :ivar this_kind: kind of file_id in 'this' tree
227
83
 
228
84
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
229
85
            winner):
230
 
        self._merger = merger
 
86
        self.merger = merger
231
87
        self.file_id = file_id
232
88
        self.trans_id = trans_id
233
89
        self.this_kind = this_kind
241
97
    @decorators.cachedproperty
242
98
    def base_lines(self):
243
99
        """The lines of the 'base' version of the file."""
244
 
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
100
        return self.merger.get_lines(self.merger.base_tree, self.file_id)
245
101
 
246
102
    @decorators.cachedproperty
247
103
    def this_lines(self):
248
104
        """The lines of the 'this' version of the file."""
249
 
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
105
        return self.merger.get_lines(self.merger.this_tree, self.file_id)
250
106
 
251
107
    @decorators.cachedproperty
252
108
    def other_lines(self):
253
109
        """The lines of the 'other' version of the file."""
254
 
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
110
        return self.merger.get_lines(self.merger.other_tree, self.file_id)
255
111
 
256
112
 
257
113
class Merger(object):
278
134
        self.interesting_files = None
279
135
        self.show_base = False
280
136
        self.reprocess = False
281
 
        if pb is not None:
282
 
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
 
137
        if pb is None:
 
138
            pb = progress.DummyProgress()
 
139
        self._pb = pb
283
140
        self.pp = None
284
141
        self.recurse = recurse
285
142
        self.change_reporter = change_reporter
495
352
    def _add_parent(self):
496
353
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
497
354
        new_parent_trees = []
498
 
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
499
355
        for revision_id in new_parents:
500
356
            try:
501
357
                tree = self.revision_tree(revision_id)
503
359
                tree = None
504
360
            else:
505
361
                tree.lock_read()
506
 
                operation.add_cleanup(tree.unlock)
507
362
            new_parent_trees.append((revision_id, tree))
508
 
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
 
363
        try:
 
364
            self.this_tree.set_parent_trees(new_parent_trees,
 
365
                                            allow_leftmost_as_ghost=True)
 
366
        finally:
 
367
            for _revision_id, tree in new_parent_trees:
 
368
                if tree is not None:
 
369
                    tree.unlock()
509
370
 
510
371
    def set_other(self, other_revision, possible_transports=None):
511
372
        """Set the revision and tree to merge from.
634
495
                  'other_tree': self.other_tree,
635
496
                  'interesting_ids': self.interesting_ids,
636
497
                  'interesting_files': self.interesting_files,
637
 
                  'this_branch': self.this_branch,
 
498
                  'pp': self.pp, 'this_branch': self.this_branch,
638
499
                  'do_merge': False}
639
500
        if self.merge_type.requires_base:
640
501
            kwargs['base_tree'] = self.base_tree
658
519
        if self._is_criss_cross and getattr(self.merge_type,
659
520
                                            'supports_lca_trees', False):
660
521
            kwargs['lca_trees'] = self._lca_trees
661
 
        return self.merge_type(pb=None,
 
522
        return self.merge_type(pb=self._pb,
662
523
                               change_reporter=self.change_reporter,
663
524
                               **kwargs)
664
525
 
665
 
    def _do_merge_to(self):
666
 
        merge = self.make_merger()
 
526
    def _do_merge_to(self, merge):
667
527
        if self.other_branch is not None:
668
528
            self.other_branch.update_references(self.this_branch)
669
529
        merge.do_merge()
683
543
                    sub_tree.branch.repository.revision_tree(base_revision)
684
544
                sub_merge.base_rev_id = base_revision
685
545
                sub_merge.do_merge()
686
 
        return merge
687
546
 
688
547
    def do_merge(self):
689
 
        operation = OperationWithCleanups(self._do_merge_to)
690
548
        self.this_tree.lock_tree_write()
691
 
        operation.add_cleanup(self.this_tree.unlock)
692
 
        if self.base_tree is not None:
693
 
            self.base_tree.lock_read()
694
 
            operation.add_cleanup(self.base_tree.unlock)
695
 
        if self.other_tree is not None:
696
 
            self.other_tree.lock_read()
697
 
            operation.add_cleanup(self.other_tree.unlock)
698
 
        merge = operation.run_simple()
 
549
        try:
 
550
            if self.base_tree is not None:
 
551
                self.base_tree.lock_read()
 
552
            try:
 
553
                if self.other_tree is not None:
 
554
                    self.other_tree.lock_read()
 
555
                try:
 
556
                    merge = self.make_merger()
 
557
                    self._do_merge_to(merge)
 
558
                finally:
 
559
                    if self.other_tree is not None:
 
560
                        self.other_tree.unlock()
 
561
            finally:
 
562
                if self.base_tree is not None:
 
563
                    self.base_tree.unlock()
 
564
        finally:
 
565
            self.this_tree.unlock()
699
566
        if len(merge.cooked_conflicts) == 0:
700
567
            if not self.ignore_zero and not trace.is_quiet():
701
568
                trace.note("All changes applied successfully.")
736
603
 
737
604
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
738
605
                 interesting_ids=None, reprocess=False, show_base=False,
739
 
                 pb=None, pp=None, change_reporter=None,
 
606
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
740
607
                 interesting_files=None, do_merge=True,
741
608
                 cherrypick=False, lca_trees=None, this_branch=None):
742
609
        """Initialize the merger object and perform the merge.
745
612
        :param this_tree: The local tree in the merge operation
746
613
        :param base_tree: The common tree in the merge operation
747
614
        :param other_tree: The other tree to merge changes from
748
 
        :param this_branch: The branch associated with this_tree.  Defaults to
749
 
            this_tree.branch if not supplied.
 
615
        :param this_branch: The branch associated with this_tree
750
616
        :param interesting_ids: The file_ids of files that should be
751
617
            participate in the merge.  May not be combined with
752
618
            interesting_files.
753
619
        :param: reprocess If True, perform conflict-reduction processing.
754
620
        :param show_base: If True, show the base revision in text conflicts.
755
621
            (incompatible with reprocess)
756
 
        :param pb: ignored
 
622
        :param pb: A Progress bar
757
623
        :param pp: A ProgressPhase object
758
624
        :param change_reporter: An object that should report changes made
759
625
        :param interesting_files: The tree-relative paths of files that should
770
636
        if interesting_files is not None and interesting_ids is not None:
771
637
            raise ValueError(
772
638
                'specify either interesting_ids or interesting_files')
773
 
        if this_branch is None:
774
 
            this_branch = this_tree.branch
775
639
        self.interesting_ids = interesting_ids
776
640
        self.interesting_files = interesting_files
777
641
        self.this_tree = working_tree
788
652
        # making sure we haven't missed any corner cases.
789
653
        # if lca_trees is None:
790
654
        #     self._lca_trees = [self.base_tree]
 
655
        self.pb = pb
 
656
        self.pp = pp
791
657
        self.change_reporter = change_reporter
792
658
        self.cherrypick = cherrypick
 
659
        if self.pp is None:
 
660
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
793
661
        if do_merge:
794
662
            self.do_merge()
795
 
        if pp is not None:
796
 
            warnings.warn("pp argument to Merge3Merger is deprecated")
797
 
        if pb is not None:
798
 
            warnings.warn("pb argument to Merge3Merger is deprecated")
799
663
 
800
664
    def do_merge(self):
801
 
        operation = OperationWithCleanups(self._do_merge)
802
665
        self.this_tree.lock_tree_write()
803
 
        operation.add_cleanup(self.this_tree.unlock)
804
666
        self.base_tree.lock_read()
805
 
        operation.add_cleanup(self.base_tree.unlock)
806
667
        self.other_tree.lock_read()
807
 
        operation.add_cleanup(self.other_tree.unlock)
808
 
        operation.run()
809
 
 
810
 
    def _do_merge(self, operation):
811
 
        self.tt = transform.TreeTransform(self.this_tree, None)
812
 
        operation.add_cleanup(self.tt.finalize)
813
 
        self._compute_transform()
814
 
        results = self.tt.apply(no_conflicts=True)
815
 
        self.write_modified(results)
816
668
        try:
817
 
            self.this_tree.add_conflicts(self.cooked_conflicts)
818
 
        except errors.UnsupportedOperation:
819
 
            pass
 
669
            self.tt = transform.TreeTransform(self.this_tree, self.pb)
 
670
            try:
 
671
                self.pp.next_phase()
 
672
                self._compute_transform()
 
673
                self.pp.next_phase()
 
674
                results = self.tt.apply(no_conflicts=True)
 
675
                self.write_modified(results)
 
676
                try:
 
677
                    self.this_tree.add_conflicts(self.cooked_conflicts)
 
678
                except errors.UnsupportedOperation:
 
679
                    pass
 
680
            finally:
 
681
                self.tt.finalize()
 
682
        finally:
 
683
            self.other_tree.unlock()
 
684
            self.base_tree.unlock()
 
685
            self.this_tree.unlock()
 
686
            self.pb.clear()
820
687
 
821
688
    def make_preview_transform(self):
822
 
        operation = OperationWithCleanups(self._make_preview_transform)
823
689
        self.base_tree.lock_read()
824
 
        operation.add_cleanup(self.base_tree.unlock)
825
690
        self.other_tree.lock_read()
826
 
        operation.add_cleanup(self.other_tree.unlock)
827
 
        return operation.run_simple()
828
 
 
829
 
    def _make_preview_transform(self):
830
691
        self.tt = transform.TransformPreview(self.this_tree)
831
 
        self._compute_transform()
 
692
        try:
 
693
            self.pp.next_phase()
 
694
            self._compute_transform()
 
695
            self.pp.next_phase()
 
696
        finally:
 
697
            self.other_tree.unlock()
 
698
            self.base_tree.unlock()
 
699
            self.pb.clear()
832
700
        return self.tt
833
701
 
834
702
    def _compute_transform(self):
840
708
            resolver = self._lca_multi_way
841
709
        child_pb = ui.ui_factory.nested_progress_bar()
842
710
        try:
843
 
            factories = Merger.hooks['merge_file_content']
844
 
            hooks = [factory(self) for factory in factories] + [self]
845
 
            self.active_hooks = [hook for hook in hooks if hook is not None]
846
711
            for num, (file_id, changed, parents3, names3,
847
712
                      executable3) in enumerate(entries):
848
713
                child_pb.update('Preparing file merge', num, len(entries))
849
714
                self._merge_names(file_id, parents3, names3, resolver=resolver)
850
715
                if changed:
851
 
                    file_status = self._do_merge_contents(file_id)
 
716
                    file_status = self.merge_contents(file_id)
852
717
                else:
853
718
                    file_status = 'unmodified'
854
719
                self._merge_executable(file_id,
856
721
        finally:
857
722
            child_pb.finished()
858
723
        self.fix_root()
 
724
        self.pp.next_phase()
859
725
        child_pb = ui.ui_factory.nested_progress_bar()
860
726
        try:
861
727
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
1059
925
                        continue
1060
926
                else:
1061
927
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
 
928
                # XXX: We need to handle kind == 'symlink'
1062
929
 
1063
930
            # If we have gotten this far, that means something has changed
1064
931
            result.append((file_id, content_changed,
1086
953
        other_root = self.tt.trans_id_file_id(other_root_file_id)
1087
954
        if other_root == self.tt.root:
1088
955
            return
1089
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1090
 
            # the other tree's root is a non-root in the current tree (as when
1091
 
            # a previously unrelated branch is merged into another)
1092
 
            return
1093
956
        try:
1094
957
            self.tt.final_kind(other_root)
1095
 
            other_root_is_present = True
1096
958
        except errors.NoSuchFile:
1097
 
            # other_root doesn't have a physical representation. We still need
1098
 
            # to move any references to the actual root of the tree.
1099
 
            other_root_is_present = False
1100
 
        # 'other_tree.inventory.root' is not present in this tree. We are
1101
 
        # calling adjust_path for children which *want* to be present with a
1102
 
        # correct place to go.
1103
 
        for thing, child in self.other_tree.inventory.root.children.iteritems():
 
959
            return
 
960
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
 
961
            # the other tree's root is a non-root in the current tree
 
962
            return
 
963
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
 
964
        self.tt.cancel_creation(other_root)
 
965
        self.tt.cancel_versioning(other_root)
 
966
 
 
967
    def reparent_children(self, ie, target):
 
968
        for thing, child in ie.children.iteritems():
1104
969
            trans_id = self.tt.trans_id_file_id(child.file_id)
1105
 
            if not other_root_is_present:
1106
 
                # FIXME: Make final_kind returns None instead of raising
1107
 
                # NoSuchFile to avoid the ugly construct below -- vila 20100402
1108
 
                try:
1109
 
                    self.tt.final_kind(trans_id)
1110
 
                    # The item exist in the final tree and has a defined place
1111
 
                    # to go already.
1112
 
                    continue
1113
 
                except errors.NoSuchFile, e:
1114
 
                    pass
1115
 
            # Move the item into the root
1116
 
            self.tt.adjust_path(self.tt.final_name(trans_id),
1117
 
                                self.tt.root, trans_id)
1118
 
        if other_root_is_present:
1119
 
            self.tt.cancel_creation(other_root)
1120
 
            self.tt.cancel_versioning(other_root)
 
970
            self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1121
971
 
1122
972
    def write_modified(self, results):
1123
973
        modified_hashes = {}
1170
1020
 
1171
1021
    @staticmethod
1172
1022
    def _three_way(base, other, this):
 
1023
        #if base == other, either they all agree, or only THIS has changed.
1173
1024
        if base == other:
1174
 
            # if 'base == other', either they all agree, or only 'this' has
1175
 
            # changed.
1176
1025
            return 'this'
1177
1026
        elif this not in (base, other):
1178
 
            # 'this' is neither 'base' nor 'other', so both sides changed
1179
1027
            return 'conflict'
 
1028
        # "Ambiguous clean merge" -- both sides have made the same change.
1180
1029
        elif this == other:
1181
 
            # "Ambiguous clean merge" -- both sides have made the same change.
1182
1030
            return "this"
 
1031
        # this == base: only other has changed.
1183
1032
        else:
1184
 
            # this == base: only other has changed.
1185
1033
            return "other"
1186
1034
 
1187
1035
    @staticmethod
1231
1079
                # only has an lca value
1232
1080
                return 'other'
1233
1081
 
1234
 
        # At this point, the lcas disagree, and the tip disagree
 
1082
        # At this point, the lcas disagree, and the tips disagree
1235
1083
        return 'conflict'
1236
1084
 
1237
1085
    @staticmethod
1238
 
    @deprecated_method(deprecated_in((2, 2, 0)))
1239
1086
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1240
1087
        """Do a three-way test on a scalar.
1241
1088
        Return "this", "other" or "conflict", depending whether a value wins.
1291
1138
                parent_id_winner = "other"
1292
1139
        if name_winner == "this" and parent_id_winner == "this":
1293
1140
            return
1294
 
        if name_winner == 'conflict' or parent_id_winner == 'conflict':
1295
 
            # Creating helpers (.OTHER or .THIS) here cause problems down the
1296
 
            # road if a ContentConflict needs to be created so we should not do
1297
 
            # that
1298
 
            trans_id = self.tt.trans_id_file_id(file_id)
1299
 
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1300
 
                                        this_parent, this_name,
1301
 
                                        other_parent, other_name))
 
1141
        if name_winner == "conflict":
 
1142
            trans_id = self.tt.trans_id_file_id(file_id)
 
1143
            self._raw_conflicts.append(('name conflict', trans_id,
 
1144
                                        this_name, other_name))
 
1145
        if parent_id_winner == "conflict":
 
1146
            trans_id = self.tt.trans_id_file_id(file_id)
 
1147
            self._raw_conflicts.append(('parent conflict', trans_id,
 
1148
                                        this_parent, other_parent))
1302
1149
        if other_name is None:
1303
1150
            # it doesn't matter whether the result was 'other' or
1304
1151
            # 'conflict'-- if there's no 'other', we leave it alone.
1305
1152
            return
 
1153
        # if we get here, name_winner and parent_winner are set to safe values.
 
1154
        trans_id = self.tt.trans_id_file_id(file_id)
1306
1155
        parent_id = parents[self.winner_idx[parent_id_winner]]
1307
1156
        if parent_id is not None:
1308
 
            # if we get here, name_winner and parent_winner are set to safe
1309
 
            # values.
 
1157
            parent_trans_id = self.tt.trans_id_file_id(parent_id)
1310
1158
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1311
 
                                self.tt.trans_id_file_id(parent_id),
1312
 
                                self.tt.trans_id_file_id(file_id))
 
1159
                                parent_trans_id, trans_id)
1313
1160
 
1314
 
    def _do_merge_contents(self, file_id):
 
1161
    def merge_contents(self, file_id):
1315
1162
        """Performs a merge on file_id contents."""
1316
1163
        def contents_pair(tree):
1317
1164
            if file_id not in tree:
1351
1198
        trans_id = self.tt.trans_id_file_id(file_id)
1352
1199
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1353
1200
            other_pair[0], winner)
1354
 
        hooks = self.active_hooks
 
1201
        hooks = Merger.hooks['merge_file_content']
 
1202
        hooks = list(hooks) + [self.default_text_merge]
1355
1203
        hook_status = 'not_applicable'
1356
1204
        for hook in hooks:
1357
 
            hook_status, lines = hook.merge_contents(params)
 
1205
            hook_status, lines = hook(params)
1358
1206
            if hook_status != 'not_applicable':
1359
1207
                # Don't try any more hooks, this one applies.
1360
1208
                break
1431
1279
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1432
1280
                % (file_id,))
1433
1281
 
1434
 
    def merge_contents(self, merge_hook_params):
1435
 
        """Fallback merge logic after user installed hooks."""
1436
 
        # This function is used in merge hooks as the fallback instance.
1437
 
        # Perhaps making this function and the functions it calls be a 
1438
 
        # a separate class would be better.
 
1282
    def default_text_merge(self, merge_hook_params):
1439
1283
        if merge_hook_params.winner == 'other':
1440
1284
            # OTHER is a straight winner, so replace this contents with other
1441
1285
            return self._default_other_winner_merge(merge_hook_params)
1594
1438
 
1595
1439
    def cook_conflicts(self, fs_conflicts):
1596
1440
        """Convert all conflicts into a form that doesn't depend on trans_id"""
 
1441
        name_conflicts = {}
1597
1442
        self.cooked_conflicts.extend(transform.cook_conflicts(
1598
1443
                fs_conflicts, self.tt))
1599
1444
        fp = transform.FinalPaths(self.tt)
1600
1445
        for conflict in self._raw_conflicts:
1601
1446
            conflict_type = conflict[0]
1602
 
            if conflict_type == 'path conflict':
1603
 
                (trans_id, file_id,
1604
 
                this_parent, this_name,
1605
 
                other_parent, other_name) = conflict[1:]
1606
 
                if this_parent is None or this_name is None:
1607
 
                    this_path = '<deleted>'
1608
 
                else:
1609
 
                    parent_path =  fp.get_path(
1610
 
                        self.tt.trans_id_file_id(this_parent))
1611
 
                    this_path = osutils.pathjoin(parent_path, this_name)
1612
 
                if other_parent is None or other_name is None:
1613
 
                    other_path = '<deleted>'
1614
 
                else:
1615
 
                    parent_path =  fp.get_path(
1616
 
                        self.tt.trans_id_file_id(other_parent))
1617
 
                    other_path = osutils.pathjoin(parent_path, other_name)
1618
 
                c = _mod_conflicts.Conflict.factory(
1619
 
                    'path conflict', path=this_path,
1620
 
                    conflict_path=other_path,
1621
 
                    file_id=file_id)
1622
 
            elif conflict_type == 'contents conflict':
 
1447
            if conflict_type in ('name conflict', 'parent conflict'):
 
1448
                trans_id = conflict[1]
 
1449
                conflict_args = conflict[2:]
 
1450
                if trans_id not in name_conflicts:
 
1451
                    name_conflicts[trans_id] = {}
 
1452
                transform.unique_add(name_conflicts[trans_id], conflict_type,
 
1453
                                     conflict_args)
 
1454
            if conflict_type == 'contents conflict':
1623
1455
                for trans_id in conflict[1]:
1624
1456
                    file_id = self.tt.final_file_id(trans_id)
1625
1457
                    if file_id is not None:
1631
1463
                        break
1632
1464
                c = _mod_conflicts.Conflict.factory(conflict_type,
1633
1465
                                                    path=path, file_id=file_id)
1634
 
            elif conflict_type == 'text conflict':
 
1466
                self.cooked_conflicts.append(c)
 
1467
            if conflict_type == 'text conflict':
1635
1468
                trans_id = conflict[1]
1636
1469
                path = fp.get_path(trans_id)
1637
1470
                file_id = self.tt.final_file_id(trans_id)
1638
1471
                c = _mod_conflicts.Conflict.factory(conflict_type,
1639
1472
                                                    path=path, file_id=file_id)
 
1473
                self.cooked_conflicts.append(c)
 
1474
 
 
1475
        for trans_id, conflicts in name_conflicts.iteritems():
 
1476
            try:
 
1477
                this_parent, other_parent = conflicts['parent conflict']
 
1478
                if this_parent == other_parent:
 
1479
                    raise AssertionError()
 
1480
            except KeyError:
 
1481
                this_parent = other_parent = \
 
1482
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
 
1483
            try:
 
1484
                this_name, other_name = conflicts['name conflict']
 
1485
                if this_name == other_name:
 
1486
                    raise AssertionError()
 
1487
            except KeyError:
 
1488
                this_name = other_name = self.tt.final_name(trans_id)
 
1489
            other_path = fp.get_path(trans_id)
 
1490
            if this_parent is not None and this_name is not None:
 
1491
                this_parent_path = \
 
1492
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
 
1493
                this_path = osutils.pathjoin(this_parent_path, this_name)
1640
1494
            else:
1641
 
                raise AssertionError('bad conflict type: %r' % (conflict,))
 
1495
                this_path = "<deleted>"
 
1496
            file_id = self.tt.final_file_id(trans_id)
 
1497
            c = _mod_conflicts.Conflict.factory('path conflict', path=this_path,
 
1498
                                                conflict_path=other_path,
 
1499
                                                file_id=file_id)
1642
1500
            self.cooked_conflicts.append(c)
1643
1501
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1644
1502
 
1759
1617
                other_rev_id=None,
1760
1618
                interesting_files=None,
1761
1619
                this_tree=None,
1762
 
                pb=None,
 
1620
                pb=progress.DummyProgress(),
1763
1621
                change_reporter=None):
1764
1622
    """Primary interface for merging.
1765
1623