~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Andrew Bennetts
  • Date: 2010-02-12 04:33:05 UTC
  • mfrom: (5031 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5032.
  • Revision ID: andrew.bennetts@canonical.com-20100212043305-ujdbsdoviql2t7i3
Merge lp:bzr

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
30
    progress,
28
 
    registry,
29
31
    revision as _mod_revision,
30
32
    textfile,
31
33
    trace,
51
53
        from_tree.unlock()
52
54
 
53
55
 
 
56
class MergeHooks(hooks.Hooks):
 
57
 
 
58
    def __init__(self):
 
59
        hooks.Hooks.__init__(self)
 
60
        self.create_hook(hooks.HookPoint('merge_file_content',
 
61
            "Called with a bzrlib.merge.Merger object to create a per file "
 
62
            "merge object when starting a merge. "
 
63
            "Should return either None or a subclass of "
 
64
            "``bzrlib.merge.AbstractPerFileMerger``. "
 
65
            "Such objects will then be called per file "
 
66
            "that needs to be merged (including when one "
 
67
            "side has deleted the file and the other has changed it). "
 
68
            "See the AbstractPerFileMerger API docs for details on how it is "
 
69
            "used by merge.",
 
70
            (2, 1), None))
 
71
 
 
72
 
 
73
class AbstractPerFileMerger(object):
 
74
    """PerFileMerger objects are used by plugins extending merge for bzrlib.
 
75
 
 
76
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
77
    
 
78
    :ivar merger: The Merge3Merger performing the merge.
 
79
    """
 
80
 
 
81
    def __init__(self, merger):
 
82
        """Create a PerFileMerger for use with merger."""
 
83
        self.merger = merger
 
84
 
 
85
    def merge_contents(self, merge_params):
 
86
        """Attempt to merge the contents of a single file.
 
87
        
 
88
        :param merge_params: A bzrlib.merge.MergeHookParams
 
89
        :return : A tuple of (status, chunks), where status is one of
 
90
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
 
91
            is 'success' or 'conflicted', then chunks should be an iterable of
 
92
            strings for the new file contents.
 
93
        """
 
94
        return ('not applicable', None)
 
95
 
 
96
 
 
97
class ConfigurableFileMerger(AbstractPerFileMerger):
 
98
    """Merge individual files when configured via a .conf file.
 
99
 
 
100
    This is a base class for concrete custom file merging logic. Concrete
 
101
    classes should implement ``merge_text``.
 
102
 
 
103
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
104
    
 
105
    :ivar affected_files: The configured file paths to merge.
 
106
 
 
107
    :cvar name_prefix: The prefix to use when looking up configuration
 
108
        details. <name_prefix>_merge_files describes the files targeted by the
 
109
        hook for example.
 
110
        
 
111
    :cvar default_files: The default file paths to merge when no configuration
 
112
        is present.
 
113
    """
 
114
 
 
115
    name_prefix = None
 
116
    default_files = None
 
117
 
 
118
    def __init__(self, merger):
 
119
        super(ConfigurableFileMerger, self).__init__(merger)
 
120
        self.affected_files = None
 
121
        self.default_files = self.__class__.default_files or []
 
122
        self.name_prefix = self.__class__.name_prefix
 
123
        if self.name_prefix is None:
 
124
            raise ValueError("name_prefix must be set.")
 
125
 
 
126
    def filename_matches_config(self, params):
 
127
        """Check whether the file should call the merge hook.
 
128
 
 
129
        <name_prefix>_merge_files configuration variable is a list of files
 
130
        that should use the hook.
 
131
        """
 
132
        affected_files = self.affected_files
 
133
        if affected_files is None:
 
134
            config = self.merger.this_tree.branch.get_config()
 
135
            # Until bzr provides a better policy for caching the config, we
 
136
            # just add the part we're interested in to the params to avoid
 
137
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
138
            # branch.conf).
 
139
            config_key = self.name_prefix + '_merge_files'
 
140
            affected_files = config.get_user_option_as_list(config_key)
 
141
            if affected_files is None:
 
142
                # If nothing was specified in the config, use the default.
 
143
                affected_files = self.default_files
 
144
            self.affected_files = affected_files
 
145
        if affected_files:
 
146
            filename = self.merger.this_tree.id2path(params.file_id)
 
147
            if filename in affected_files:
 
148
                return True
 
149
        return False
 
150
 
 
151
    def merge_contents(self, params):
 
152
        """Merge the contents of a single file."""
 
153
        # First, check whether this custom merge logic should be used.  We
 
154
        # expect most files should not be merged by this handler.
 
155
        if (
 
156
            # OTHER is a straight winner, rely on default merge.
 
157
            params.winner == 'other' or
 
158
            # THIS and OTHER aren't both files.
 
159
            not params.is_file_merge() or
 
160
            # The filename isn't listed in the 'NAME_merge_files' config
 
161
            # option.
 
162
            not self.filename_matches_config(params)):
 
163
            return 'not_applicable', None
 
164
        return self.merge_text(params)
 
165
 
 
166
    def merge_text(self, params):
 
167
        """Merge the byte contents of a single file.
 
168
 
 
169
        This is called after checking that the merge should be performed in
 
170
        merge_contents, and it should behave as per
 
171
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
172
        """
 
173
        raise NotImplementedError(self.merge_text)
 
174
 
 
175
 
 
176
class MergeHookParams(object):
 
177
    """Object holding parameters passed to merge_file_content hooks.
 
178
 
 
179
    There are some fields hooks can access:
 
180
 
 
181
    :ivar file_id: the file ID of the file being merged
 
182
    :ivar trans_id: the transform ID for the merge of this file
 
183
    :ivar this_kind: kind of file_id in 'this' tree
 
184
    :ivar other_kind: kind of file_id in 'other' tree
 
185
    :ivar winner: one of 'this', 'other', 'conflict'
 
186
    """
 
187
 
 
188
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
189
            winner):
 
