/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
 
    registry,
29
30
    revision as _mod_revision,
30
31
    textfile,
31
32
    trace,
35
36
    ui,
36
37
    versionedfile
37
38
    )
 
39
from bzrlib.cleanup import OperationWithCleanups
38
40
from bzrlib.symbol_versioning import (
39
41
    deprecated_in,
40
42
    deprecated_method,
44
46
 
45
47
def transform_tree(from_tree, to_tree, interesting_ids=None):
46
48
    from_tree.lock_tree_write()
47
 
    try:
48
 
        merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
49
 
                    interesting_ids=interesting_ids, this_tree=from_tree)
50
 
    finally:
51
 
        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)
52
255
 
53
256
 
54
257
class Merger(object):
 
258
 
 
259
    hooks = MergeHooks()
 
260
 
55
261
    def __init__(self, this_branch, other_tree=None, base_tree=None,
56
262
                 this_tree=None, pb=None, change_reporter=None,
57
263
                 recurse='down', revision_graph=None):
72
278
        self.interesting_files = None
73
279
        self.show_base = False
74
280
        self.reprocess = False
75
 
        if pb is None:
76
 
            pb = progress.DummyProgress()
77
 
        self._pb = pb
 
281
        if pb is not None:
 
282
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
78
283
        self.pp = None
79
284
        self.recurse = recurse
80
285
        self.change_reporter = change_reporter
240
445
        if self.other_rev_id is None:
241
446
            other_basis_tree = self.revision_tree(self.other_basis)
242
447
            if other_basis_tree.has_changes(self.other_tree):
243
 
                raise WorkingTreeNotRevision(self.this_tree)
 
448
                raise errors.WorkingTreeNotRevision(self.this_tree)
244
449
            other_rev_id = self.other_basis
245
450
            self.other_tree = other_basis_tree
246
451
 
290
495
    def _add_parent(self):
291
496
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
292
497
        new_parent_trees = []
 
498
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
293
499
        for revision_id in new_parents:
294
500
            try:
295
501
                tree = self.revision_tree(revision_id)
297
503
                tree = None
298
504
            else:
299
505
                tree.lock_read()
 
506
                operation.add_cleanup(tree.unlock)
300
507
            new_parent_trees.append((revision_id, tree))
301
 
        try:
302
 
            self.this_tree.set_parent_trees(new_parent_trees,
303
 
                                            allow_leftmost_as_ghost=True)
304
 
        finally:
305
 
            for _revision_id, tree in new_parent_trees:
306
 
                if tree is not None:
307
 
                    tree.unlock()
 
508
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
308
509
 
309
510
    def set_other(self, other_revision, possible_transports=None):
310
511
        """Set the revision and tree to merge from.
433
634
                  'other_tree': self.other_tree,
434
635
                  'interesting_ids': self.interesting_ids,
435
636
                  'interesting_files': self.interesting_files,
436
 
                  'pp': self.pp,
 
637
                  'this_branch': self.this_branch,
437
638
                  'do_merge': False}
438
639
        if self.merge_type.requires_base:
439
640
            kwargs['base_tree'] = self.base_tree
457
658
        if self._is_criss_cross and getattr(self.merge_type,
458
659
                                            'supports_lca_trees', False):
459
660
            kwargs['lca_trees'] = self._lca_trees
460
 
        return self.merge_type(pb=self._pb,
 
661
        return self.merge_type(pb=None,
461
662
                               change_reporter=self.change_reporter,
462
663
                               **kwargs)
463
664
 
464
 
    def _do_merge_to(self, merge):
 
665
    def _do_merge_to(self):
 
666
        merge = self.make_merger()
465
667
        if self.other_branch is not None:
466
668
            self.other_branch.update_references(self.this_branch)
467
669
        merge.do_merge()
481
683
                    sub_tree.branch.repository.revision_tree(base_revision)
482
684
                sub_merge.base_rev_id = base_revision
483
685
                sub_merge.do_merge()
 
686
        return merge
484
687
 
485
688
    def do_merge(self):
 
689
        operation = OperationWithCleanups(self._do_merge_to)
486
690
        self.this_tree.lock_tree_write()
487
 
        try:
488
 
            if self.base_tree is not None:
489
 
                self.base_tree.lock_read()
490
 
            try:
491
 
                if self.other_tree is not None:
492
 
                    self.other_tree.lock_read()
493
 
                try:
494
 
                    merge = self.make_merger()
495
 
                    self._do_merge_to(merge)
496
 
                finally:
497
 
                    if self.other_tree is not None:
498
 
                        self.other_tree.unlock()
499
 
            finally:
500
 
                if self.base_tree is not None:
501
 
                    self.base_tree.unlock()
502
 
        finally:
503
 
            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()
504
699
        if len(merge.cooked_conflicts) == 0:
505
700
            if not self.ignore_zero and not trace.is_quiet():
506
701
                trace.note("All changes applied successfully.")
541
736
 
542
737
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
543
738
                 interesting_ids=None, reprocess=False, show_base=False,
544
 
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
 
739
                 pb=None, pp=None, change_reporter=None,
545
740
                 interesting_files=None, do_merge=True,
546
 
                 cherrypick=False, lca_trees=None):
 
741
                 cherrypick=False, lca_trees=None, this_branch=None):
547
742
        """Initialize the merger object and perform the merge.
