~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-01-14 00:01:32 UTC
  • mfrom: (4957.1.1 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100114000132-3p3rabnonjw3gzqb
(jam) Merge bzr.stable, bringing in bug fixes #175839, #504390

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,
21
20
    conflicts as _mod_conflicts,
22
21
    debug,
23
 
    decorators,
24
22
    errors,
25
23
    graph as _mod_graph,
26
 
    hooks,
27
24
    merge3,
28
25
    osutils,
29
26
    patiencediff,
 
27
    progress,
30
28
    revision as _mod_revision,
31
29
    textfile,
32
30
    trace,
36
34
    ui,
37
35
    versionedfile
38
36
    )
39
 
from bzrlib.cleanup import OperationWithCleanups
40
37
from bzrlib.symbol_versioning import (
41
38
    deprecated_in,
42
39
    deprecated_method,
46
43
 
47
44
def transform_tree(from_tree, to_tree, interesting_ids=None):
48
45
    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)
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)
 
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()
214
51
 
215
52
 
216
53
class Merger(object):
217
 
 
218
 
    hooks = MergeHooks()
219
 
 
220
54
    def __init__(self, this_branch, other_tree=None, base_tree=None,
221
55
                 this_tree=None, pb=None, change_reporter=None,
222
56
                 recurse='down', revision_graph=None):
237
71
        self.interesting_files = None
238
72
        self.show_base = False
239
73
        self.reprocess = False
240
 
        if pb is not None:
241
 
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
 
74
        if pb is None:
 
75
            pb = progress.DummyProgress()
 
76
        self._pb = pb
242
77
        self.pp = None
243
78
        self.recurse = recurse
244
79
        self.change_reporter = change_reporter
454
289
    def _add_parent(self):
455
290
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
456
291
        new_parent_trees = []
457
 
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
458
292
        for revision_id in new_parents:
459
293
            try:
460
294
                tree = self.revision_tree(revision_id)
462
296
                tree = None
463
297
            else:
464
298
                tree.lock_read()
465
 
                operation.add_cleanup(tree.unlock)
466
299
            new_parent_trees.append((revision_id, tree))
467
 
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
 
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()
468
307
 
469
308
    def set_other(self, other_revision, possible_transports=None):
470
309
        """Set the revision and tree to merge from.
593
432
                  'other_tree': self.other_tree,
594
433
                  'interesting_ids': self.interesting_ids,
595
434
                  'interesting_files': self.interesting_files,
596
 
                  'this_branch': self.this_branch,
 
435
                  'pp': self.pp,
597
436
                  'do_merge': False}
598
437
        if self.merge_type.requires_base:
599
438
            kwargs['base_tree'] = self.base_tree
617
456
        if self._is_criss_cross and getattr(self.merge_type,
618
457
                                            'supports_lca_trees', False):
619
458
            kwargs['lca_trees'] = self._lca_trees
620
 
        return self.merge_type(pb=None,
 
459
        return self.merge_type(pb=self._pb,
621
460
                               change_reporter=self.change_reporter,
622
461
                               **kwargs)
623
462
 
624
 
    def _do_merge_to(self):
625
 
        merge = self.make_merger()
 
463
    def _do_merge_to(self, merge):
626
464
        if self.other_branch is not None:
627
465
            self.other_branch.update_references(self.this_branch)
628
466
        merge.do_merge()
642
480
                    sub_tree.branch.repository.revision_tree(base_revision)
643
481
                sub_merge.base_rev_id = base_revision
644
482
                sub_merge.do_merge()
645
 
        return merge
646
483
 
647
484
    def do_merge(self):
648
 
        operation = OperationWithCleanups(self._do_merge_to)
649
485
        self.this_tree.lock_tree_write()
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()
 
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()
658
503
        if len(merge.cooked_conflicts) == 0:
659
504
            if not self.ignore_zero and not trace.is_quiet():
660
505
                trace.note("All changes applied successfully.")
695
540
 
696
541
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
697
542
                 interesting_ids=None, reprocess=False, show_base=False,
698
 
                 pb=None, pp=None, change_reporter=None,
 
543
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
699
544
                 interesting_files=None, do_merge=True,
700
 
                 cherrypick=False, lca_trees=None, this_branch=None):
 
545
                 cherrypick=False, lca_trees=None):
701
546
        """Initialize the merger object and perform the merge.