190
        self._merger = merger
 
191
        self.file_id = file_id
 
192
        self.trans_id = trans_id
 
193
        self.this_kind = this_kind
 
194
        self.other_kind = other_kind
 
195
        self.winner = winner
 
196
 
 
197
    def is_file_merge(self):
 
198
        """True if this_kind and other_kind are both 'file'."""
 
199
        return self.this_kind == 'file' and self.other_kind == 'file'
 
200
 
 
201
    @decorators.cachedproperty
 
202
    def base_lines(self):
 
203
        """The lines of the 'base' version of the file."""
 
204
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
205
 
 
206
    @decorators.cachedproperty
 
207
    def this_lines(self):
 
208
        """The lines of the 'this' version of the file."""
 
209
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
210
 
 
211
    @decorators.cachedproperty
 
212
    def other_lines(self):
 
213
        """The lines of the 'other' version of the file."""
 
214
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
215
 
 
216
 
54
217
class Merger(object):
 
218
 
 
219
    hooks = MergeHooks()
 
220
 
55
221
    def __init__(self, this_branch, other_tree=None, base_tree=None,
56
222
                 this_tree=None, pb=None, change_reporter=None,
57
223
                 recurse='down', revision_graph=None):
72
238
        self.interesting_files = None
73
239
        self.show_base = False
74
240
        self.reprocess = False
75
 
        if pb is None:
76
 
            pb = progress.DummyProgress()
77
 
        self._pb = pb
 
241
        if pb is not None:
 
242
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
78
243
        self.pp = None
79
244
        self.recurse = recurse
80
245
        self.change_reporter = change_reporter
240
405
        if self.other_rev_id is None:
241
406
            other_basis_tree = self.revision_tree(self.other_basis)
242
407
            if other_basis_tree.has_changes(self.other_tree):
