~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: John Arbash Meinel
  • Date: 2010-02-17 17:11:16 UTC
  • mfrom: (4797.2.17 2.1)
  • mto: (4797.2.18 2.1)
  • mto: This revision was merged to the branch mainline in revision 5055.
  • Revision ID: john@arbash-meinel.com-20100217171116-h7t9223ystbnx5h8
merge bzr.2.1 in preparation for NEWS entry.

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
19
19
    branch as _mod_branch,
20
20
    conflicts as _mod_conflicts,
21
21
    debug,
 
22
    decorators,
22
23
    errors,
23
24
    graph as _mod_graph,
 
25
    hooks,
24
26
    merge3,
25
27
    osutils,
26
28
    patiencediff,
27
29
    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 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_tree.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)
52
214
 
53
215
 
54
216
class Merger(object):
 
217
 
 
218
    hooks = MergeHooks()
 
219
 
55
220
    def __init__(self, this_branch, other_tree=None, base_tree=None,
56
221
                 this_tree=None, pb=None, change_reporter=None,
57
222
                 recurse='down', revision_graph=None):
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
 
290
455
    def _add_parent(self):
291
456
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
292
457
        new_parent_trees = []
 
458
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
293
459
        for revision_id in new_parents:
294
460
            try:
295
461
                tree = self.revision_tree(revision_id)
297
463
                tree = None
298
464
            else:
299
465
                tree.lock_read()
 
466
                operation.add_cleanup(tree.unlock)
300
467
            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()
 
468
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
308
469
 
309
470
    def set_other(self, other_revision, possible_transports=None):
310
471
        """Set the revision and tree to merge from.
433
594
                  'other_tree': self.other_tree,
434
595
                  'interesting_ids': self.interesting_ids,
435
596
                  'interesting_files': self.interesting_files,
436
 
                  'pp': self.pp,
 
597
                  'pp': self.pp, 'this_branch': self.this_branch,
437
598
                  'do_merge': False}
438
599
        if self.merge_type.requires_base:
439
600
            kwargs['base_tree'] = self.base_tree
461
622
                               change_reporter=self.change_reporter,
462
623
                               **kwargs)
463
624
 
464
 
    def _do_merge_to(self, merge):
 
625
    def _do_merge_to(self):
 
626
        merge = self.make_merger()
465
627
        if self.other_branch is not None:
466
628
            self.other_branch.update_references(self.this_branch)
467
629
        merge.do_merge()
481
643
                    sub_tree.branch.repository.revision_tree(base_revision)
482
644
                sub_merge.base_rev_id = base_revision
483
645
                sub_merge.do_merge()
 
646
        return merge
484
647
 
485
648
    def do_merge(self):
 
649
        operation = OperationWithCleanups(self._do_merge_to)
486
650
        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()
 
651
        operation.add_cleanup(self.this_tree.unlock)
 
652
        if self.base_tree is not None:
 
653
            self.base_tree.lock_read()
 
654
            operation.add_cleanup(self.base_tree.unlock)
 
655
        if self.other_tree is not None:
 
656
            self.other_tree.lock_read()
 
657
            operation.add_cleanup(self.other_tree.unlock)
 
658
        merge = operation.run_simple()
504
659
        if len(merge.cooked_conflicts) == 0:
505
660
            if not self.ignore_zero and not trace.is_quiet():
506
661
                trace.note("All changes applied successfully.")
543
698
                 interesting_ids=None, reprocess=False, show_base=False,
544
699
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
545
700
                 interesting_files=None, do_merge=True,
546
 
                 cherrypick=False, lca_trees=None):
 
701
                 cherrypick=False, lca_trees=None, this_branch=None):
547
702
        """Initialize the merger object and perform the merge.
548
703
 
549
704
        :param working_tree: The working tree to apply the merge to
550
705
        :param this_tree: The local tree in the merge operation
551
706
        :param base_tree: The common tree in the merge operation
552
707
        :param other_tree: The other tree to merge changes from
 
708
        :param this_branch: The branch associated with this_tree
553
709
        :param interesting_ids: The file_ids of files that should be