702
547
 
703
548
        :param working_tree: The working tree to apply the merge to
704
549
        :param this_tree: The local tree in the merge operation
705
550
        :param base_tree: The common tree in the merge operation
706
551
        :param other_tree: The other tree to merge changes from
707
 
        :param this_branch: The branch associated with this_tree
708
552
        :param interesting_ids: The file_ids of files that should be
709
553
            participate in the merge.  May not be combined with
710
554
            interesting_files.
711
555
        :param: reprocess If True, perform conflict-reduction processing.
712
556
        :param show_base: If True, show the base revision in text conflicts.
713
557
            (incompatible with reprocess)
714
 
        :param pb: ignored
 
558
        :param pb: A Progress bar
715
559
        :param pp: A ProgressPhase object
716
560
        :param change_reporter: An object that should report changes made
717
561
        :param interesting_files: The tree-relative paths of files that should
733
577
        self.this_tree = working_tree
734
578
        self.base_tree = base_tree
735
579
        self.other_tree = other_tree
736
 
        self.this_branch = this_branch
737
580
        self._raw_conflicts = []
738
581
        self.cooked_conflicts = []
739
582
        self.reprocess = reprocess
744
587
        # making sure we haven't missed any corner cases.
745
588
        # if lca_trees is None:
746
589
        #     self._lca_trees = [self.base_tree]
 
590
        self.pb = pb
 
591
        self.pp = pp
747
592
        self.change_reporter = change_reporter
748
593
        self.cherrypick = cherrypick
 
594
        if self.pp is None:
 
595
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
749
596
        if do_merge:
750
597
            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")
755
598
 
756
599
    def do_merge(self):
757
 
        operation = OperationWithCleanups(self._do_merge)
758
600
        self.this_tree.lock_tree_write()
759
 
        operation.add_cleanup(self.this_tree.unlock)
760
601
        self.base_tree.lock_read()
761
 
        operation.add_cleanup(self.base_tree.unlock)
762
602
        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)
772
603
        try:
773
 
            self.this_tree.add_conflicts(self.cooked_conflicts)
774
 
        except errors.UnsupportedOperation:
775
 
            pass
 
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()
776
622
 
777
623
    def make_preview_transform(self):
778
 
        operation = OperationWithCleanups(self._make_preview_transform)
779
624
        self.base_tree.lock_read()
780
 
        operation.add_cleanup(self.base_tree.unlock)
781
625
        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):
786
626
        self.tt = transform.TransformPreview(self.this_tree)
787
 
        self._compute_transform()
 
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()
788
635
        return self.tt
789
636
 
790
637
    def _compute_transform(self):
796
643
            resolver = self._lca_multi_way
797
644
        child_pb = ui.ui_factory.nested_progress_bar()
798
645
        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]
802
646
            for num, (file_id, changed, parents3, names3,
803
647
                      executable3) in enumerate(entries):
804
648
                child_pb.update('Preparing file merge', num, len(entries))
805
649
                self._merge_names(file_id, parents3, names3, resolver=resolver)
806
650
                if changed:
807
 
                    file_status = self._do_merge_contents(file_id)
 
651
                    file_status = self.merge_contents(file_id)
808
652
                else:
809
653
                    file_status = 'unmodified'
810
654
                self._merge_executable(file_id,
812
656
        finally:
813
657
            child_pb.finished()
814
658
        self.fix_root()
 
659
        self.pp.next_phase()
815
660
        child_pb = ui.ui_factory.nested_progress_bar()
816
661
        try:
817
662
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
1047
892
            self.tt.final_kind(other_root)
1048
893
        except errors.NoSuchFile:
1049
894
            return
1050
 
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
 
895
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1051
896
            # the other tree's root is a non-root in the current tree
1052
897
            return
1053
898
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
1095
940
    @staticmethod
1096
941
    def executable(tree, file_id):
1097
942
        """Determine the executability of a file-id (used as a key method)."""
1098
 
        if not tree.has_id(file_id):
 
943
        if file_id not in tree:
1099
944
            return None
1100
945
        if tree.kind(file_id) != "file":
1101
946
            return False
1104
949
    @staticmethod
1105
950
    def kind(tree, file_id):
1106
951
        """Determine the kind of a file-id (used as a key method)."""
1107
 
        if not tree.has_id(file_id):
 
952
        if file_id not in tree:
1108
953
            return None
1109
954
        return tree.kind(file_id)
1110
955
 
1193
1038
 
1194
1039
    def merge_names(self, file_id):
1195
1040
        def get_entry(tree):
1196
 
            if tree.has_id(file_id):
 
1041
            if file_id in tree.inventory:
1197
1042
                return tree.inventory[file_id]
1198
1043
            else:
1199
1044
                return None
1248
1093
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1249
1094
                                parent_trans_id, trans_id)