243
 
                raise WorkingTreeNotRevision(self.this_tree)
 
408
                raise errors.WorkingTreeNotRevision(self.this_tree)
244
409
            other_rev_id = self.other_basis
245
410
            self.other_tree = other_basis_tree
246
411
 
433
598
                  'other_tree': self.other_tree,
434
599
                  'interesting_ids': self.interesting_ids,
435
600
                  'interesting_files': self.interesting_files,
436
 
                  'pp': self.pp,
 
601
                  'this_branch': self.this_branch,
437
602
                  'do_merge': False}
438
603
        if self.merge_type.requires_base:
439
604
            kwargs['base_tree'] = self.base_tree
457
622
        if self._is_criss_cross and getattr(self.merge_type,
458
623
                                            'supports_lca_trees', False):
459
624
            kwargs['lca_trees'] = self._lca_trees
460
 
        return self.merge_type(pb=self._pb,
 
625
        return self.merge_type(pb=None,
461
626
                               change_reporter=self.change_reporter,
462
627
                               **kwargs)
463
628
 
541
706
 
542
707
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
543
708
                 interesting_ids=None, reprocess=False, show_base=False,
544
 
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
 
709
                 pb=None, pp=None, change_reporter=None,
545
710
                 interesting_files=None, do_merge=True,
546
 
                 cherrypick=False, lca_trees=None):
 
711
                 cherrypick=False, lca_trees=None, this_branch=None):
