~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-01-29 11:48:10 UTC
  • mfrom: (4988.1.2 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100129114810-pizbq0hfw5wctdaq
(jam) Merge 2.1.0rc2 into bzr.dev, including per-file merge hook

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
57
57
    def __init__(self):
58
58
        hooks.Hooks.__init__(self)
59
59
        self.create_hook(hooks.HookPoint('merge_file_content',
60
 
            "Called when file content needs to be merged (including when one "
61
 
            "side has deleted the file and the other has changed it)."
62
 
            "merge_file_content is called with a "
63
 
            "bzrlib.merge.MergeHookParams. The function should return a tuple "
64
 
            "of (status, lines), where status is one of 'not_applicable', "
65
 
            "'success', 'conflicted', or 'delete'.  If status is success or "
66
 
            "conflicted, then lines should be an iterable of strings of the "
67
 
            "new file contents.",
 
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.",
68
69
            (2, 1), None))
69
70
 
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
    :ivar affected_files: The configured file paths to merge.
 
103
    :cvar name_prefix: The prefix to use when looking up configuration
 
104
        details.
 
105
    :cvar default_files: The default file paths to merge when no configuration
 
106
        is present.
 
107
    """
 
108
 
 
109
    name_prefix = None
 
110
    default_files = None
 
111
 
 
112
    def __init__(self, merger):
 
113
        super(ConfigurableFileMerger, self).__init__(merger)
 
114
        self.affected_files = None
 
115
        self.default_files = self.__class__.default_files or []
 
116
        self.name_prefix = self.__class__.name_prefix
 
117
        if self.name_prefix is None:
 
118
            raise ValueError("name_prefix must be set.")
 
119
 
 
120
    def filename_matches_config(self, params):
 
121
        affected_files = self.affected_files
 
122
        if affected_files is None:
 
123
            config = self.merger.this_tree.branch.get_config()
 
124
            # Until bzr provides a better policy for caching the config, we
 
125
            # just add the part we're interested in to the params to avoid
 
126
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
127
            # branch.conf).
 
128
            config_key = self.name_prefix + '_merge_files'
 
129
            affected_files = config.get_user_option_as_list(config_key)
 
130
            if affected_files is None:
 
131
                # If nothing was specified in the config, use the default.
 
132
                affected_files = self.default_files
 
133
            self.affected_files = affected_files
 
134
        if affected_files:
 
135
            filename = self.merger.this_tree.id2path(params.file_id)
 
136
            if filename in affected_files:
 
137
                return True
 
138
        return False
 
139
 
 
140
    def merge_contents(self, params):
 
141
        """Merge the contents of a single file."""
 
142
        # First, check whether this custom merge logic should be used.  We
 
143
        # expect most files should not be merged by this handler.
 
144
        if (
 
145
            # OTHER is a straight winner, rely on default merge.
 
146
            params.winner == 'other' or
 
147
            # THIS and OTHER aren't both files.
 
148
            not params.is_file_merge() or
 
149
            # The filename isn't listed in the 'NAME_merge_files' config
 
150
            # option.
 
151
            not self.filename_matches_config(params)):
 
152
            return 'not_applicable', None
 
153
        return self.merge_text(self, params)
 
154
 
 
155
    def merge_text(self, params):
 
156
        """Merge the byte contents of a single file.
 
157
 
 
158
        This is called after checking that the merge should be performed in
 
159
        merge_contents, and it should behave as per
 
160
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
161
        """
 
162
        raise NotImplementedError(self.merge_text)
 
163
 
 
164
 
71
165
class MergeHookParams(object):
72
166
    """Object holding parameters passed to merge_file_content hooks.
73
167
 
74
 
    There are 3 fields hooks can access:
 
168
    There are some fields hooks can access:
75
169
 
76
 
    :ivar merger: the Merger object
77
170
    :ivar file_id: the file ID of the file being merged
78
171
    :ivar trans_id: the transform ID for the merge of this file
79
172
    :ivar this_kind: kind of file_id in 'this' tree
83
176
 
84
177
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
85
178
            winner):
86
 
        self.merger = merger
 
179
        self._merger = merger
87
180
        self.file_id = file_id
88
181
        self.trans_id = trans_id
89
182
        self.this_kind = this_kind
97
190
    @decorators.cachedproperty
98
191
    def base_lines(self):
99
192
        """The lines of the 'base' version of the file."""
100
 
        return self.merger.get_lines(self.merger.base_tree, self.file_id)
 
193
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
101
194
 
102
195
    @decorators.cachedproperty
103
196
    def this_lines(self):
104
197
        """The lines of the 'this' version of the file."""
105
 
        return self.merger.get_lines(self.merger.this_tree, self.file_id)
 
198
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
106
199
 
107
200
    @decorators.cachedproperty
108
201
    def other_lines(self):
109
202
        """The lines of the 'other' version of the file."""
110
 
        return self.merger.get_lines(self.merger.other_tree, self.file_id)
 
203
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
111
204
 
112
205
 
113
206
class Merger(object):
708
801
            resolver = self._lca_multi_way
709
802
        child_pb = ui.ui_factory.nested_progress_bar()
710
803
        try:
 
804
            factories = Merger.hooks['merge_file_content']
 
805
            hooks = [factory(self) for factory in factories] + [self]
 
806
            self.active_hooks = [hook for hook in hooks if hook is not None]
711
807
            for num, (file_id, changed, parents3, names3,
712
808
                      executable3) in enumerate(entries):
713
809
                child_pb.update('Preparing file merge', num, len(entries))
714
810
                self._merge_names(file_id, parents3, names3, resolver=resolver)
715
811
                if changed:
716
 
                    file_status = self.merge_contents(file_id)
 
812
                    file_status = self._do_merge_contents(file_id)
717
813
                else:
718
814
                    file_status = 'unmodified'
719
815
                self._merge_executable(file_id,
1158
1254
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1159
1255
                                parent_trans_id, trans_id)
1160
1256
 
1161
 
    def merge_contents(self, file_id):
 
1257
    def _do_merge_contents(self, file_id):
1162
1258
        """Performs a merge on file_id contents."""
1163
1259
        def contents_pair(tree):
1164
1260
            if file_id not in tree:
1198
1294
        trans_id = self.tt.trans_id_file_id(file_id)
1199
1295
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1200
1296
            other_pair[0], winner)
1201
 
        hooks = Merger.hooks['merge_file_content']
1202
 
        hooks = list(hooks) + [self.default_text_merge]
 
1297
        hooks = self.active_hooks
1203
1298
        hook_status = 'not_applicable'
1204
1299
        for hook in hooks:
1205
 
            hook_status, lines = hook(params)
 
1300
            hook_status, lines = hook.merge_contents(params)
1206
1301
            if hook_status != 'not_applicable':
1207
1302
                # Don't try any more hooks, this one applies.
1208
1303
                break
1279
1374
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1280
1375
                % (file_id,))
1281
1376
 
1282
 
    def default_text_merge(self, merge_hook_params):
 
1377
    def merge_contents(self, merge_hook_params):
 
1378
        """Fallback merge logic after user installed hooks."""
 
1379
        # This function is used in merge hooks as the fallback instance.
 
1380
        # Perhaps making this function and the functions it calls be a 
 
1381
        # a separate class would be better.
1283
1382
        if merge_hook_params.winner == 'other':
1284
1383
            # OTHER is a straight winner, so replace this contents with other
1285
1384
            return self._default_other_winner_merge(merge_hook_params)