/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: Jelmer Vernooij
  • Date: 2010-03-21 21:39:33 UTC
  • mfrom: (5102 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5143.
  • Revision ID: jelmer@samba.org-20100321213933-fexeh9zcoz8oaju2
merge bzr.dev.

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 ConfigurableFileMerger(AbstractPerFileMerger):
 
97
    """Merge individual files when configured via a .conf file.
 
98
 
 
99
    This is a base class for concrete custom file merging logic. Concrete
 
100
    classes should implement ``merge_text``.
 
101
 
 
102
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
103
    
 
104
    :ivar affected_files: The configured file paths to merge.
 
105
 
 
106
    :cvar name_prefix: The prefix to use when looking up configuration
 
107
        details. <name_prefix>_merge_files describes the files targeted by the
 
108
        hook for example.
 
109
        
 
110
    :cvar default_files: The default file paths to merge when no configuration
 
111
        is present.
 
112
    """
 
113
 
 
114
    name_prefix = None
 
115
    default_files = None
 
116
 
 
117
    def __init__(self, merger):
 
118
        super(ConfigurableFileMerger, self).__init__(merger)
 
119
        self.affected_files = None
 
120
        self.default_files = self.__class__.default_files or []
 
121
        self.name_prefix = self.__class__.name_prefix
 
122
        if self.name_prefix is None:
 
123
            raise ValueError("name_prefix must be set.")
 
124
 
 
125
    def filename_matches_config(self, params):
 
126
        """Check whether the file should call the merge hook.
 
127
 
 
128
        <name_prefix>_merge_files configuration variable is a list of files
 
129
        that should use the hook.
 
130
        """
 
131
        affected_files = self.affected_files
 
132
        if affected_files is None:
 
133
            config = self.merger.this_branch.get_config()
 
134
            # Until bzr provides a better policy for caching the config, we
 
135
            # just add the part we're interested in to the params to avoid
 
136
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
137
            # branch.conf).
 
138
            config_key = self.name_prefix + '_merge_files'
 
139
            affected_files = config.get_user_option_as_list(config_key)
 
140
            if affected_files is None:
 
141
                # If nothing was specified in the config, use the default.
 
142
                affected_files = self.default_files
 
143
            self.affected_files = affected_files
 
144
        if affected_files:
 
145
            filename = self.merger.this_tree.id2path(params.file_id)
 
146
            if filename in affected_files:
 
147
                return True
 
148
        return False
 
149
 
 
150
    def merge_contents(self, params):
 
151
        """Merge the contents of a single file."""
 
152
        # First, check whether this custom merge logic should be used.  We
 
153
        # expect most files should not be merged by this handler.
 
154
        if (
 
155
            # OTHER is a straight winner, rely on default merge.
 
156
            params.winner == 'other' or
 
157
            # THIS and OTHER aren't both files.
 
158
            not params.is_file_merge() or
 
159
            # The filename isn't listed in the 'NAME_merge_files' config
 
160
            # option.
 
161
            not self.filename_matches_config(params)):
 
162
            return 'not_applicable', None
 
163
        return self.merge_text(params)
 
164
 
 
165
    def merge_text(self, params):
 
166
        """Merge the byte contents of a single file.
 
167
 
 
168
        This is called after checking that the merge should be performed in
 
169
        merge_contents, and it should behave as per
 
170
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
171
        """
 
172
        raise NotImplementedError(self.merge_text)
 
173
 
 
174
 
 
175
class MergeHookParams(object):
 
176
    """Object holding parameters passed to merge_file_content hooks.
 
177
 
 
178
    There are some fields hooks can access:
 
179
 
 
180
    :ivar file_id: the file ID of the file being merged
 
181
    :ivar trans_id: the transform ID for the merge of this file
 
182
    :ivar this_kind: kind of file_id in 'this' tree
 
183
    :ivar other_kind: kind of file_id in 'other' tree
 
184
    :ivar winner: one of 'this', 'other', 'conflict'
 
185
    """
 
186
 
 
187
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
188
            winner):
 
189
        self._merger = merger
 
190
        self.file_id = file_id
 
191
        self.trans_id = trans_id
 
192
        self.this_kind = this_kind
 
193
        self.other_kind = other_kind
 
194
        self.winner = winner
 
195
 
 
196
    def is_file_merge(self):
 
197
        """True if this_kind and other_kind are both 'file'."""
 
198
        return self.this_kind == 'file' and self.other_kind == 'file'
 
199
 
 
200
    @decorators.cachedproperty
 