547
712
        """Initialize the merger object and perform the merge.
548
713
 
549
714
        :param working_tree: The working tree to apply the merge to
550
715
        :param this_tree: The local tree in the merge operation
551
716
        :param base_tree: The common tree in the merge operation
552
717
        :param other_tree: The other tree to merge changes from
 
718
        :param this_branch: The branch associated with this_tree
553
719
        :param interesting_ids: The file_ids of files that should be
554
720
            participate in the merge.  May not be combined with
555
721
            interesting_files.
556
722
        :param: reprocess If True, perform conflict-reduction processing.
557
723
        :param show_base: If True, show the base revision in text conflicts.
558
724
            (incompatible with reprocess)
559
 
        :param pb: A Progress bar
 
725
        :param pb: ignored
560
726
        :param pp: A ProgressPhase object
561
727
        :param change_reporter: An object that should report changes made
562
728
        :param interesting_files: The tree-relative paths of files that should
578
744
        self.this_tree = working_tree
579
745
        self.base_tree = base_tree
580
746
        self.other_tree = other_tree
 
747
        self.this_branch = this_branch
581
748
        self._raw_conflicts = []
582
749
        self.cooked_conflicts = []
583
750
        self.reprocess = reprocess
588
755
        # making sure we haven't missed any corner cases.
589
756
        # if lca_trees is None:
590
757
        #     self._lca_trees = [self.base_tree]
591
 
        self.pb = pb
592
 
        self.pp = pp
593
758
        self.change_reporter = change_reporter
594
759
        self.cherrypick = cherrypick
595
 
        if self.pp is None:
596
 
            self.pp = progress.ProgressPhase("Merge phase", 3, self.pb)
597
760
        if do_merge:
598
761
            self.do_merge()
 
762
        if pp is not None:
 
763
            warnings.warn("pp argument to Merge3Merger is deprecated")
 
764
        if pb is not None:
 
765
            warnings.warn("pb argument to Merge3Merger is deprecated")
599
766
 
600
767
    def do_merge(self):
601
768
        self.this_tree.lock_tree_write()
602
769
        self.base_tree.lock_read()
603
770
        self.other_tree.lock_read()
604
771
        try:
605
 
            self.tt = transform.TreeTransform(self.this_tree, self.pb)
 
772
            self.tt = transform.TreeTransform(self.this_tree, None)
606
773
            try:
607
 
                self.pp.next_phase()
608
774
                self._compute_transform()
609
 
                self.pp.next_phase()
610
775
                results = self.tt.apply(no_conflicts=True)
611
776
                self.write_modified(results)
612
777
                try:
619
784
            self.other_tree.unlock()
620
785
            self.base_tree.unlock()
621
786
            self.this_tree.unlock()
622
 
            self.pb.clear()
623
787
 
624
788
    def make_preview_transform(self):
625
789
        self.base_tree.lock_read()
626
790
        self.other_tree.lock_read()
627
791
        self.tt = transform.TransformPreview(self.this_tree)
628
792
        try:
629
 
            self.pp.next_phase()
630
793
            self._compute_transform()
631
 
            self.pp.next_phase()
632
794
        finally:
633
795
            self.other_tree.unlock()
634
796
            self.base_tree.unlock()
635
 
            self.pb.clear()
636
797
        return self.tt
637
798
 
638
799
    def _compute_transform(self):
644
805
            resolver = self._lca_multi_way
645
806
        child_pb = ui.ui_factory.nested_progress_bar()
646
807
        try:
 
808
            factories = Merger.hooks['merge_file_content']
 
809
            hooks = [factory(self) for factory in factories] + [self]
 
810
            self.active_hooks = [hook for hook in hooks if hook is not None]
647
811
            for num, (file_id, changed, parents3, names3,
648
812
                      executable3) in enumerate(entries):
649
813
                child_pb.update('Preparing file merge', num, len(entries))
650
814
                self._merge_names(file_id, parents3, names3, resolver=resolver)
651
815
                if changed:
652
 
                    file_status = self.merge_contents(file_id)
 
816
                    file_status = self._do_merge_contents(file_id)
653
817
                else:
654
818
                    file_status = 'unmodified'
655
819
                self._merge_executable(file_id,
657
821
        finally:
658
822
            child_pb.finished()
659
823
        self.fix_root()
660
 
        self.pp.next_phase()
661
824
        child_pb = ui.ui_factory.nested_progress_bar()
662
825
        try:
663
826
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
893
1056
            self.tt.final_kind(other_root)
894
1057
        except errors.NoSuchFile:
895
1058
            return
896
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
1059
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
897
1060
            # the other tree's root is a non-root in the current tree
898
1061
            return
899
1062
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
941
1104
    @staticmethod
942
1105
    def executable(tree, file_id):
943
1106
        """Determine the executability of a file-id (used as a key method)."""
944
 
        if file_id not in tree:
 
1107
        if not tree.has_id(file_id):
945
1108
            return None
946
1109
        if tree.kind(file_id) != "file":
947
1110
            return False
950
1113
    @staticmethod
951
1114
    def kind(tree, file_id):
952
1115
        """Determine the kind of a file-id (used as a key method)."""
953
 
        if file_id not in tree:
 
1116
        if not tree.has_id(file_id):
954
1117
            return None
955
1118
        return tree.kind(file_id)
956
1119
 
1039
1202
 
1040
1203
    def merge_names(self, file_id):
1041
1204
        def get_entry(tree):
1042
 
            if file_id in tree.inventory:
 
1205
            if tree.has_id(file_id):
1043
1206
                return tree.inventory[file_id]
1044
1207
            else:
1045
1208
                return None
1094
1257
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1095
1258
                                parent_trans_id, trans_id)
1096
1259
 
1097
 
    def merge_contents(self, file_id):
 
1260
    def _do_merge_contents(self, file_id):
1098
1261
        """Performs a merge on file_id contents."""
1099
1262
        def contents_pair(tree):
1100
1263
            if file_id not in tree:
1108
1271
                contents = None
1109
1272
            return kind, contents
1110
1273
 
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
1274
        # See SPOT run.  run, SPOT, run.
1124
1275
        # So we're not QUITE repeating ourselves; we do tricky things with
1125
1276
        # file kind...
1141
1292
        if winner == 'this':
1142
1293
            # No interesting changes introduced by OTHER
1143
1294
            return "unmodified"
 
1295
        # We have a hypothetical conflict, but if we have files, then we
 
1296
        # can try to merge the content
1144
1297
        trans_id = self.tt.trans_id_file_id(file_id)
1145
 
        if winner == 'other':
 
1298
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1299
            other_pair[0], winner)
 
1300
        hooks = self.active_hooks
 
1301
        hook_status = 'not_applicable'
 
1302
        for hook in hooks:
 
1303
            hook_status, lines = hook.merge_contents(params)
 
1304
            if hook_status != 'not_applicable':
 
1305
                # Don't try any more hooks, this one applies.
 
1306
                break
 
1307
        result = "modified"
 
1308
        if hook_status == 'not_applicable':
 
1309
            # This is a contents conflict, because none of the available
 
1310
            # functions could merge it.
 
1311
            result = None
 
1312
            name = self.tt.final_name(trans_id)
 
1313
            parent_id = self.tt.final_parent(trans_id)
 
1314
            if self.this_tree.has_id(file_id):
 
1315
                self.tt.unversion_file(trans_id)
 
1316
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1317
                                              set_version=True)
 
1318
            self._raw_conflicts.append(('contents conflict', file_group))
 
1319
        elif hook_status == 'success':
 
1320
            self.tt.create_file(lines, trans_id)
 
1321
        elif hook_status == 'conflicted':
 
1322
            # XXX: perhaps the hook should be able to provide
 
1323
            # the BASE/THIS/OTHER files?
 
1324
            self.tt.create_file(lines, trans_id)
 
1325
            self._raw_conflicts.append(('text conflict', trans_id))
 
1326
            name = self.tt.final_name(trans_id)
 
1327
            parent_id = self.tt.final_parent(trans_id)
 
1328
            self._dump_conflicts(name, parent_id, file_id)
 
1329
        elif hook_status == 'delete':
 
1330
            self.tt.unversion_file(trans_id)
 
1331
            result = "deleted"
 
1332
        elif hook_status == 'done':
 
1333
            # The hook function did whatever it needs to do directly, no
 
1334
            # further action needed here.
 
1335
            pass
 
1336
        else:
 
1337
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1338
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1339
            self.tt.version_file(file_id, trans_id)
 
1340
        # The merge has been performed, so the old contents should not be
 
1341
        # retained.
 
1342
        try:
 
1343
            self.tt.delete_contents(trans_id)
 
1344
        except errors.NoSuchFile:
 
1345
            pass
 
1346
        return result
 
1347
 
 
1348
    def _default_other_winner_merge(self, merge_hook_params):
 
1349
        """Replace this contents with other."""
 
1350
        file_id = merge_hook_params.file_id
 
1351
        trans_id = merge_hook_params.trans_id
 
1352
        file_in_this = self.this_tree.has_id(file_id)
 
1353
        if self.other_tree.has_id(file_id):
 
1354
            # OTHER changed the file
 
1355
            wt = self.this_tree
 
1356
            if wt.supports_content_filtering():
 
1357
                # We get the path from the working tree if it exists.
 
1358
                # That fails though when OTHER is adding a file, so
 
1359
                # we fall back to the other tree to find the path if
 
1360
                # it doesn't exist locally.
 
1361
                try:
 
1362
                    filter_tree_path = wt.id2path(file_id)
 
1363
                except errors.NoSuchId:
 
1364
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1365
            else:
 
1366
                # Skip the id2path lookup for older formats
 
1367
                filter_tree_path = None
 
1368
            transform.create_from_tree(self.tt, trans_id,
 
1369
                             self.other_tree, file_id,
 
1370
                             filter_tree_path=filter_tree_path)
 
1371
            return 'done', None
 
1372
        elif file_in_this:
 
1373
            # OTHER deleted the file
 
1374
            return 'delete', None
 
1375
        else:
 
1376
            raise AssertionError(
 
1377
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1378
                % (file_id,))
 
1379
 
 
1380
    def merge_contents(self, merge_hook_params):
 
1381
        """Fallback merge logic after user installed hooks."""
 
1382
        # This function is used in merge hooks as the fallback instance.
 
1383
        # Perhaps making this function and the functions it calls be a 
 
1384
        # a separate class would be better.
 
1385
        if merge_hook_params.winner == 'other':
1146
1386
            # 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"
 
1387
            return self._default_other_winner_merge(merge_hook_params)
 
1388
        elif merge_hook_params.is_file_merge():
 
1389
            # THIS and OTHER are both files, so text merge.  Either
 
1390
            # BASE is a file, or both converted to files, so at least we
 
1391
            # have agreement that output should be a file.
 
1392
            try:
 
1393
                self.text_merge(merge_hook_params.file_id,
 
1394
                    merge_hook_params.trans_id)
 
1395
            except errors.BinaryFile:
 
1396
                return 'not_applicable', None
 
1397
            return 'done', None
1162
1398
        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()
 
1399
            return 'not_applicable', None
1183
1400
 
1184
1401
    def get_lines(self, tree, file_id):
1185
1402
        """Return the lines in a file, or an empty list."""
1186
 
        if file_id in tree:
 
1403
        if tree.has_id(file_id):
1187
1404
            return tree.get_file(file_id).readlines()
1188
1405
        else:
1189
1406
            return []
1192
1409
        """Perform a three-way text merge on a file_id"""
1193
1410
        # it's possible that we got here with base as a different type.
1194
1411
        # if so, we just want two-way text conflicts.
1195
 
        if file_id in self.base_tree and \
 
1412
        if self.base_tree.has_id(file_id) and \
1196
1413
            self.base_tree.kind(file_id) == "file":
1197
1414
            base_lines = self.get_lines(self.base_tree, file_id)
1198
1415
        else:
1244
1461
                ('THIS', self.this_tree, this_lines)]
1245
1462
        if not no_base:
1246
1463
            data.append(('BASE', self.base_tree, base_lines))
 
1464
 
 
1465
        # We need to use the actual path in the working tree of the file here,
 
1466
        # ignoring the conflict suffixes
 
1467
        wt = self.this_tree
 
1468
        if wt.supports_content_filtering():
 
1469
            try:
 
1470
                filter_tree_path = wt.id2path(file_id)
 
1471
            except errors.NoSuchId:
 
1472
                # file has been deleted
 
1473
                filter_tree_path = None
 
1474
        else:
 
1475
            # Skip the id2path lookup for older formats
 
1476
            filter_tree_path = None
 
1477
 
1247
1478
        versioned = False
1248
1479
        file_group = []
1249
1480
        for suffix, tree, lines in data:
1250
 
            if file_id in tree:
 
1481
            if tree.has_id(file_id):
1251
1482
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1252
 
                                               suffix, lines)
 
1483
                                               suffix, lines, filter_tree_path)
1253
1484
                file_group.append(trans_id)
1254
1485
                if set_version and not versioned:
1255
1486
                    self.tt.version_file(file_id, trans_id)
1257
1488
        return file_group
1258
1489
 
1259
1490
    def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1260
 
                       lines=None):
 
1491
                       lines=None, filter_tree_path=None):
1261
1492
        """Emit a single conflict file."""
1262
1493
        name = name + '.' + suffix
1263
1494
        trans_id = self.tt.create_path(name, parent_id)
1264
 
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines)
 
1495
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
 
1496
            filter_tree_path)
1265
1497
        return trans_id
1266
1498
 
1267
1499
    def merge_executable(self, file_id, file_status):
1296
1528
        if winner == "this":
1297
1529
            executability = this_executable
1298
1530
        else:
1299
 
            if file_id in self.other_tree:
 
1531
            if self.other_tree.has_id(file_id):
1300
1532
                executability = other_executable
1301
 
            elif file_id in self.this_tree:
 
1533
            elif self.this_tree.has_id(file_id):
1302
1534
                executability = this_executable
1303
 
            elif file_id in self.base_tree:
 
1535
            elif self.base_tree_has_id(file_id):
1304
1536
                executability = base_executable
1305
1537
        if executability is not None:
1306
1538
            trans_id = self.tt.trans_id_file_id(file_id)
1378
1610
    supports_reverse_cherrypick = False
1379
1611
    history_based = True
1380
1612
 
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,
 
1613
    def _generate_merge_plan(self, file_id, base):
 
1614
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1391
1615
                                              base=base)
 
1616
 
 
1617
    def _merged_lines(self, file_id):
 
1618
        """Generate the merged lines.
 
