/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 bzrlib/merge.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
17
18
 
18
19
from bzrlib import (
19
20
    branch as _mod_branch,
20
21
    conflicts as _mod_conflicts,
21
22
    debug,
 
23
    decorators,
22
24
    errors,
23
25
    graph as _mod_graph,
 
26
    hooks,
24
27
    merge3,
25
28
    osutils,
26
29
    patiencediff,
27
 
    progress,
28
30
    revision as _mod_revision,
29
31
    textfile,
30
32
    trace,
34
36
    ui,
35
37
    versionedfile
36
38
    )
 
39
from bzrlib.cleanup import OperationWithCleanups
37
40
from bzrlib.symbol_versioning import (
38
41
    deprecated_in,
39
42
    deprecated_method,
43
46
 
44
47
def transform_tree(from_tree, to_tree, interesting_ids=None):
45
48
    from_tree.lock_tree_write()
46
 
    try:
47
 
        merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
48
 
                    interesting_ids=interesting_ids, this_tree=from_tree)
49
 
    finally:
50
 
        from_tree.unlock()
 
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)
 
53
 
 
54
 
 
55
class MergeHooks(hooks.Hooks):
 
56
 
 
57
    def __init__(self):
 
58
        hooks.Hooks.__init__(self)
 
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.",
 
69
            (2, 1), None))
 
70
 
 
71
 
 
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
class MergeHookParams(object):
 
217
    """Object holding parameters passed to merge_file_content hooks.
 
218
 
 
219
    There are some fields hooks can access:
 
220
 
 
221
    :ivar file_id: the file ID of the file being merged
 
222
    :ivar trans_id: the transform ID for the merge of this file
 
223
    :ivar this_kind: kind of file_id in 'this' tree
 
224
    :ivar other_kind: kind of file_id in 'other' tree
 
225
    :ivar winner: one of 'this', 'other', 'conflict'
 
226
    """
 
227
 
 
228
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
229
            winner):
 
230
        self._merger = merger
 
231
        self.file_id = file_id
 
232
        self.trans_id = trans_id
 
233
        self.this_kind = this_kind
 
234
        self.other_kind = other_kind
 
235
        self.winner = winner
 
236
 
 
237
    def is_file_merge(self):
 
238
        """True if this_kind and other_kind are both 'file'."""
 
239
        return self.this_kind == 'file' and self.other_kind == 'file'
 
240
 
 
241
    @decorators.cachedproperty
 
242
    def base_lines(self):
 
243
        """The lines of the 'base' version of the file."""
 
244
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
245
 
 
246
    @decorators.cachedproperty
 
247
    def this_lines(self):
 
248
        """The lines of the 'this' version of the file."""
 
249
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
250
 
 
251
    @decorators.cachedproperty
 
252
    def other_lines(self):
 
253
        """The lines of the 'other' version of the file."""
 
254
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
51
255
 
52
256
 
53
257
class Merger(object):
 
258
 
 
259
    hooks = MergeHooks()
 
260
 
54
261
    def __init__(self, this_branch, other_tree=None, base_tree=None,
55
262
                 this_tree=None, pb=None, change_reporter=None,
56
263
                 recurse='down', revision_graph=None):
71
278
        self.interesting_files = None
72
279
        self.show_base = False
73
280
        self.reprocess = False
74
 
        if pb is None:
75
 
            pb = progress.DummyProgress()
76
 
        self._pb = pb
 
281
        if pb is not None:
 
282
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
77
283
        self.pp = None
78
284
        self.recurse = recurse
79
285
        self.change_reporter = change_reporter
289
495
    def _add_parent(self):
290
496
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
291
497
        new_parent_trees = []
 
498
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
292
499
        for revision_id in new_parents:
293
500
            try:
294
501
                tree = self.revision_tree(revision_id)
296
503
                tree = None
297
504
            else:
298
505
                tree.lock_read()
 
506
                operation.add_cleanup(tree.unlock)
299
507
            new_parent_trees.append((revision_id, tree))
300
 
        try:
301
 
            self.this_tree.set_parent_trees(new_parent_trees,
302
 
                                            allow_leftmost_as_ghost=True)
303
 
        finally:
304
 
            for _revision_id, tree in new_parent_trees:
305
 
                if tree is not None:
306
 
                    tree.unlock()
 
508
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
307
509
 
308
510
    def set_other(self, other_revision, possible_transports=None):