548
743
 
549
744
        :param working_tree: The working tree to apply the merge to
550
745
        :param this_tree: The local tree in the merge operation
551
746
        :param base_tree: The common tree in the merge operation
552
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.
553
750
        :param interesting_ids: The file_ids of files that should be
554
751
            participate in the merge.  May not be combined with
555
752
            interesting_files.
556
753
        :param: reprocess If True, perform conflict-reduction processing.
557
754
        :param show_base: If True, show the base revision in text conflicts.
558
755
            (incompatible with reprocess)
559
 
        :param pb: A Progress bar
 
756
        :param pb: ignored
560
757
        :param pp: A ProgressPhase object
561
758
        :param change_reporter: An object that should report changes made
562
759
        :param interesting_files: The tree-relative paths of files that should
573
770
        if interesting_files is not None and interesting_ids is not None:
574
771
            raise ValueError(
575
772
                'specify either interesting_ids or interesting_files')
 
773
        if this_branch is None:
 
774
            this_branch = this_tree.branch
576
775
        self.interesting_ids = interesting_ids
577
776
        self.interesting_files = interesting_files
578
777
        self.this_tree = working_tree
579
778
        self.base_tree = base_tree
580
779
        self.other_tree = other_tree
 
780
        self.this_branch = this_branch
581
781
        self._raw_conflicts = []
582
782
        self.cooked_conflicts = []
583
783
        self.reprocess = reprocess
588
788
        # making sure we haven't missed any corner cases.
589
789
        # if lca_trees is None:
590
790
        #     self._lca_trees = [self.base_tree]
591
 
        self.pb = pb
592
 
        self.pp = pp
593
791
        self.change_reporter = change_reporter
594
792
        self.cherrypick = cherrypick
595
 
        if self.pp is None:
596
 
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
597
793
        if do_merge:
598
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")
599
799
 
600
800
    def do_merge(self):
 
801
        operation = OperationWithCleanups(self._do_merge)
601
802
        self.this_tree.lock_tree_write()
 
803
        operation.add_cleanup(self.this_tree.unlock)
602
804
        self.base_tree.lock_read()
 
805
        operation.add_cleanup(self.base_tree.unlock)
603
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)
604
816
        try:
605
 
            self.tt = transform.TreeTransform(self.this_tree, self.pb)
606
 
            try:
607
 
                self.pp.next_phase()
608
 
                self._compute_transform()
609
 
                self.pp.next_phase()
610
 
                results = self.tt.apply(no_conflicts=True)
611
 
                self.write_modified(results)
612
 
                try:
613
 
                    self.this_tree.add_conflicts(self.cooked_conflicts)
614
 
                except errors.UnsupportedOperation:
615
 
                    pass
616
 
            finally:
617
 
                self.tt.finalize()
618
 
        finally:
619
 
            self.other_tree.unlock()
620
 
            self.base_tree.unlock()
621
 
            self.this_tree.unlock()
622
 
            self.pb.clear()
 
817
            self.this_tree.add_conflicts(self.cooked_conflicts)
 