1619
        There is no distinction between lines that are meant to contain <<<<<<<
 
1620
        and conflicts.
 
1621
        """
 
1622
        if self.cherrypick:
 
1623
            base = self.base_tree
 
1624
        else:
 
1625
            base = None
 
1626
        plan = self._generate_merge_plan(file_id, base)
1392
1627
        if 'merge' in debug.debug_flags:
1393
1628
            plan = list(plan)
1394
1629
            trans_id = self.tt.trans_id_file_id(file_id)
1395
1630
            name = self.tt.final_name(trans_id) + '.plan'
1396
 
            contents = ('%10s|%s' % l for l in plan)
 
1631
            contents = ('%11s|%s' % l for l in plan)
1397
1632
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1398
1633
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1399
1634
                                                 '>>>>>>> MERGE-SOURCE\n')
1400
 
        return textmerge.merge_lines(self.reprocess)
 
1635
        lines, conflicts = textmerge.merge_lines(self.reprocess)
 
1636
        if conflicts:
 
1637
            base_lines = textmerge.base_from_plan()
 
1638
        else:
 
1639
            base_lines = None
 
1640
        return lines, base_lines
1401
1641
 
1402
1642
    def text_merge(self, file_id, trans_id):
1403
1643
        """Perform a (weave) text merge for a given file and file-id.