201
    def base_lines(self):
 
202
        """The lines of the 'base' version of the file."""
 
203
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
204
 
 
205
    @decorators.cachedproperty
 
206
    def this_lines(self):
 
207
        """The lines of the 'this' version of the file."""
 
208
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
209
 
 
210
    @decorators.cachedproperty
 
211
    def other_lines(self):
 
212
        """The lines of the 'other' version of the file."""
 
213
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
51
214
 
52
215
 
53
216
class Merger(object):
 
217
 
 
218
    hooks = MergeHooks()
 
219
 
54
220
    def __init__(self, this_branch, other_tree=None, base_tree=None,
55
221
                 this_tree=None, pb=None, change_reporter=None,
56
222
                 recurse='down', revision_graph=None):
71
237
        self.interesting_files = None
72
238
        self.show_base = False
73
239
        self.reprocess = False
74
 
        if pb is None:
75
 
            pb = progress.DummyProgress()
76
 
        self._pb = pb
 
240
        if pb is not None:
 
241
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
77
242
        self.pp = None
78
243
        self.recurse = recurse
79
244
        self.change_reporter = change_reporter
289
454
    def _add_parent(self):
290
455
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
291
456
        new_parent_trees = []
 
457
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
292
458
        for revision_id in new_parents:
293
459
            try:
294
460
                tree = self.revision_tree(revision_id)
296
462
                tree = None
297
463
            else:
298
464
                tree.lock_read()
 
465
                operation.add_cleanup(tree.unlock)
299
466
            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()
 
467
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
307
468
 
308
469
    def set_other(self, other_revision, possible_transports=None):
309
470
        """Set the revision and tree to merge from.
432
593
                  'other_tree': self.other_tree,
433
594
                  'interesting_ids': self.interesting_ids,
434
595
                  'interesting_files': self.interesting_files,
435
 
                  'pp': self.pp,
 
596
                  'this_branch': self.this_branch,
436
597
                  'do_merge': False}
437
598
        if self.merge_type.requires_base:
438
599
            kwargs['base_tree'] = self.base_tree
456
617
        if self._is_criss_cross and getattr(self.merge_type,
457
618
                                            'supports_lca_trees', False):
458
619
            kwargs['lca_trees'] = self._lca_trees
459
 
        return self.merge_type(pb=self._pb,
 
620
        return self.merge_type(pb=None,
460
621
                               change_reporter=self.change_reporter,
461
622
                               **kwargs)
462
623
 
463
 
    def _do_merge_to(self, merge):
 
624
    def _do_merge_to(self):
 
625
        merge = self.make_merger()
464
626
        if self.other_branch is not None:
465
627
            self.other_branch.update_references(self.this_branch)
466
628
        merge.do_merge()
480
642
                    sub_tree.branch.repository.revision_tree(base_revision)
481
643
                sub_merge.base_rev_id = base_revision
482
644
                sub_merge.do_merge()
 
645
        return merge
483
646
 
484
647
    def do_merge(self):
 
648
        operation = OperationWithCleanups(self._do_merge_to)
485
649
        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()
 
650
        operation.add_cleanup(self.this_tree.unlock)
 
651
        if self.base_tree is not None:
 
652
            self.base_tree.lock_read()
 
653
            operation.add_cleanup(self.base_tree.unlock)
 
654
        if self.other_tree is not None:
 
655
            self.other_tree.lock_read()
 
656
            operation.add_cleanup(self.other_tree.unlock)
 
657
        merge = operation.run_simple()
503
658
        if len(merge.cooked_conflicts) == 0:
504
659
            if not self.ignore_zero and not trace.is_quiet():
505
660
                trace.note("All changes applied successfully.")
540
695
 
541
696
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
542
697
                 interesting_ids=None, reprocess=False, show_base=False,
543
 
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
 
698
                 pb=None, pp=None, change_reporter=None,
544
699
                 interesting_files=None, do_merge=True,
545
 
                 cherrypick=False, lca_trees=None):
 
700
                 cherrypick=False, lca_trees=None, this_branch=None):
546
701
        """Initialize the merger object and perform the merge.
547
702
 
548
703
        :param working_tree: The working tree to apply the merge to
549
704
        :param this_tree: The local tree in the merge operation
550
705
        :param base_tree: The common tree in the merge operation
551
706
        :param other_tree: The other tree to merge changes from
 
707
        :param this_branch: The branch associated with this_tree
552
708
        :param interesting_ids: The file_ids of files that should be
