~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-04 16:06:36 UTC
  • mfrom: (5007 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5023.
  • Revision ID: john@arbash-meinel.com-20100204160636-xqeuwz8bwt8bbts4
Merge bzr.dev 5007, resolve conflict, update NEWS

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
    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
 
71
175
class MergeHookParams(object):
72
176
    """Object holding parameters passed to merge_file_content hooks.
73
177
 
74
 
    There are 3 fields hooks can access:
 
178
    There are some fields hooks can access:
75
179
 
76
 
    :ivar merger: the Merger object
77
180
    :ivar file_id: the file ID of the file being merged
78
181
    :ivar trans_id: the transform ID for the merge of this file
79
182
    :ivar this_kind: kind of file_id in 'this' tree
83
186
 
84
187
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
85
188
            winner):
86
 
        self.merger = merger
 
189
        self._merger = merger
87
190
        self.file_id = file_id
88
191
        self.trans_id = trans_id
89
192
        self.this_kind = this_kind
97
200
    @decorators.cachedproperty
98
201
    def base_lines(self):
99
202
        """The lines of the 'base' version of the file."""
100
 
        return self.merger.get_lines(self.merger.base_tree, self.file_id)
 
203
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
101
204
 
102
205
    @decorators.cachedproperty
103
206
    def this_lines(self):
104
207
        """The lines of the 'this' version of the file."""
105
 
        return self.merger.get_lines(self.merger.this_tree, self.file_id)
 
208
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
106
209
 
107
210
    @decorators.cachedproperty
108
211
    def other_lines(self):
109
212
        """The lines of the 'other' version of the file."""
110
 
        return self.merger.get_lines(self.merger.other_tree, self.file_id)
 
213
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
111
214
 
112
215
 
113
216
class Merger(object):
708
811
            resolver = self._lca_multi_way
709
812
        child_pb = ui.ui_factory.nested_progress_bar()
710
813
        try:
 
814
            factories = Merger.hooks['merge_file_content']
 
815
            hooks = [factory(self) for factory in factories] + [self]
 
816
            self.active_hooks = [hook for hook in hooks if hook is not None]
711
817
            for num, (file_id, changed, parents3, names3,
712
818
                      executable3) in enumerate(entries):
713
819
                child_pb.update('Preparing file merge', num, len(entries))
714
820
                self._merge_names(file_id, parents3, names3, resolver=resolver)
715
821
                if changed:
716
 
                    file_status = self.merge_contents(file_id)
 
822
                    file_status = self._do_merge_contents(file_id)
717
823
                else:
718
824
                    file_status = 'unmodified'
719
825
                self._merge_executable(file_id,
1158
1264
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1159
1265
                                parent_trans_id, trans_id)
1160
1266
 
1161
 
    def merge_contents(self, file_id):
 
1267
    def _do_merge_contents(self, file_id):
1162
1268
        """Performs a merge on file_id contents."""
1163
1269
        def contents_pair(tree):
1164
1270
            if file_id not in tree:
1198
1304
        trans_id = self.tt.trans_id_file_id(file_id)
1199
1305
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1200
1306
            other_pair[0], winner)
1201
 
        hooks = Merger.hooks['merge_file_content']
1202
 
        hooks = list(hooks) + [self.default_text_merge]
 
1307
        hooks = self.active_hooks
1203
1308
        hook_status = 'not_applicable'
1204
1309
        for hook in hooks:
1205
 
            hook_status, lines = hook(params)
 
1310
            hook_status, lines = hook.merge_contents(params)
1206
1311
            if hook_status != 'not_applicable':
1207
1312
                # Don't try any more hooks, this one applies.
1208
1313
                break
1279
1384
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1280
1385
                % (file_id,))
1281
1386
 
1282
 
    def default_text_merge(self, merge_hook_params):
 
1387
    def merge_contents(self, merge_hook_params):
 
1388
        """Fallback merge logic after user installed hooks."""
 
1389
        # This function is used in merge hooks as the fallback instance.
 
1390
        # Perhaps making this function and the functions it calls be a 
 
1391
        # a separate class would be better.
1283
1392
        if merge_hook_params.winner == 'other':
1284
1393
            # OTHER is a straight winner, so replace this contents with other
1285
1394
            return self._default_other_winner_merge(merge_hook_params)