818
        except errors.UnsupportedOperation:
 
819
            pass
623
820
 
624
821
    def make_preview_transform(self):
 
822
        operation = OperationWithCleanups(self._make_preview_transform)
625
823
        self.base_tree.lock_read()
 
824
        operation.add_cleanup(self.base_tree.unlock)
626
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):
627
830
        self.tt = transform.TransformPreview(self.this_tree)
628
 
        try:
629
 
            self.pp.next_phase()
630
 
            self._compute_transform()
631
 
            self.pp.next_phase()
632
 
        finally:
633
 
            self.other_tree.unlock()
634
 
            self.base_tree.unlock()
635
 
            self.pb.clear()
 
831
        self._compute_transform()
636
832
        return self.tt
637
833
 
638
834
    def _compute_transform(self):
644
840
            resolver = self._lca_multi_way
645
841
        child_pb = ui.ui_factory.nested_progress_bar()
646
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]
647
846
            for num, (file_id, changed, parents3, names3,
648
847
                      executable3) in enumerate(entries):
649
848
                child_pb.update('Preparing file merge', num, len(entries))
650
849
                self._merge_names(file_id, parents3, names3, resolver=resolver)
651
850
                if changed:
652
 
                    file_status = self.merge_contents(file_id)
 
851
                    file_status = self._do_merge_contents(file_id)
653
852
                else:
654
853
                    file_status = 'unmodified'