309
511
        """Set the revision and tree to merge from.
432
634
                  'other_tree': self.other_tree,
433
635
                  'interesting_ids': self.interesting_ids,
434
636
                  'interesting_files': self.interesting_files,
435
 
                  'pp': self.pp,
 
637
                  'this_branch': self.this_branch,
436
638
                  'do_merge': False}
437
639
        if self.merge_type.requires_base:
438
640
            kwargs['base_tree'] = self.base_tree
456
658
        if self._is_criss_cross and getattr(self.merge_type,
457
659
                                            'supports_lca_trees', False):
458
660
            kwargs['lca_trees'] = self._lca_trees
459
 
        return self.merge_type(pb=self._pb,
 
661
        return self.merge_type(pb=None,
460
662
                               change_reporter=self.change_reporter,
461
663
                               **kwargs)
462
664
 
463
 
    def _do_merge_to(self, merge):
 
665
    def _do_merge_to(self):
 
666
        merge = self.make_merger()
464
667
        if self.other_branch is not None:
465
668
            self.other_branch.update_references(self.this_branch)
466
669
        merge.do_merge()
480
683
                    sub_tree.branch.repository.revision_tree(base_revision)
481
684
                sub_merge.base_rev_id = base_revision
482
685
                sub_merge.do_merge()
 
686
        return merge
483
687
 
484
688
    def do_merge(self):
 
689
        operation = OperationWithCleanups(self._do_merge_to)
485
690
        self.this_tree.lock_tree_write()
486
 
        try:
487
 
            if self.base_tree is not None:
488
 
                self.base_tree.lock_read()
489
 
            try:
490
 
                if self.other_tree is not None:
491
 
                    self.other_tree.lock_read()
492
 
                try:
493
 
                    merge = self.make_merger()
494
 
                    self._do_merge_to(merge)
495
 
                finally:
496
 
                    if self.other_tree is not None:
497
 
                        self.other_tree.unlock()
498
 
            finally:
499
 
                if self.base_tree is not None:
500
 
                    self.base_tree.unlock()
501
 
        finally:
502
 
            self.this_tree.unlock()
 
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()
503
699
        if len(merge.cooked_conflicts) == 0:
504
700
            if not self.ignore_zero and not trace.is_quiet():
505
701
                trace.note("All changes applied successfully.")
540
736
 
541
737
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
542
738
                 interesting_ids=None, reprocess=False, show_base=False,
543
 
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
 
739
                 pb=None, pp=None, change_reporter=None,
544
740
                 interesting_files=None, do_merge=True,
545
 
                 cherrypick=False, lca_trees=None):
 
741
                 cherrypick=False, lca_trees=None, this_branch=None):
546
742
        """Initialize the merger object and perform the merge.
547
743
 
548
744
        :param working_tree: The working tree to apply the merge to
549
745
        :param this_tree: The local tree in the merge operation
550
746
        :param base_tree: The common tree in the merge operation
551
747
        :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.
552
750
        :param interesting_ids: The file_ids of files that should be
553
751
            participate in the merge.  May not be combined with
554
752
            interesting_files.
555
753
        :param: reprocess If True, perform conflict-reduction processing.
556
754
        :param show_base: If True, show the base revision in text conflicts.
557
755
            (incompatible with reprocess)
558
 
        :param pb: A Progress bar
 
756
        :param pb: ignored
559
757
        :param pp: A ProgressPhase object
560
758
        :param change_reporter: An object that should report changes made
561
759
        :param interesting_files: The tree-relative paths of files that should
572
770
        if interesting_files is not None and interesting_ids is not None:
573
771
            raise ValueError(
574
772
                'specify either interesting_ids or interesting_files')
 
773
        if this_branch is None:
 
774
            this_branch = this_tree.branch
575
775
        self.interesting_ids = interesting_ids
576
776
        self.interesting_files = interesting_files
577
777
        self.this_tree = working_tree
578
778
        self.base_tree = base_tree
579
779
        self.other_tree = other_tree
 
780
        self.this_branch = this_branch
580
781
        self._raw_conflicts = []
581
782
        self.cooked_conflicts = []
582
783
        self.reprocess = reprocess
587
788
        # making sure we haven't missed any corner cases.
588
789
        # if lca_trees is None:
589
790
        #     self._lca_trees = [self.base_tree]
590
 
        self.pb = pb