1404
1644
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1405
1645
        and a conflict will be noted.
1406
1646
        """
1407
 
        lines, conflicts = self._merged_lines(file_id)
 
1647
        lines, base_lines = self._merged_lines(file_id)
1408
1648
        lines = list(lines)
1409
1649
        # Note we're checking whether the OUTPUT is binary in this case,
1410
1650
        # because we don't want to get into weave merge guts.
1411
1651
        textfile.check_text_lines(lines)
1412
1652
        self.tt.create_file(lines, trans_id)
1413
 
        if conflicts:
 
1653
        if base_lines is not None:
 
1654
            # Conflict
1414
1655
            self._raw_conflicts.append(('text conflict', trans_id))
1415
1656
            name = self.tt.final_name(trans_id)
1416
1657
            parent_id = self.tt.final_parent(trans_id)
1417
1658
            file_group = self._dump_conflicts(name, parent_id, file_id,
1418
 
                                              no_base=True)
 
1659
                                              no_base=False,
 
1660
                                              base_lines=base_lines)
1419
1661
            file_group.append(trans_id)
1420
1662
 
1421
1663
 
1422
1664
class LCAMerger(WeaveMerger):
1423
1665
 
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,
 
1666
    def _generate_merge_plan(self, file_id, base):
 
1667
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1434
1668
                                                  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
1669
 
1446
1670
class Diff3Merger(Merge3Merger):
1447
1671
    """Three-way merger using external diff3 for text merging"""
1495
1719
                other_rev_id=None,
1496
1720
                interesting_files=None,
1497
1721
                this_tree=None,
1498
 
                pb=progress.DummyProgress(),
 
1722
                pb=None,
1499
1723
                change_reporter=None):
1500
1724
    """Primary interface for merging.
1501
1725