655
854
                self._merge_executable(file_id,
657
856
        finally:
658
857
            child_pb.finished()
659
858
        self.fix_root()
660
 
        self.pp.next_phase()
661
859
        child_pb = ui.ui_factory.nested_progress_bar()
662
860
        try:
663
861
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
861
1059
                        continue
862
1060
                else:
863
1061
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
864
 
                # XXX: We need to handle kind == 'symlink'
865
1062
 
866
1063
            # If we have gotten this far, that means something has changed
867
1064
            result.append((file_id, content_changed,
889
1086
        other_root = self.tt.trans_id_file_id(other_root_file_id)
890
1087
        if other_root == self.tt.root:
891
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
892
1093
        try:
893
1094
            self.tt.final_kind(other_root)
 
1095
            other_root_is_present = True
894
1096
        except errors.NoSuchFile:
895
 
            return
896
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
897
 
            # the other tree's root is a non-root in the current tree
898
 
            return
899
 
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
900
 
        self.tt.cancel_creation(other_root)
901
 
        self.tt.cancel_versioning(other_root)
902
 
 
903
 
    def reparent_children(self, ie, target):
904
 
        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():
905
1104
            trans_id = self.tt.trans_id_file_id(child.file_id)
906
 
            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)
907
1121
 
908
1122
    def write_modified(self, results):
909
1123
        modified_hashes = {}
941
1155
    @staticmethod
942
1156
    def executable(tree, file_id):
943
1157
        """Determine the executability of a file-id (used as a key method)."""
944
 
        if file_id not in tree:
 
1158
        if not tree.has_id(file_id):
945
1159
            return None
946
1160
        if tree.kind(file_id) != "file":
947
1161
            return False
950
1164
    @staticmethod
951
1165
    def kind(tree, file_id):
952
1166
        """Determine the kind of a file-id (used as a key method)."""
953
 
        if file_id not in tree:
 
1167
        if not tree.has_id(file_id):
954
1168
            return None
955
1169
        return tree.kind(file_id)
956
1170
 
957
1171
    @staticmethod
958
1172
    def _three_way(base, other, this):
959
 
        #if base == other, either they all agree, or only THIS has changed.
960
1173
        if base == other:
 
1174
            # if 'base == other', either they all agree, or only 'this' has
 
1175
            # changed.
961
1176
            return 'this'
962
1177
        elif this not in (base, other):
 
1178
            # 'this' is neither 'base' nor 'other', so both sides changed
963
1179
            return 'conflict'
964
 
        # "Ambiguous clean merge" -- both sides have made the same change.
965
1180
        elif this == other:
 
1181
            # "Ambiguous clean merge" -- both sides have made the same change.
966
1182
            return "this"
967
 
        # this == base: only other has changed.
968
1183
        else:
 
1184
            # this == base: only other has changed.
969
1185
            return "other"
970
1186
 
971
1187
    @staticmethod
1015
1231
                # only has an lca value
1016
1232
                return 'other'
1017
1233
 
1018
 
        # At this point, the lcas disagree, and the tips disagree
 
1234
        # At this point, the lcas disagree, and the tip disagree
1019
1235
        return 'conflict'
1020
1236
 
1021
1237
    @staticmethod
 
1238
    @deprecated_method(deprecated_in((2, 2, 0)))
1022
1239
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1023
1240
        """Do a three-way test on a scalar.
1024
1241
        Return "this", "other" or "conflict", depending whether a value wins.
1039
1256
 
1040
1257
    def merge_names(self, file_id):
1041
1258
        def get_entry(tree):
1042
 
            if file_id in tree.inventory:
 
1259
            if tree.has_id(file_id):
1043
1260
                return tree.inventory[file_id]
1044
1261
            else:
1045
1262
                return None
1074
1291
                parent_id_winner = "other"
1075
1292
        if name_winner == "this" and parent_id_winner == "this":
1076
1293
            return
1077
 
        if name_winner == "conflict":
1078
 
            trans_id = self.tt.trans_id_file_id(file_id)
1079
 
            self._raw_conflicts.append(('name conflict', trans_id,
1080
 
                                        this_name, other_name))
1081
 
        if parent_id_winner == "conflict":
1082
 
            trans_id = self.tt.trans_id_file_id(file_id)
1083
 
            self._raw_conflicts.append(('parent conflict', trans_id,
1084
 
                                        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))
1085
1302
        if other_name is None:
1086
1303
            # it doesn't matter whether the result was 'other' or
1087
1304
            # 'conflict'-- if there's no 'other', we leave it alone.
1088
1305
            return
1089
 
        # if we get here, name_winner and parent_winner are set to safe values.
1090
 
        trans_id = self.tt.trans_id_file_id(file_id)
1091
1306
        parent_id = parents[self.winner_idx[parent_id_winner]]
1092
1307
        if parent_id is not None:
1093
 
            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.
1094
1310
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1095
 
                                parent_trans_id, trans_id)
 
1311
                                self.tt.trans_id_file_id(parent_id),
 
1312
                                self.tt.trans_id_file_id(file_id))
1096
1313
 
1097
 
    def merge_contents(self, file_id):
 
1314
    def _do_merge_contents(self, file_id):
1098
1315
        """Performs a merge on file_id contents."""
1099
1316
        def contents_pair(tree):
1100
1317
            if file_id not in tree:
1108
1325
                contents = None
1109
1326
            return kind, contents
1110
1327
 
1111
 
        def contents_conflict():
1112
 
            trans_id = self.tt.trans_id_file_id(file_id)
1113
 
            name = self.tt.final_name(trans_id)
1114
 
            parent_id = self.tt.final_parent(trans_id)
1115
 
            if file_id in self.this_tree.inventory:
1116
 
                self.tt.unversion_file(trans_id)
1117
 
                if file_id in self.this_tree:
1118
 
                    self.tt.delete_contents(trans_id)
1119
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1120
 
                                              set_version=True)
1121
 
            self._raw_conflicts.append(('contents conflict', file_group))
1122
 
 
1123
1328
        # See SPOT run.  run, SPOT, run.
1124
1329
        # So we're not QUITE repeating ourselves; we do tricky things with
1125
1330
        # file kind...
1141
1346
        if winner == 'this':
1142
1347
            # No interesting changes introduced by OTHER
1143
1348
            return "unmodified"
 
1349
        # We have a hypothetical conflict, but if we have files, then we
 
1350
        # can try to merge the content
1144
1351
        trans_id = self.tt.trans_id_file_id(file_id)
1145
 
        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':
1146
1440
            # OTHER is a straight winner, so replace this contents with other
1147
 
            file_in_this = file_id in self.this_tree
1148
 
            if file_in_this:
1149
 
                # Remove any existing contents
1150
 
                self.tt.delete_contents(trans_id)
1151
 
            if file_id in self.other_tree:
1152
 
                # OTHER changed the file
1153
 
                transform.create_from_tree(self.tt, trans_id,
1154
 
                                           self.other_tree, file_id)
1155
 
                if not file_in_this:
1156
 
                    self.tt.version_file(file_id, trans_id)
1157
 
                return "modified"
1158
 
            elif file_in_this:
1159
 
                # OTHER deleted the file
1160
 
                self.tt.unversion_file(trans_id)
1161
 
                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
1162
1452
        else:
1163
 
            # We have a hypothetical conflict, but if we have files, then we
1164
 
            # can try to merge the content
1165
 
            if this_pair[0] == 'file' and other_pair[0] == 'file':
1166
 
                # THIS and OTHER are both files, so text merge.  Either
1167
 
                # BASE is a file, or both converted to files, so at least we
1168
 
                # have agreement that output should be a file.
1169
 
                try:
1170
 
                    self.text_merge(file_id, trans_id)
1171
 
                except errors.BinaryFile:
1172
 
                    return contents_conflict()
1173
 
                if file_id not in self.this_tree:
1174
 
                    self.tt.version_file(file_id, trans_id)
1175
 
                try:
1176
 
                    self.tt.tree_kind(trans_id)
1177
 
                    self.tt.delete_contents(trans_id)
1178
 
                except errors.NoSuchFile:
1179
 
                    pass
1180
 
                return "modified"
1181
 
            else:
1182
 
                return contents_conflict()
 
1453
            return 'not_applicable', None
1183
1454
 
1184
1455
    def get_lines(self, tree, file_id):
1185
1456
        """Return the lines in a file, or an empty list."""
1186
 
        if file_id in tree:
 
1457
        if tree.has_id(file_id):
1187
1458
            return tree.get_file(file_id).readlines()
1188
1459
        else:
1189
1460
            return []
1192
1463
        """Perform a three-way text merge on a file_id"""
1193
1464
        # it's possible that we got here with base as a different type.
1194
1465
        # if so, we just want two-way text conflicts.
1195
 
        if file_id in self.base_tree and \
 
1466
        if self.base_tree.has_id(file_id) and \
1196
1467
            self.base_tree.kind(file_id) == "file":
1197
1468
            base_lines = self.get_lines(self.base_tree, file_id)
1198
1469
        else:
1244
1515
                ('THIS', self.this_tree, this_lines)]
1245
1516
        if not no_base:
1246
1517
            data.append(('BASE', self.base_tree, base_lines))
 
1518
 
 
1519
        # We need to use the actual path in the working tree of the file here,
 
1520
        # ignoring the conflict suffixes
 
1521
        wt = self.this_tree
 
1522
        if wt.supports_content_filtering():
 
1523
            try:
 
1524
                filter_tree_path = wt.id2path(file_id)
 
1525
            except errors.NoSuchId:
 
1526
                # file has been deleted
 
1527
                filter_tree_path = None
 
1528
        else:
 
1529
            # Skip the id2path lookup for older formats
 
1530
            filter_tree_path = None
 
1531
 
1247
1532
        versioned = False
1248
1533
        file_group = []
1249
1534
        for suffix, tree, lines in data:
1250
 
            if file_id in tree:
 
1535
            if tree.has_id(file_id):
1251
1536
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1252
 
                                               suffix, lines)
 
1537
                                               suffix, lines, filter_tree_path)
1253
1538
                file_group.append(trans_id)
1254
1539
                if set_version and not versioned:
1255
1540
                    self.tt.version_file(file_id, trans_id)
1257
1542
        return file_group
1258
1543
 
1259
1544
    def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1260
 
                       lines=None):
 
1545
                       lines=None, filter_tree_path=None):
1261
1546
        """Emit a single conflict file."""
1262
1547
        name = name + '.' + suffix
1263
1548
        trans_id = self.tt.create_path(name, parent_id)
1264
 
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines)
 
1549
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
 
1550
            filter_tree_path)
1265
1551
        return trans_id
1266
1552
 
1267
1553
    def merge_executable(self, file_id, file_status):
1296
1582
        if winner == "this":
1297
1583
            executability = this_executable
1298
1584
        else:
1299
 
            if file_id in self.other_tree:
 
1585
            if self.other_tree.has_id(file_id):
1300
1586
                executability = other_executable
1301
 
            elif file_id in self.this_tree:
 
1587
            elif self.this_tree.has_id(file_id):
1302
1588
                executability = this_executable
1303
 
            elif file_id in self.base_tree:
 
1589
            elif self.base_tree_has_id(file_id):
1304
1590
                executability = base_executable
1305
1591
        if executability is not None:
1306
1592
            trans_id = self.tt.trans_id_file_id(file_id)
1308
1594
 
1309
1595
    def cook_conflicts(self, fs_conflicts):
1310
1596
        """Convert all conflicts into a form that doesn't depend on trans_id"""
1311
 
        name_conflicts = {}
1312
1597
        self.cooked_conflicts.extend(transform.cook_conflicts(
1313
1598
                fs_conflicts, self.tt))
1314
1599
        fp = transform.FinalPaths(self.tt)
1315
1600
        for conflict in self._raw_conflicts:
1316
1601
            conflict_type = conflict[0]
1317
 
            if conflict_type in ('name conflict', 'parent conflict'):
1318
 
                trans_id = conflict[1]
1319
 
                conflict_args = conflict[2:]
1320
 
                if trans_id not in name_conflicts:
1321
 
                    name_conflicts[trans_id] = {}
1322
 
                transform.unique_add(name_conflicts[trans_id], conflict_type,
1323
 
                                     conflict_args)
1324
 
            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':
1325
1623
                for trans_id in conflict[1]:
1326
1624
                    file_id = self.tt.final_file_id(trans_id)
1327
1625
                    if file_id is not None:
1333
1631
                        break
1334
1632
                c = _mod_conflicts.Conflict.factory(conflict_type,
1335
1633
                                                    path=path, file_id=file_id)
1336
 
                self.cooked_conflicts.append(c)
1337
 
            if conflict_type == 'text conflict':
 
1634
            elif conflict_type == 'text conflict':
1338
1635
                trans_id = conflict[1]
1339
1636
                path = fp.get_path(trans_id)
1340
1637
                file_id = self.tt.final_file_id(trans_id)
1341
1638
                c = _mod_conflicts.Conflict.factory(conflict_type,
1342
1639
                                                    path=path, file_id=file_id)
1343
 
                self.cooked_conflicts.append(c)
1344
 
 
1345
 
        for trans_id, conflicts in name_conflicts.iteritems():
1346
 
            try:
1347
 
                this_parent, other_parent = conflicts['parent conflict']
1348
 
                if this_parent == other_parent:
1349
 
                    raise AssertionError()
1350
 
            except KeyError:
1351
 
                this_parent = other_parent = \
1352
 
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
1353
 
            try:
1354
 
                this_name, other_name = conflicts['name conflict']
1355
 
                if this_name == other_name:
1356
 
                    raise AssertionError()
1357
 
            except KeyError:
1358
 
                this_name = other_name = self.tt.final_name(trans_id)
1359
 
            other_path = fp.get_path(trans_id)
1360
 
            if this_parent is not None and this_name is not None:
1361
 
                this_parent_path = \
1362
 
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
1363
 
                this_path = osutils.pathjoin(this_parent_path, this_name)
1364
1640
            else:
1365
 
                this_path = "<deleted>"
1366
 
            file_id = self.tt.final_file_id(trans_id)
1367
 
            c = _mod_conflicts.Conflict.factory('path conflict', path=this_path,
1368
 
                                                conflict_path=other_path,
1369
 
                                                file_id=file_id)
 
1641
                raise AssertionError('bad conflict type: %r' % (conflict,))
1370
1642
            self.cooked_conflicts.append(c)
1371
1643
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1372
1644
 
1378
1650
    supports_reverse_cherrypick = False
1379
1651
    history_based = True
1380
1652
 
1381
 
    def _merged_lines(self, file_id):
1382
 
        """Generate the merged lines.
1383
 
        There is no distinction between lines that are meant to contain <<<<<<<
1384
 
        and conflicts.
1385
 
        """
1386
 
        if self.cherrypick:
1387
 
            base = self.base_tree
1388
 
        else:
1389
 
            base = None
1390
 
        plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
 
1653
    def _generate_merge_plan(self, file_id, base):
 
1654
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1391
1655
                                              base=base)
 
1656
 
 
1657
    def _merged_lines(self, file_id):
 
1658
        """Generate the merged lines.
 
1659
        There is no distinction between lines that are meant to contain <<<<<<<
 
1660
        and conflicts.
 
1661
        """
 
1662
        if self.cherrypick:
 
1663
            base = self.base_tree
 
1664
        else:
 
1665
            base = None
 
1666
        plan = self._generate_merge_plan(file_id, base)
1392
1667
        if 'merge' in debug.debug_flags:
1393
1668
            plan = list(plan)
1394
1669
            trans_id = self.tt.trans_id_file_id(file_id)
1395
1670
            name = self.tt.final_name(trans_id) + '.plan'
1396
 
            contents = ('%10s|%s' % l for l in plan)
 
1671
            contents = ('%11s|%s' % l for l in plan)
1397
1672
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1398
1673
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1399
1674
                                                 '>>>>>>> MERGE-SOURCE\n')