591
 
        self.pp = pp
592
791
        self.change_reporter = change_reporter
593
792
        self.cherrypick = cherrypick
594
 
        if self.pp is None:
595
 
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
596
793
        if do_merge:
597
794
            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")
598
799
 
599
800
    def do_merge(self):
 
801
        operation = OperationWithCleanups(self._do_merge)
600
802
        self.this_tree.lock_tree_write()
 
803
        operation.add_cleanup(self.this_tree.unlock)
601
804
        self.base_tree.lock_read()
 
805
        operation.add_cleanup(self.base_tree.unlock)
602
806
        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)
603
816
        try:
604
 
            self.tt = transform.TreeTransform(self.this_tree, self.pb)
605
 
            try:
606
 
                self.pp.next_phase()
607
 
                self._compute_transform()
608
 
                self.pp.next_phase()
609
 
                results = self.tt.apply(no_conflicts=True)
610
 
                self.write_modified(results)
611
 
                try:
612
 
                    self.this_tree.add_conflicts(self.cooked_conflicts)
613
 
                except errors.UnsupportedOperation:
614
 
                    pass
615
 
            finally:
616
 
                self.tt.finalize()
617
 
        finally:
618
 
            self.other_tree.unlock()
619
 
            self.base_tree.unlock()
620
 
            self.this_tree.unlock()
621
 
            self.pb.clear()
 
817
            self.this_tree.add_conflicts(self.cooked_conflicts)
 
818
        except errors.UnsupportedOperation:
 
819
            pass
622
820
 
623
821
    def make_preview_transform(self):
 
822
        operation = OperationWithCleanups(self._make_preview_transform)
624
823
        self.base_tree.lock_read()
 
824
        operation.add_cleanup(self.base_tree.unlock)
625
825
        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):
626
830
        self.tt = transform.TransformPreview(self.this_tree)
627
 
        try:
628
 
            self.pp.next_phase()
629
 
            self._compute_transform()
630
 
            self.pp.next_phase()
631
 
        finally:
632
 
            self.other_tree.unlock()
633
 
            self.base_tree.unlock()
634
 
            self.pb.clear()
 
831
        self._compute_transform()
635
832
        return self.tt
636
833
 
637
834
    def _compute_transform(self):
643
840
            resolver = self._lca_multi_way
644
841
        child_pb = ui.ui_factory.nested_progress_bar()
645
842
        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]
646
846
            for num, (file_id, changed, parents3, names3,
647
847
                      executable3) in enumerate(entries):
648
848
                child_pb.update('Preparing file merge', num, len(entries))
649
849
                self._merge_names(file_id, parents3, names3, resolver=resolver)
650
850
                if changed:
651
 
                    file_status = self.merge_contents(file_id)
 
851
                    file_status = self._do_merge_contents(file_id)
652
852
                else:
653
853
                    file_status = 'unmodified'