1250
1095
 
1251
 
    def _do_merge_contents(self, file_id):
 
1096
    def merge_contents(self, file_id):
1252
1097
        """Performs a merge on file_id contents."""
1253
1098
        def contents_pair(tree):
1254
1099
            if file_id not in tree:
1262
1107
                contents = None
1263
1108
            return kind, contents
1264
1109
 
 
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
 
1265
1122
        # See SPOT run.  run, SPOT, run.
1266
1123
        # So we're not QUITE repeating ourselves; we do tricky things with
1267
1124
        # file kind...
1283
1140
        if winner == 'this':
1284
1141
            # No interesting changes introduced by OTHER
1285
1142
            return "unmodified"
1286
 
        # We have a hypothetical conflict, but if we have files, then we
1287
 
        # can try to merge the content
1288
1143
        trans_id = self.tt.trans_id_file_id(file_id)
1289
 
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1290
 
            other_pair[0], winner)
1291
 
        hooks = self.active_hooks
1292
 
        hook_status = 'not_applicable'
1293
 
        for hook in hooks:
1294
 
            hook_status, lines = hook.merge_contents(params)
1295
 
            if hook_status != 'not_applicable':
1296
 
                # Don't try any more hooks, this one applies.
1297
 
                break
1298
 
        result = "modified"
1299
 
        if hook_status == 'not_applicable':
1300
 
            # This is a contents conflict, because none of the available
1301
 
            # functions could merge it.
1302
 
            result = None
1303
 
            name = self.tt.final_name(trans_id)
1304
 
            parent_id = self.tt.final_parent(trans_id)
1305
 
            if self.this_tree.has_id(file_id):
1306
 
                self.tt.unversion_file(trans_id)
1307
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1308
 
                                              set_version=True)
1309
 
            self._raw_conflicts.append(('contents conflict', file_group))
1310
 
        elif hook_status == 'success':
1311
 
            self.tt.create_file(lines, trans_id)
1312
 
        elif hook_status == 'conflicted':
1313
 
            # XXX: perhaps the hook should be able to provide
1314
 
            # the BASE/THIS/OTHER files?
1315
 
            self.tt.create_file(lines, trans_id)
1316
 
            self._raw_conflicts.append(('text conflict', trans_id))
1317
 
            name = self.tt.final_name(trans_id)
1318
 
            parent_id = self.tt.final_parent(trans_id)
1319
 
            self._dump_conflicts(name, parent_id, file_id)
1320
 
        elif hook_status == 'delete':
1321
 
            self.tt.unversion_file(trans_id)
1322
 
            result = "deleted"
1323
 
        elif hook_status == 'done':
1324
 
            # The hook function did whatever it needs to do directly, no
1325
 
            # further action needed here.
1326
 
            pass
1327
 
        else:
1328
 
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1329
 
        if not self.this_tree.has_id(file_id) and result == "modified":
1330
 
            self.tt.version_file(file_id, trans_id)
1331
 
        # The merge has been performed, so the old contents should not be
1332
 
        # retained.
1333
 
        try:
1334
 
            self.tt.delete_contents(trans_id)
1335
 
        except errors.NoSuchFile:
1336
 
            pass
1337
 
        return result
1338
 
 
1339
 
    def _default_other_winner_merge(self, merge_hook_params):
1340
 
        """Replace this contents with other."""
1341
 
        file_id = merge_hook_params.file_id
1342
 
        trans_id = merge_hook_params.trans_id
1343
 
        file_in_this = self.this_tree.has_id(file_id)
1344
 
        if self.other_tree.has_id(file_id):
1345
 
            # OTHER changed the file
1346
 
            wt = self.this_tree