1400
 
        return textmerge.merge_lines(self.reprocess)
 
1675
        lines, conflicts = textmerge.merge_lines(self.reprocess)
 
1676
        if conflicts:
 
1677
            base_lines = textmerge.base_from_plan()
 
1678
        else:
 
1679
            base_lines = None
 
1680
        return lines, base_lines
1401
1681
 
1402
1682
    def text_merge(self, file_id, trans_id):
1403
1683
        """Perform a (weave) text merge for a given file and file-id.
1404
1684
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1405
1685
        and a conflict will be noted.
1406
1686
        """
1407
 
        lines, conflicts = self._merged_lines(file_id)
 
1687
        lines, base_lines = self._merged_lines(file_id)
1408
1688
        lines = list(lines)
1409
1689
        # Note we're checking whether the OUTPUT is binary in this case,
1410
1690
        # because we don't want to get into weave merge guts.
1411
1691
        textfile.check_text_lines(lines)
1412
1692
        self.tt.create_file(lines, trans_id)
1413
 
        if conflicts:
 
1693
        if base_lines is not None:
 
1694
            # Conflict
1414
1695
            self._raw_conflicts.append(('text conflict', trans_id))
1415
1696
            name = self.tt.final_name(trans_id)
1416
1697
            parent_id = self.tt.final_parent(trans_id)