654
854
                self._merge_executable(file_id,
656
856
        finally:
657
857
            child_pb.finished()
658
858
        self.fix_root()
659
 
        self.pp.next_phase()
660
859
        child_pb = ui.ui_factory.nested_progress_bar()
661
860
        try:
662
861
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
860
1059
                        continue
861
1060
                else:
862
1061
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
863
 
                # XXX: We need to handle kind == 'symlink'
864
1062
 
865
1063
            # If we have gotten this far, that means something has changed
866
1064
            result.append((file_id, content_changed,
888
1086
        other_root = self.tt.trans_id_file_id(other_root_file_id)
889
1087
        if other_root == self.tt.root:
890
1088
            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
891
1093
        try:
892
1094
            self.tt.final_kind(other_root)
 
1095
            other_root_is_present = True
893
1096
        except errors.NoSuchFile:
894
 
            return
895
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
896
 
            # the other tree's root is a non-root in the current tree
897
 
            return
898
 
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
899
 
        self.tt.cancel_creation(other_root)
900
 
        self.tt.cancel_versioning(other_root)
901
 
 
902
 
    def reparent_children(self, ie, target):
903
 
        for thing, child in ie.children.iteritems():
 
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():
904
1104
            trans_id = self.tt.trans_id_file_id(child.file_id)
905
 
            self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_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)
906
1121
 
907
1122
    def write_modified(self, results):
908
1123
        modified_hashes = {}
940
1155
    @staticmethod
941
1156
    def executable(tree, file_id):
942
1157
        """Determine the executability of a file-id (used as a key method)."""
943
 
        if file_id not in tree:
 
1158
        if not tree.has_id(file_id):
944
1159
            return None
945
1160
        if tree.kind(file_id) != "file":
946
1161
            return False
949
1164
    @staticmethod
950
1165
    def kind(tree, file_id):
951
1166
        """Determine the kind of a file-id (used as a key method)."""
952
 
        if file_id not in tree:
 
1167
        if not tree.has_id(file_id):
953
1168
            return None
954
1169
        return tree.kind(file_id)
955
1170
 
956
1171
    @staticmethod
957
1172
    def _three_way(base, other, this):
958
 
        #if base == other, either they all agree, or only THIS has changed.
959
1173
        if base == other:
 
1174
            # if 'base == other', either they all agree, or only 'this' has
 
1175
            # changed.
960
1176
            return 'this'
961
1177
        elif this not in (base, other):
 
1178
            # 'this' is neither 'base' nor 'other', so both sides changed
962
1179
            return 'conflict'
963
 
        # "Ambiguous clean merge" -- both sides have made the same change.
964
1180
        elif this == other:
 
1181
            # "Ambiguous clean merge" -- both sides have made the same change.
965
1182
            return "this"
966
 
        # this == base: only other has changed.
967
1183
        else:
 
1184
            # this == base: only other has changed.
968
1185
            return "other"
969
1186
 
970
1187
    @staticmethod
1014
1231
                # only has an lca value
1015
1232
                return 'other'
1016
1233
 
1017
 
        # At this point, the lcas disagree, and the tips disagree
 
1234
        # At this point, the lcas disagree, and the tip disagree
1018
1235
        return 'conflict'
1019
1236
 
1020
1237
    @staticmethod
 
1238
    @deprecated_method(deprecated_in((2, 2, 0)))
1021
1239
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1022
1240
        """Do a three-way test on a scalar.
1023
1241
        Return "this", "other" or "conflict", depending whether a value wins.
1038
1256
 
1039
1257
    def merge_names(self, file_id):
1040
1258
        def get_entry(tree):
1041
 
            if file_id in tree.inventory:
 
1259
            if tree.has_id(file_id):
1042
1260
                return tree.inventory[file_id]
1043
1261
            else:
1044
1262
                return None
1073
1291
                parent_id_winner = "other"
1074
1292
        if name_winner == "this" and parent_id_winner == "this":
1075
1293
            return
1076
 
        if name_winner == "conflict":
1077
 
            trans_id = self.tt.trans_id_file_id(file_id)
1078
 
            self._raw_conflicts.append(('name conflict', trans_id,
1079
 
                                        this_name, other_name))
1080
 
        if parent_id_winner == "conflict":
1081
 
            trans_id = self.tt.trans_id_file_id(file_id)
1082
 
            self._raw_conflicts.append(('parent conflict', trans_id,
1083
 
                                        this_parent, other_parent))
 
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))
1084
1302
        if other_name is None:
1085
1303
            # it doesn't matter whether the result was 'other' or
1086
1304
            # 'conflict'-- if there's no 'other', we leave it alone.
1087
1305
            return
1088
 
        # if we get here, name_winner and parent_winner are set to safe values.
1089
 
        trans_id = self.tt.trans_id_file_id(file_id)
1090
1306
        parent_id = parents[self.winner_idx[parent_id_winner]]
1091
1307
        if parent_id is not None:
1092
 
            parent_trans_id = self.tt.trans_id_file_id(parent_id)
 
1308
            # if we get here, name_winner and parent_winner are set to safe
 
1309
            # values.
1093
1310
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1094
 
                                parent_trans_id, trans_id)
 
1311
                                self.tt.trans_id_file_id(parent_id),
 
1312
                                self.tt.trans_id_file_id(file_id))
1095
1313
 
1096
 
    def merge_contents(self, file_id):
 
1314
    def _do_merge_contents(self, file_id):
1097
1315
        """Performs a merge on file_id contents."""
1098
1316
        def contents_pair(tree):
1099
1317
            if file_id not in tree:
1107
1325
                contents = None
1108
1326
            return kind, contents
1109
1327
 
1110
 
        def contents_conflict():
1111
 
            trans_id = self.tt.trans_id_file_id(file_id)
1112
 
            name = self.tt.final_name(trans_id)
1113
 
            parent_id = self.tt.final_parent(trans_id)
1114
 
            if file_id in self.this_tree.inventory:
1115
 
                self.tt.unversion_file(trans_id)
1116
 
                if file_id in self.this_tree:
1117
 
                    self.tt.delete_contents(trans_id)
1118
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1119
 
                                              set_version=True)
1120
 
            self._raw_conflicts.append(('contents conflict', file_group))
1121
 
 
1122
1328
        # See SPOT run.  run, SPOT, run.
1123
1329
        # So we're not QUITE repeating ourselves; we do tricky things with
1124
1330
        # file kind...
1140
1346
        if winner == 'this':
1141
1347
            # No interesting changes introduced by OTHER
1142
1348
            return "unmodified"
 
1349
        # We have a hypothetical conflict, but if we have files, then we
 
1350
        # can try to merge the content
1143
1351
        trans_id = self.tt.trans_id_file_id(file_id)
1144
 
        if winner == 'other':
 
1352
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1353
            other_pair[0], winner)
 
1354
        hooks = self.active_hooks
 
1355
        hook_status = 'not_applicable'
 
1356
        for hook in hooks:
 
1357
            hook_status, lines = hook.merge_contents(params)
 
1358
            if hook_status != 'not_applicable':
 
1359
                # Don't try any more hooks, this one applies.
 
1360
                break
 
1361
        result = "modified"
 
1362
        if hook_status == 'not_applicable':
 
1363
            # This is a contents conflict, because none of the available
 
1364
            # functions could merge it.
 
1365
            result = None
 
1366
            name = self.tt.final_name(trans_id)
 
1367
            parent_id = self.tt.final_parent(trans_id)
 
1368
            if self.this_tree.has_id(file_id):
 
1369
                self.tt.unversion_file(trans_id)
 
1370
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1371
                                              set_version=True)
 
1372
            self._raw_conflicts.append(('contents conflict', file_group))
 
1373
        elif hook_status == 'success':
 
1374
            self.tt.create_file(lines, trans_id)
 
1375
        elif hook_status == 'conflicted':
 
1376
            # XXX: perhaps the hook should be able to provide
 
1377
            # the BASE/THIS/OTHER files?
 
1378
            self.tt.create_file(lines, trans_id)
 
1379
            self._raw_conflicts.append(('text conflict', trans_id))
 
1380
            name = self.tt.final_name(trans_id)
 
1381
            parent_id = self.tt.final_parent(trans_id)
 
1382
            self._dump_conflicts(name, parent_id, file_id)
 
1383
        elif hook_status == 'delete':
 
1384
            self.tt.unversion_file(trans_id)
 
1385
            result = "deleted"
 
1386
        elif hook_status == 'done':
 
1387
            # The hook function did whatever it needs to do directly, no
 
1388
            # further action needed here.
 
1389
            pass
 
1390
        else:
 
1391
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1392
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1393
            self.tt.version_file(file_id, trans_id)
 
1394
        # The merge has been performed, so the old contents should not be
 
1395
        # retained.
 
1396
        try:
 
1397
            self.tt.delete_contents(trans_id)
 
1398
        except errors.NoSuchFile:
 
1399
            pass
 
1400
        return result
 
1401
 
 
1402
    def _default_other_winner_merge(self, merge_hook_params):
 
1403
        """Replace this contents with other."""
 
1404
        file_id = merge_hook_params.file_id
 
1405
        trans_id = merge_hook_params.trans_id
 
1406
        file_in_this = self.this_tree.has_id(file_id)
 
1407
        if self.other_tree.has_id(file_id):
 
1408
            # OTHER changed the file
 
1409
            wt = self.this_tree
 
1410
            if wt.supports_content_filtering():
 
1411
                # We get the path from the working tree if it exists.
 
1412
                # That fails though when OTHER is adding a file, so
 
1413
                # we fall back to the other tree to find the path if
 
1414
                # it doesn't exist locally.
 
1415
                try:
 
1416
                    filter_tree_path = wt.id2path(file_id)
 
1417
                except errors.NoSuchId:
 
1418
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1419
            else:
 
1420
                # Skip the id2path lookup for older formats
 
1421
                filter_tree_path = None
 
1422
            transform.create_from_tree(self.tt, trans_id,
 
1423
                             self.other_tree, file_id,
 
1424
                             filter_tree_path=filter_tree_path)
 
1425
            return 'done', None
 
1426
        elif file_in_this:
 
1427
            # OTHER deleted the file
 
1428
            return 'delete', None
 
1429
        else:
 
1430
            raise AssertionError(
 
1431
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1432
                % (file_id,))
 
1433
 
 
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.
 
1439
        if merge_hook_params.winner == 'other':
1145
1440
            # OTHER is a straight winner, so replace this contents with other
1146
 
            file_in_this = file_id in self.this_tree
1147
 
            if file_in_this:
1148
 
                # Remove any existing contents
1149
 
                self.tt.delete_contents(trans_id)
1150
 
            if file_id in self.other_tree:
1151
 
                # OTHER changed the file
1152
 
                wt = self.this_tree
1153
 
                if wt.supports_content_filtering():
1154
 
                    # We get the path from the working tree if it exists.
1155
 
                    # That fails though when OTHER is adding a file, so
1156
 
                    # we fall back to the other tree to find the path if
1157
 
                    # it doesn't exist locally.
1158
 
                    try:
1159
 
                        filter_tree_path = wt.id2path(file_id)
1160
 
                    except errors.NoSuchId:
1161
 
                        filter_tree_path = self.other_tree.id2path(file_id)
1162
 
                else:
1163
 
                    # Skip the id2path lookup for older formats
1164
 
                    filter_tree_path = None
1165
 
                transform.create_from_tree(self.tt, trans_id,
1166
 
                                 self.other_tree, file_id,
1167
 
                                 filter_tree_path=filter_tree_path)
1168
 
                if not file_in_this:
1169
 
                    self.tt.version_file(file_id, trans_id)
1170
 
                return "modified"
1171
 
            elif file_in_this:
1172
 
                # OTHER deleted the file
1173
 
                self.tt.unversion_file(trans_id)
1174
 
                return "deleted"
 
1441
            return self._default_other_winner_merge(merge_hook_params)
 
1442
        elif merge_hook_params.is_file_merge():
 
1443
            # THIS and OTHER are both files, so text merge.  Either
 
1444
            # BASE is a file, or both converted to files, so at least we
 
1445
            # have agreement that output should be a file.
 
1446
            try:
 
1447
                self.text_merge(merge_hook_params.file_id,
 
1448
                    merge_hook_params.trans_id)
 
1449
            except errors.BinaryFile:
 
1450
                return 'not_applicable', None
 
1451
            return 'done', None
1175
1452
        else:
1176
 
            # We have a hypothetical conflict, but if we have files, then we
1177
 
            # can try to merge the content
1178
 
            if this_pair[0] == 'file' and other_pair[0] == 'file':
1179
 
                # THIS and OTHER are both files, so text merge.  Either
1180
 
                # BASE is a file, or both converted to files, so at least we
1181
 
                # have agreement that output should be a file.
1182
 
                try:
1183
 
                    self.text_merge(file_id, trans_id)
1184
 
                except errors.BinaryFile:
1185
 
                    return contents_conflict()
1186
 
                if file_id not in self.this_tree:
1187
 
                    self.tt.version_file(file_id, trans_id)
1188
 
                try:
1189
 
                    self.tt.tree_kind(trans_id)
1190
 
                    self.tt.delete_contents(trans_id)
1191
 
                except errors.NoSuchFile:
1192
 
                    pass
1193
 
                return "modified"
1194
 
            else:
1195
 
                return contents_conflict()
 
1453
            return 'not_applicable', None
1196
1454
 
1197
1455
    def get_lines(self, tree, file_id):
1198
1456
        """Return the lines in a file, or an empty list."""
1199
 
        if file_id in tree:
 
1457
        if tree.has_id(file_id):
1200
1458
            return tree.get_file(file_id).readlines()
1201
1459
        else:
1202
1460
            return []
1205
1463
        """Perform a three-way text merge on a file_id"""
1206
1464
        # it's possible that we got here with base as a different type.
1207
1465
        # if so, we just want two-way text conflicts.
1208
 
        if file_id in self.base_tree and \
 
1466
        if self.base_tree.has_id(file_id) and \
1209
1467
            self.base_tree.kind(file_id) == "file":
1210
1468
            base_lines = self.get_lines(self.base_tree, file_id)
1211
1469
        else:
1274
1532
        versioned = False
1275
1533
        file_group = []
1276
1534
        for suffix, tree, lines in data:
1277
 
            if file_id in tree:
 
1535
            if tree.has_id(file_id):
1278
1536
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1279
1537
                                               suffix, lines, filter_tree_path)
1280
1538
                file_group.append(trans_id)
1324
1582
        if winner == "this":
1325
1583
            executability = this_executable
1326
1584
        else:
1327
 
            if file_id in self.other_tree:
 
1585
            if self.other_tree.has_id(file_id):
1328
1586
                executability = other_executable
1329
 
            elif file_id in self.this_tree:
 
1587
            elif self.this_tree.has_id(file_id):
1330
1588
                executability = this_executable
1331
 
            elif file_id in self.base_tree:
 
1589
            elif self.base_tree_has_id(file_id):
1332
1590
                executability = base_executable
1333
1591
        if executability is not None:
1334
1592
            trans_id = self.tt.trans_id_file_id(file_id)
1336
1594
 
1337
1595
    def cook_conflicts(self, fs_conflicts):
1338
1596
        """Convert all conflicts into a form that doesn't depend on trans_id"""
1339
 
        name_conflicts = {}
1340
1597
        self.cooked_conflicts.extend(transform.cook_conflicts(
1341
1598
                fs_conflicts, self.tt))
1342
1599
        fp = transform.FinalPaths(self.tt)
1343
1600
        for conflict in self._raw_conflicts:
1344
1601
            conflict_type = conflict[0]
1345
 
            if conflict_type in ('name conflict', 'parent conflict'):
1346
 
                trans_id = conflict[1]
1347
 
                conflict_args = conflict[2:]
1348
 
                if trans_id not in name_conflicts:
1349
 
                    name_conflicts[trans_id] = {}
1350
 
                transform.unique_add(name_conflicts[trans_id], conflict_type,
1351
 
                                     conflict_args)
1352
 
            if conflict_type == 'contents conflict':
 
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':
1353
1623
                for trans_id in conflict[1]:
1354
1624
                    file_id = self.tt.final_file_id(trans_id)
1355
1625
                    if file_id is not None:
1361
1631
                        break
1362
1632
                c = _mod_conflicts.Conflict.factory(conflict_type,
1363
1633
                                                    path=path, file_id=file_id)
1364
 
                self.cooked_conflicts.append(c)
1365
 
            if conflict_type == 'text conflict':
 
1634
            elif conflict_type == 'text conflict':
1366
1635
                trans_id = conflict[1]
1367
1636
                path = fp.get_path(trans_id)
1368
1637
                file_id = self.tt.final_file_id(trans_id)
1369
1638
                c = _mod_conflicts.Conflict.factory(conflict_type,
1370
1639
                                                    path=path, file_id=file_id)
1371
 
                self.cooked_conflicts.append(c)
1372
 
 
1373
 
        for trans_id, conflicts in name_conflicts.iteritems():
1374
 
            try:
1375
 
                this_parent, other_parent = conflicts['parent conflict']
1376
 
                if this_parent == other_parent:
1377
 
                    raise AssertionError()
1378
 
            except KeyError:
1379
 
                this_parent = other_parent = \
1380
 
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
1381
 
            try:
1382
 
                this_name, other_name = conflicts['name conflict']
1383
 
                if this_name == other_name:
1384
 
                    raise AssertionError()
1385
 
            except KeyError:
1386
 
                this_name = other_name = self.tt.final_name(trans_id)
1387
 
            other_path = fp.get_path(trans_id)
1388
 
            if this_parent is not None and this_name is not None:
1389
 
                this_parent_path = \
1390
 
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
1391
 
                this_path = osutils.pathjoin(this_parent_path, this_name)
1392
1640
            else:
1393
 
                this_path = "<deleted>"
1394
 
            file_id = self.tt.final_file_id(trans_id)
1395
 
            c = _mod_conflicts.Conflict.factory('path conflict', path=this_path,
1396
 
                                                conflict_path=other_path,
1397
 
                                                file_id=file_id)
 
1641
                raise AssertionError('bad conflict type: %r' % (conflict,))
1398
1642
            self.cooked_conflicts.append(c)
1399
1643
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1400
1644
 
1515
1759
                other_rev_id=None,
1516
1760
                interesting_files=None,
1517
1761
                this_tree=None,
1518
 
                pb=progress.DummyProgress(),
 
1762
                pb=None,
1519
1763
                change_reporter=None):
1520
1764
    """Primary interface for merging.
1521
1765