554
710
            participate in the merge.  May not be combined with
555
711
            interesting_files.
578
734
        self.this_tree = working_tree
579
735
        self.base_tree = base_tree
580
736
        self.other_tree = other_tree
 
737
        self.this_branch = this_branch
581
738
        self._raw_conflicts = []
582
739
        self.cooked_conflicts = []
583
740
        self.reprocess = reprocess
598
755
            self.do_merge()
599
756
 
600
757
    def do_merge(self):
 
758
        operation = OperationWithCleanups(self._do_merge)
 
759
        operation.add_cleanup(self.pb.clear)
601
760
        self.this_tree.lock_tree_write()
 
761
        operation.add_cleanup(self.this_tree.unlock)
602
762
        self.base_tree.lock_read()
 
763
        operation.add_cleanup(self.base_tree.unlock)
603
764
        self.other_tree.lock_read()
 
765
        operation.add_cleanup(self.other_tree.unlock)
 
766
        operation.run()
 
767
 
 
768
    def _do_merge(self, operation):
 
769
        self.tt = transform.TreeTransform(self.this_tree, self.pb)
 
770
        operation.add_cleanup(self.tt.finalize)
 
771
        self.pp.next_phase()
 
772
        self._compute_transform()
 
773
        self.pp.next_phase()
 
774
        results = self.tt.apply(no_conflicts=True)
 
775
        self.write_modified(results)
604
776
        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()
 
777
            self.this_tree.add_conflicts(self.cooked_conflicts)
 
778
        except errors.UnsupportedOperation:
 
779
            pass
623
780
 
624
781
    def make_preview_transform(self):
 
782
        operation = OperationWithCleanups(self._make_preview_transform)
 
783
        operation.add_cleanup(self.pb.clear)
625
784
        self.base_tree.lock_read()
 
785
        operation.add_cleanup(self.base_tree.unlock)
626
786
        self.other_tree.lock_read()
 
787
        operation.add_cleanup(self.other_tree.unlock)
 
788
        return operation.run_simple()
 
789
 
 
790
    def _make_preview_transform(self):
627
791
        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()
 
792
        self.pp.next_phase()
 
793
        self._compute_transform()
 
794
        self.pp.next_phase()
636
795
        return self.tt
637
796
 
638
797
    def _compute_transform(self):
644
803
            resolver = self._lca_multi_way
645
804
        child_pb = ui.ui_factory.nested_progress_bar()
646
805
        try:
 
806
            factories = Merger.hooks['merge_file_content']
 
807
            hooks = [factory(self) for factory in factories] + [self]
 
808
            self.active_hooks = [hook for hook in hooks if hook is not None]
647
809
            for num, (file_id, changed, parents3, names3,
648
810
                      executable3) in enumerate(entries):
649
811
                child_pb.update('Preparing file merge', num, len(entries))
650
812
                self._merge_names(file_id, parents3, names3, resolver=resolver)
651
813
                if changed:
652
 
                    file_status = self.merge_contents(file_id)
 
814
                    file_status = self._do_merge_contents(file_id)
653
815
                else:
654
816
                    file_status = 'unmodified'
655
817
                self._merge_executable(file_id,
893
1055
            self.tt.final_kind(other_root)
894
1056
        except errors.NoSuchFile:
895
1057
            return
896
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
1058
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
897
1059
            # the other tree's root is a non-root in the current tree
898
1060
            return
899
1061
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
941
1103
    @staticmethod
942
1104
    def executable(tree, file_id):
943
1105
        """Determine the executability of a file-id (used as a key method)."""
944
 
        if file_id not in tree:
 
1106
        if not tree.has_id(file_id):
945
1107
            return None
946
1108
        if tree.kind(file_id) != "file":
947
1109
            return False
950
1112
    @staticmethod
951
1113
    def kind(tree, file_id):
952
1114
        """Determine the kind of a file-id (used as a key method)."""
953
 
        if file_id not in tree:
 
1115
        if not tree.has_id(file_id):
954
1116
            return None
955
1117
        return tree.kind(file_id)
956
1118
 
1039
1201
 
1040
1202
    def merge_names(self, file_id):
1041
1203
        def get_entry(tree):
1042
 
            if file_id in tree.inventory:
 
1204
            if tree.has_id(file_id):
1043
1205
                return tree.inventory[file_id]
1044
1206
            else:
1045
1207
                return None
1094
1256
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1095
1257
                                parent_trans_id, trans_id)