1417
1698
            file_group = self._dump_conflicts(name, parent_id, file_id,
1418
 
                                              no_base=True)
 
1699
                                              no_base=False,
 
1700
                                              base_lines=base_lines)
1419
1701
            file_group.append(trans_id)
1420
1702
 
1421
1703
 
1422
1704
class LCAMerger(WeaveMerger):
1423
1705
 
1424
 
    def _merged_lines(self, file_id):
1425
 
        """Generate the merged lines.
1426
 
        There is no distinction between lines that are meant to contain <<<<<<<
1427
 
        and conflicts.
1428
 
        """
1429
 
        if self.cherrypick:
1430
 
            base = self.base_tree
1431
 
        else:
1432
 
            base = None
1433
 
        plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
 
1706
    def _generate_merge_plan(self, file_id, base):
 
1707
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1434
1708
                                                  base=base)
1435
 
        if 'merge' in debug.debug_flags:
1436
 
            plan = list(plan)
1437
 
            trans_id = self.tt.trans_id_file_id(file_id)
1438
 
            name = self.tt.final_name(trans_id) + '.plan'
1439
 
            contents = ('%10s|%s' % l for l in plan)
1440
 
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1441
 
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1442
 
                                                 '>>>>>>> MERGE-SOURCE\n')
1443
 
        return textmerge.merge_lines(self.reprocess)
1444
 
 
1445
1709
 
1446
1710
class Diff3Merger(Merge3Merger):
1447
1711
    """Three-way merger using external diff3 for text merging"""
1495
1759
                other_rev_id=None,
1496
1760
                interesting_files=None,
1497
1761
                this_tree=None,
1498
 
                pb=progress.DummyProgress(),
 
1762
                pb=None,
1499
1763
                change_reporter=None):
1500
1764
    """Primary interface for merging.
1501
1765