553
709
            participate in the merge.  May not be combined with
554
710
            interesting_files.
555
711
        :param: reprocess If True, perform conflict-reduction processing.
556
712
        :param show_base: If True, show the base revision in text conflicts.
557
713
            (incompatible with reprocess)
558
 
        :param pb: A Progress bar
 
714
        :param pb: ignored
559
715
        :param pp: A ProgressPhase object
560
716
        :param change_reporter: An object that should report changes made
561
717
        :param interesting_files: The tree-relative paths of files that should
577
733
        self.this_tree = working_tree
578
734
        self.base_tree = base_tree
579
735
        self.other_tree = other_tree
 
736
        self.this_branch = this_branch
580
737
        self._raw_conflicts = []
581
738
        self.cooked_conflicts = []
582
739
        self.reprocess = reprocess
587
744
        # making sure we haven't missed any corner cases.
588
745
        # if lca_trees is None:
589
746
        #     self._lca_trees = [self.base_tree]
590
 
        self.pb = pb
591
 
        self.pp = pp
592
747
        self.change_reporter = change_reporter
593
748
        self.cherrypick = cherrypick
594
 
        if self.pp is None:
595
 
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
596
749
        if do_merge:
597
750
            self.do_merge()
 
751
        if pp is not None:
 
752
            warnings.warn("pp argument to Merge3Merger is deprecated")
 
753
        if pb is not None:
 
754
            warnings.warn("pb argument to Merge3Merger is deprecated")
598
755
 
599
756
    def do_merge(self):
 
757
        operation = OperationWithCleanups(self._do_merge)
600
758
        self.this_tree.lock_tree_write()
 
759
        operation.add_cleanup(self.this_tree.unlock)
601
760
        self.base_tree.lock_read()
 
761
        operation.add_cleanup(self.base_tree.unlock)
602
762
        self.other_tree.lock_read()
 
763
        operation.add_cleanup(self.other_tree.unlock)
 
764
        operation.run()
 
765
 
 
766
    def _do_merge(self, operation):
 
767
        self.tt = transform.TreeTransform(self.this_tree, None)
 
768
        operation.add_cleanup(self.tt.finalize)
 
769
        self._compute_transform()
 
770
        results = self.tt.apply(no_conflicts=True)
 
771
        self.write_modified(results)
603
772
        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()
 
773
            self.this_tree.add_conflicts(self.cooked_conflicts)
 
774
        except errors.UnsupportedOperation:
 
775
            pass
622
776
 
623
777
    def make_preview_transform(self):
 
778
        operation = OperationWithCleanups(self._make_preview_transform)
624
779
        self.base_tree.lock_read()
 
780
        operation.add_cleanup(self.base_tree.unlock)
625
781
        self.other_tree.lock_read()
 
782
        operation.add_cleanup(self.other_tree.unlock)
 
783
        return operation.run_simple()
 
784
 
 
785
    def _make_preview_transform(self):
626
786
        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()
 
787
        self._compute_transform()
635
788
        return self.tt
636
789
 
637
790
    def _compute_transform(self):
643
796
            resolver = self._lca_multi_way
644
797
        child_pb = ui.ui_factory.nested_progress_bar()
645
798
        try:
 
799
            factories = Merger.hooks['merge_file_content']
 
800
            hooks = [factory(self) for factory in factories] + [self]
 
801
            self.active_hooks = [hook for hook in hooks if hook is not None]
646
802
            for num, (file_id, changed, parents3, names3,
647
803
                      executable3) in enumerate(entries):
648
804
                child_pb.update('Preparing file merge', num, len(entries))
649
805
                self._merge_names(file_id, parents3, names3, resolver=resolver)
650
806
                if changed:
651
 
                    file_status = self.merge_contents(file_id)
 
807
                    file_status = self._do_merge_contents(file_id)
652
808
                else:
653
809
                    file_status = 'unmodified'