1096
1258
 
1097
 
    def merge_contents(self, file_id):
 
1259
    def _do_merge_contents(self, file_id):
1098
1260
        """Performs a merge on file_id contents."""
1099
1261
        def contents_pair(tree):
1100
1262
            if file_id not in tree:
1108
1270
                contents = None
1109
1271
            return kind, contents
1110
1272
 
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
1273
        # See SPOT run.  run, SPOT, run.
1124
1274
        # So we're not QUITE repeating ourselves; we do tricky things with
1125
1275
        # file kind...
1141
1291
        if winner == 'this':
1142
1292
            # No interesting changes introduced by OTHER
1143
1293
            return "unmodified"
 
1294
        # We have a hypothetical conflict, but if we have files, then we
 
1295
        # can try to merge the content
1144
1296
        trans_id = self.tt.trans_id_file_id(file_id)
1145
 
        if winner == 'other':
 
1297
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1298
            other_pair[0], winner)
 
1299
        hooks = self.active_hooks
 
1300
        hook_status = 'not_applicable'
 
1301
        for hook in hooks:
 
1302
            hook_status, lines = hook.merge_contents(params)
 
1303
            if hook_status != 'not_applicable':
 
1304
                # Don't try any more hooks, this one applies.
 
1305
                break
 
1306
        result = "modified"
 
1307
        if hook_status == 'not_applicable':
 
1308
            # This is a contents conflict, because none of the available
 
1309
            # functions could merge it.
 
1310
            result = None
 
1311
            name = self.tt.final_name(trans_id)
 
1312
            parent_id = self.tt.final_parent(trans_id)
 
1313
            if self.this_tree.has_id(file_id):
 
1314
                self.tt.unversion_file(trans_id)
 
1315
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1316
                                              set_version=True)
 
1317
            self._raw_conflicts.append(('contents conflict', file_group))
 
1318
        elif hook_status == 'success':
 
1319
            self.tt.create_file(lines, trans_id)
 
1320
        elif hook_status == 'conflicted':
 
1321
            # XXX: perhaps the hook should be able to provide
 
1322
            # the BASE/THIS/OTHER files?
 
1323
            self.tt.create_file(lines, trans_id)
 
1324
            self._raw_conflicts.append(('text conflict', trans_id))
 
1325
            name = self.tt.final_name(trans_id)
 
1326
            parent_id = self.tt.final_parent(trans_id)
 
1327
            self._dump_conflicts(name, parent_id, file_id)
 
1328
        elif hook_status == 'delete':
 
1329
            self.tt.unversion_file(trans_id)
 
1330
            result = "deleted"
 
1331
        elif hook_status == 'done':
 
1332
            # The hook function did whatever it needs to do directly, no
 
1333
            # further action needed here.
 
1334
            pass
 
1335
        else:
 
1336
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1337
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1338
            self.tt.version_file(file_id, trans_id)
 
1339
        # The merge has been performed, so the old contents should not be
 
1340
        # retained.
 
1341
        try:
 
1342
            self.tt.delete_contents(trans_id)
 
1343
        except errors.NoSuchFile:
 
1344
            pass
 
1345
        return result
 
1346
 
 
1347
    def _default_other_winner_merge(self, merge_hook_params):
 
1348
        """Replace this contents with other."""
 
1349
        file_id = merge_hook_params.file_id
 
1350
        trans_id = merge_hook_params.trans_id
 
1351
        file_in_this = self.this_tree.has_id(file_id)
 
1352
        if self.other_tree.has_id(file_id):
 
1353
            # OTHER changed the file
 
1354
            wt = self.this_tree
 
1355
            if wt.supports_content_filtering():
 
1356
                # We get the path from the working tree if it exists.
 
1357
                # That fails though when OTHER is adding a file, so
 