1347
 
            if wt.supports_content_filtering():
1348
 
                # We get the path from the working tree if it exists.
1349
 
                # That fails though when OTHER is adding a file, so
1350
 
                # we fall back to the other tree to find the path if
1351
 
                # it doesn't exist locally.
1352
 
                try:
1353
 
                    filter_tree_path = wt.id2path(file_id)
1354
 
                except errors.NoSuchId:
1355
 
                    filter_tree_path = self.other_tree.id2path(file_id)
1356
 
            else:
1357
 
                # Skip the id2path lookup for older formats
1358
 
                filter_tree_path = None
1359
 
            transform.create_from_tree(self.tt, trans_id,
1360
 
                             self.other_tree, file_id,
1361
 
                             filter_tree_path=filter_tree_path)
1362
 
            return 'done', None
1363
 
        elif file_in_this:
1364
 
            # OTHER deleted the file
1365
 
            return 'delete', None
1366
 
        else:
1367
 
            raise AssertionError(
1368
 
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1369
 
                % (file_id,))
1370
 
 
1371
 
    def merge_contents(self, merge_hook_params):
1372
 
        """Fallback merge logic after user installed hooks."""
1373
 
        # This function is used in merge hooks as the fallback instance.
1374
 
        # Perhaps making this function and the functions it calls be a 
1375
 
        # a separate class would be better.
1376
 
        if merge_hook_params.winner == 'other':
 
1144
        if winner == 'other':
1377
1145
            # OTHER is a straight winner, so replace this contents with other
1378
 
            return self._default_other_winner_merge(merge_hook_params)
1379
 
        elif merge_hook_params.is_file_merge():
1380
 
            # THIS and OTHER are both files, so text merge.  Either
1381
 
            # BASE is a file, or both converted to files, so at least we
1382
 
            # have agreement that output should be a file.
1383
 
            try:
1384
 
                self.text_merge(merge_hook_params.file_id,
1385
 
                    merge_hook_params.trans_id)
1386
 
            except errors.BinaryFile:
1387
 
                return 'not_applicable', None
1388
 
            return 'done', None
 
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"
1389
1175
        else:
1390
 
            return 'not_applicable', None
 
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
1196
 
1392
1197
    def get_lines(self, tree, file_id):
1393
1198
        """Return the lines in a file, or an empty list."""
1394
 
        if tree.has_id(file_id):
 
1199
        if file_id in tree:
1395
1200
            return tree.get_file(file_id).readlines()
1396
1201
        else:
1397
1202
            return []
1400
1205
        """Perform a three-way text merge on a file_id"""
1401
1206
        # it's possible that we got here with base as a different type.
1402
1207
        # if so, we just want two-way text conflicts.
1403
 
        if self.base_tree.has_id(file_id) and \
 
1208
        if file_id in self.base_tree and \
1404
1209
            self.base_tree.kind(file_id) == "file":
1405
1210
            base_lines = self.get_lines(self.base_tree, file_id)
1406
1211
        else:
1469
1274
        versioned = False
1470
1275
        file_group = []
1471
1276
        for suffix, tree, lines in data:
1472
 
            if tree.has_id(file_id):
 
1277
            if file_id in tree:
1473
1278
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1474
1279
                                               suffix, lines, filter_tree_path)
1475
1280
                file_group.append(trans_id)
1519
1324
        if winner == "this":
1520
1325
            executability = this_executable
1521
1326
        else:
1522
 
            if self.other_tree.has_id(file_id):
 
1327
            if file_id in self.other_tree:
1523
1328
                executability = other_executable
1524
 
            elif self.this_tree.has_id(file_id):
 
1329
            elif file_id in self.this_tree:
1525
1330
                executability = this_executable
1526
 
            elif self.base_tree_has_id(file_id):
 
1331
            elif file_id in self.base_tree:
1527
1332
                executability = base_executable
1528
1333
        if executability is not None:
1529
1334
            trans_id = self.tt.trans_id_file_id(file_id)
1710
1515
                other_rev_id=None,
1711
1516
                interesting_files=None,
1712
1517
                this_tree=None,
1713
 
                pb=None,
 
1518
                pb=progress.DummyProgress(),
1714
1519
                change_reporter=None):
1715
1520
    """Primary interface for merging.
1716
1521