654
810
                self._merge_executable(file_id,
656
812
        finally:
657
813
            child_pb.finished()
658
814
        self.fix_root()
659
 
        self.pp.next_phase()
660
815
        child_pb = ui.ui_factory.nested_progress_bar()
661
816
        try:
662
817
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
892
1047
            self.tt.final_kind(other_root)
893
1048
        except errors.NoSuchFile:
894
1049
            return
895
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
1050
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
896
1051
            # the other tree's root is a non-root in the current tree
897
1052
            return
898
1053
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
940
1095
    @staticmethod
941
1096
    def executable(tree, file_id):
942
1097
        """Determine the executability of a file-id (used as a key method)."""
943
 
        if file_id not in tree:
 
1098
        if not tree.has_id(file_id):
944
1099
            return None
945
1100
        if tree.kind(file_id) != "file":
946
1101
            return False
949
1104
    @staticmethod
950
1105
    def kind(tree, file_id):
951
1106
        """Determine the kind of a file-id (used as a key method)."""
952
 
        if file_id not in tree:
 
1107
        if not tree.has_id(file_id):
953
1108
            return None
954
1109
        return tree.kind(file_id)
955
1110
 
1018
1173
        return 'conflict'
1019
1174
 
1020
1175
    @staticmethod
 
1176
    @deprecated_method(deprecated_in((2, 2, 0)))
1021
1177
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1022
1178
        """Do a three-way test on a scalar.
1023
1179
        Return "this", "other" or "conflict", depending whether a value wins.
1038
1194
 
1039
1195
    def merge_names(self, file_id):
1040
1196
        def get_entry(tree):
1041
 
            if file_id in tree.inventory:
 
1197
            if tree.has_id(file_id):
1042
1198
                return tree.inventory[file_id]
1043
1199
            else:
1044
1200
                return None
1093
1249
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1094
1250
                                parent_trans_id, trans_id)
1095
1251
 
1096
 
    def merge_contents(self, file_id):
 
1252
    def _do_merge_contents(self, file_id):
1097
1253
        """Performs a merge on file_id contents."""
1098
1254
        def contents_pair(tree):
1099
1255
            if file_id not in tree:
1107
1263
                contents = None
1108
1264
            return kind, contents
1109
1265
 
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
1266
        # See SPOT run.  run, SPOT, run.
1123
1267
        # So we're not QUITE repeating ourselves; we do tricky things with
1124
1268
        # file kind...
1140
1284
        if winner == 'this':
1141
1285
            # No interesting changes introduced by OTHER
1142
1286
            return "unmodified"
 
1287
        # We have a hypothetical conflict, but if we have files, then we
 
1288
        # can try to merge the content
1143
1289
        trans_id = self.tt.trans_id_file_id(file_id)
1144
 
        if winner == 'other':
 
1290
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1291
            other_pair[0], winner)
 
1292
        hooks = self.active_hooks
 
1293
        hook_status = 'not_applicable'
 
1294
        for hook in hooks:
 
1295
            hook_status, lines = hook.merge_contents(params)
 
1296
            if hook_status != 'not_applicable':
 
1297
                # Don't try any more hooks, this one applies.
 
1298
                break
 
1299
        result = "modified"
 
1300
        if hook_status == 'not_applicable':
 
1301
            # This is a contents conflict, because none of the available
 
1302
            # functions could merge it.
 
1303
            result = None
 
1304
            name = self.tt.final_name(trans_id)
 
1305
            parent_id = self.tt.final_parent(trans_id)
 
1306
            if self.this_tree.has_id(file_id):
 
1307
                self.tt.unversion_file(trans_id)
 
1308
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1309
                                              set_version=True)
 
1310
            self._raw_conflicts.append(('contents conflict', file_group))
 
1311
        elif hook_status == 'success':
 
1312
            self.tt.create_file(lines, trans_id)
 
1313
        elif hook_status == 'conflicted':
 
1314
            # XXX: perhaps the hook should be able to provide
 
1315
            # the BASE/THIS/OTHER files?
 
1316
            self.tt.create_file(lines, trans_id)
 
1317
            self._raw_conflicts.append(('text conflict', trans_id))
 
1318
            name = self.tt.final_name(trans_id)
 
1319
            parent_id = self.tt.final_parent(trans_id)
 
1320
            self._dump_conflicts(name, parent_id, file_id)
 
1321
        elif hook_status == 'delete':
 
1322
            self.tt.unversion_file(trans_id)
 
1323
            result = "deleted"
 
1324
        elif hook_status == 'done':
 
1325
            # The hook function did whatever it needs to do directly, no
 
1326
            # further action needed here.
 
1327
            pass
 
1328
        else:
 
1329
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1330
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1331
            self.tt.version_file(file_id, trans_id)
 
1332
        # The merge has been performed, so the old contents should not be
 
1333
        # retained.
 
1334
        try:
 
1335
            self.tt.delete_contents(trans_id)
 
1336
        except errors.NoSuchFile:
 
1337
            pass
 
1338
        return result
 
1339
 
 
1340
    def _default_other_winner_merge(self, merge_hook_params):
 
1341
        """Replace this contents with other."""
 
1342
        file_id = merge_hook_params.file_id
 
1343
        trans_id = merge_hook_params.trans_id
 
1344
        file_in_this = self.this_tree.has_id(file_id)
 
1345
        if self.other_tree.has_id(file_id):
 
1346
            # OTHER changed the file
 
1347
            wt = self.this_tree
 
1348
            if wt.supports_content_filtering():
 
1349
                # We get the path from the working tree if it exists.
 
1350
                # That fails though when OTHER is adding a file, so
 
1351
                # we fall back to the other tree to find the path if
 
1352
                # it doesn't exist locally.
 
1353
                try:
 
1354
                    filter_tree_path = wt.id2path(file_id)
 
1355
                except errors.NoSuchId:
 
1356
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1357
            else:
 
1358
                # Skip the id2path lookup for older formats
 
1359
                filter_tree_path = None
 
1360
            transform.create_from_tree(self.tt, trans_id,
 
1361
                             self.other_tree, file_id,
 
1362
                             filter_tree_path=filter_tree_path)
 
1363
            return 'done', None
 
1364
        elif file_in_this:
 
1365
            # OTHER deleted the file
 
1366
            return 'delete', None
 
1367
        else:
 
1368
            raise AssertionError(
 
1369
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1370
                % (file_id,))
 
1371
 
 
1372
    def merge_contents(self, merge_hook_params):
 
1373
        """Fallback merge logic after user installed hooks."""
 
1374
        # This function is used in merge hooks as the fallback instance.
 
1375
        # Perhaps making this function and the functions it calls be a 
 
1376
        # a separate class would be better.
 
1377
        if merge_hook_params.winner == 'other':
1145
1378
            # 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"
 
1379
            return self._default_other_winner_merge(merge_hook_params)
 
1380
        elif merge_hook_params.is_file_merge():
 
1381
            # THIS and OTHER are both files, so text merge.  Either
 
1382
            # BASE is a file, or both converted to files, so at least we
 
1383
            # have agreement that output should be a file.
 
1384
            try:
 
1385
                self.text_merge(merge_hook_params.file_id,
 
1386
                    merge_hook_params.trans_id)
 
1387
            except errors.BinaryFile:
 
1388
                return 'not_applicable', None
 
1389
            return 'done', None
1175
1390
        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()
 
1391
            return 'not_applicable', None
1196
1392
 
1197
1393
    def get_lines(self, tree, file_id):
1198
1394
        """Return the lines in a file, or an empty list."""
1199
 
        if file_id in tree:
 
1395
        if tree.has_id(file_id):
1200
1396
            return tree.get_file(file_id).readlines()
1201
1397
        else:
1202
1398
            return []
1205
1401
        """Perform a three-way text merge on a file_id"""
1206
1402
        # it's possible that we got here with base as a different type.
1207
1403
        # if so, we just want two-way text conflicts.
1208
 
        if file_id in self.base_tree and \
 
1404
        if self.base_tree.has_id(file_id) and \
1209
1405
            self.base_tree.kind(file_id) == "file":
1210
1406
            base_lines = self.get_lines(self.base_tree, file_id)
1211
1407
        else:
1274
1470
        versioned = False
1275
1471
        file_group = []
1276
1472
        for suffix, tree, lines in data:
1277
 
            if file_id in tree:
 
1473
            if tree.has_id(file_id):
1278
1474
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1279
1475
                                               suffix, lines, filter_tree_path)
1280
1476
                file_group.append(trans_id)
1324
1520
        if winner == "this":
1325
1521
            executability = this_executable
1326
1522
        else:
1327
 
            if file_id in self.other_tree:
 
1523
            if self.other_tree.has_id(file_id):
1328
1524
                executability = other_executable
1329
 
            elif file_id in self.this_tree:
 
1525
            elif self.this_tree.has_id(file_id):
1330
1526
                executability = this_executable
1331
 
            elif file_id in self.base_tree:
 
1527
            elif self.base_tree_has_id(file_id):
1332
1528
                executability = base_executable
1333
1529
        if executability is not None:
1334
1530
            trans_id = self.tt.trans_id_file_id(file_id)
1515
1711
                other_rev_id=None,
1516
1712
                interesting_files=None,
1517
1713
                this_tree=None,
1518
 
                pb=progress.DummyProgress(),
 
1714
                pb=None,
1519
1715
                change_reporter=None):
1520
1716
    """Primary interface for merging.
1521
1717