1358
                # we fall back to the other tree to find the path if
 
1359
                # it doesn't exist locally.
 
1360
                try:
 
1361
                    filter_tree_path = wt.id2path(file_id)
 
1362
                except errors.NoSuchId:
 
1363
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1364
            else:
 
1365
                # Skip the id2path lookup for older formats
 
1366
                filter_tree_path = None
 
1367
            transform.create_from_tree(self.tt, trans_id,
 
1368
                             self.other_tree, file_id,
 
1369
                             filter_tree_path=filter_tree_path)
 
1370
            return 'done', None
 
1371
        elif file_in_this:
 
1372
            # OTHER deleted the file
 
1373
            return 'delete', None
 
1374
        else:
 
1375
            raise AssertionError(
 
1376
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1377
                % (file_id,))
 
1378
 
 
1379
    def merge_contents(self, merge_hook_params):
 
1380
        """Fallback merge logic after user installed hooks."""
 
1381
        # This function is used in merge hooks as the fallback instance.
 
1382
        # Perhaps making this function and the functions it calls be a 
 
1383
        # a separate class would be better.
 
1384
        if merge_hook_params.winner == 'other':
1146
1385
            # 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"
 
1386
            return self._default_other_winner_merge(merge_hook_params)
 
1387
        elif merge_hook_params.is_file_merge():
 
1388
            # THIS and OTHER are both files, so text merge.  Either
 
1389
            # BASE is a file, or both converted to files, so at least we
 
1390
            # have agreement that output should be a file.
 
1391
            try:
 
1392
                self.text_merge(merge_hook_params.file_id,
 
1393
                    merge_hook_params.trans_id)
 
1394
            except errors.BinaryFile:
 
1395
                return 'not_applicable', None
 
1396
            return 'done', None
1162
1397
        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()
 
1398
            return 'not_applicable', None
1183
1399
 
1184
1400
    def get_lines(self, tree, file_id):
1185
1401
        """Return the lines in a file, or an empty list."""
1186
 
        if file_id in tree:
 
1402
        if tree.has_id(file_id):
1187
1403
            return tree.get_file(file_id).readlines()
1188
1404
        else:
1189
1405
            return []
1192
1408
        """Perform a three-way text merge on a file_id"""
1193
1409
        # it's possible that we got here with base as a different type.
1194
1410
        # if so, we just want two-way text conflicts.
1195
 
        if file_id in self.base_tree and \
 
1411
        if self.base_tree.has_id(file_id) and \
1196
1412
            self.base_tree.kind(file_id) == "file":
1197
1413
            base_lines = self.get_lines(self.base_tree, file_id)
1198
1414
        else:
1244
1460
                ('THIS', self.this_tree, this_lines)]
1245
1461
        if not no_base:
1246
1462
            data.append(('BASE', self.base_tree, base_lines))
 
1463
 
 
1464
        # We need to use the actual path in the working tree of the file here,
 
1465
        # ignoring the conflict suffixes
 
1466
        wt = self.this_tree
 
1467
        if wt.supports_content_filtering():
 
1468
            try:
 
1469
                filter_tree_path = wt.id2path(file_id)
 
1470
            except errors.NoSuchId:
 
1471
                # file has been deleted
 
1472
                filter_tree_path = None
 
1473
        else:
 
1474
            # Skip the id2path lookup for older formats
 
1475
            filter_tree_path = None
 
1476
 
1247
1477
        versioned = False
1248
1478
        file_group = []
1249
1479
        for suffix, tree, lines in data:
1250
 
            if file_id in tree:
 
1480
            if tree.has_id(file_id):
1251
1481
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1252
 
                                               suffix, lines)
 
1482
                                               suffix, lines, filter_tree_path)
1253
1483
                file_group.append(trans_id)
1254
1484
                if set_version and not versioned:
1255
1485
                    self.tt.version_file(file_id, trans_id)
1257
1487
        return file_group
1258
1488
 
1259
1489
    def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1260
 
                       lines=None):
 
1490
                       lines=None, filter_tree_path=None):
1261
1491
        """Emit a single conflict file."""
1262
1492
        name = name + '.' + suffix
1263
1493
        trans_id = self.tt.create_path(name, parent_id)
1264
 
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines)
 
1494
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
 
1495
            filter_tree_path)
1265
1496
        return trans_id
1266
1497
 
1267
1498
    def merge_executable(self, file_id, file_status):
1296
1527
        if winner == "this":
1297
1528
            executability = this_executable
1298
1529
        else:
1299
 
            if file_id in self.other_tree:
 
1530
            if self.other_tree.has_id(file_id):
1300
1531
                executability = other_executable
1301
 
            elif file_id in self.this_tree:
 
1532
            elif self.this_tree.has_id(file_id):
1302
1533
                executability = this_executable
1303
 
            elif file_id in self.base_tree:
 
1534
            elif self.base_tree_has_id(file_id):
1304
1535
                executability = base_executable
1305
1536
        if executability is not None:
1306
1537
            trans_id = self.tt.trans_id_file_id(file_id)
1378
1609
    supports_reverse_cherrypick = False
1379
1610
    history_based = True
1380
1611
 
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,
 
1612
    def _generate_merge_plan(self, file_id, base):
 
1613
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1391
1614
                                              base=base)
 
1615
 
 
1616
    def _merged_lines(self, file_id):
 
1617
        """Generate the merged lines.
 
1618
        There is no distinction between lines that are meant to contain <<<<<<<
 
1619
        and conflicts.
 
1620
        """
 
1621
        if self.cherrypick:
 
1622
            base = self.base_tree
 
1623
        else:
 
1624
            base = None
 
1625
        plan = self._generate_merge_plan(file_id, base)
1392
1626
        if 'merge' in debug.debug_flags:
1393
1627
            plan = list(plan)
1394
1628
            trans_id = self.tt.trans_id_file_id(file_id)
1395
1629
            name = self.tt.final_name(trans_id) + '.plan'
1396
 
            contents = ('%10s|%s' % l for l in plan)
 
1630
            contents = ('%11s|%s' % l for l in plan)
1397
1631
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1398
1632
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1399
1633
                                                 '>>>>>>> MERGE-SOURCE\n')
1400
 
        return textmerge.merge_lines(self.reprocess)
 
1634
        lines, conflicts = textmerge.merge_lines(self.reprocess)
 
1635
        if conflicts:
 
1636
            base_lines = textmerge.base_from_plan()
 
1637
        else:
 
1638
            base_lines = None
 
1639
        return lines, base_lines
1401
1640
 
1402
1641
    def text_merge(self, file_id, trans_id):
1403
1642
        """Perform a (weave) text merge for a given file and file-id.
1404
1643
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1405
1644
        and a conflict will be noted.
1406
1645
        """
1407
 
        lines, conflicts = self._merged_lines(file_id)
 
1646
        lines, base_lines = self._merged_lines(file_id)
1408
1647
        lines = list(lines)
1409
1648
        # Note we're checking whether the OUTPUT is binary in this case,
1410
1649
        # because we don't want to get into weave merge guts.
1411
1650
        textfile.check_text_lines(lines)
1412
1651
        self.tt.create_file(lines, trans_id)
1413
 
        if conflicts:
 
1652
        if base_lines is not None:
 
1653
            # Conflict
1414
1654
            self._raw_conflicts.append(('text conflict', trans_id))
1415
1655
            name = self.tt.final_name(trans_id)
1416
1656
            parent_id = self.tt.final_parent(trans_id)
1417
1657
            file_group = self._dump_conflicts(name, parent_id, file_id,
1418
 
                                              no_base=True)
 
1658
                                              no_base=False,
 
1659
                                              base_lines=base_lines)
1419
1660
            file_group.append(trans_id)
1420
1661
 
1421
1662
 
1422
1663
class LCAMerger(WeaveMerger):
1423
1664
 
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,
 
1665
    def _generate_merge_plan(self, file_id, base):
 
1666
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1434
1667
                                                  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
1668
 
1446
1669
class Diff3Merger(Merge3Merger):
1447
1670
    """Three-way merger using external diff3 for text merging"""