~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Gordon Tyler
  • Date: 2012-02-28 04:58:14 UTC
  • mto: (6437.23.20 2.5)
  • mto: This revision was merged to the branch mainline in revision 6493.
  • Revision ID: gordon@doxxx.net-20120228045814-nab581dyd7ie5u14
Backport of fix for bug 939605 to bzr 2.5 series.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2011 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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
from email import Message
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
19
19
from StringIO import StringIO
 
20
import re
20
21
 
 
22
from bzrlib import lazy_import
 
23
lazy_import.lazy_import(globals(), """
21
24
from bzrlib import (
22
25
    branch as _mod_branch,
23
26
    diff,
 
27
    email_message,
24
28
    errors,
25
29
    gpg,
 
30
    hooks,
 
31
    registry,
26
32
    revision as _mod_revision,
27
33
    rio,
28
34
    testament,
29
35
    timestamp,
 
36
    trace,
30
37
    )
31
38
from bzrlib.bundle import (
32
39
    serializer as bundle_serializer,
33
40
    )
34
 
 
35
 
 
36
 
class MergeDirective(object):
37
 
 
 
41
""")
 
42
 
 
43
 
 
44
class MergeRequestBodyParams(object):
 
45
    """Parameter object for the merge_request_body hook."""
 
46
 
 
47
    def __init__(self, body, orig_body, directive, to, basename, subject,
 
48
                 branch, tree=None):
 
49
        self.body = body
 
50
        self.orig_body = orig_body
 
51
        self.directive = directive
 
52
        self.branch = branch
 
53
        self.tree = tree
 
54
        self.to = to
 
55
        self.basename = basename
 
56
        self.subject = subject
 
57
 
 
58
 
 
59
class MergeDirectiveHooks(hooks.Hooks):
 
60
    """Hooks for MergeDirective classes."""
 
61
 
 
62
    def __init__(self):
 
63
        hooks.Hooks.__init__(self, "bzrlib.merge_directive", "BaseMergeDirective.hooks")
 
64
        self.add_hook('merge_request_body',
 
65
            "Called with a MergeRequestBodyParams when a body is needed for"
 
66
            " a merge request.  Callbacks must return a body.  If more"
 
67
            " than one callback is registered, the output of one callback is"
 
68
            " provided to the next.", (1, 15, 0))
 
69
 
 
70
 
 
71
class BaseMergeDirective(object):
38
72
    """A request to perform a merge into a branch.
39
73
 
40
 
    Designed to be serialized and mailed.  It provides all the information
41
 
    needed to perform a merge automatically, by providing at minimum a revision
42
 
    bundle or the location of a branch.
43
 
 
44
 
    The serialization format is robust against certain common forms of
45
 
    deterioration caused by mailing.
46
 
 
47
 
    The format is also designed to be patch-compatible.  If the directive
48
 
    includes a diff or revision bundle, it should be possible to apply it
49
 
    directly using the standard patch program.
 
74
    This is the base class that all merge directive implementations 
 
75
    should derive from.
 
76
 
 
77
    :cvar multiple_output_files: Whether or not this merge directive 
 
78
        stores a set of revisions in more than one file
50
79
    """
51
80
 
52
 
    _format_string = 'Bazaar merge directive format 1'
 
81
    hooks = MergeDirectiveHooks()
 
82
 
 
83
    multiple_output_files = False
53
84
 
54
85
    def __init__(self, revision_id, testament_sha1, time, timezone,
55
 
                 target_branch, patch=None, patch_type=None,
56
 
                 source_branch=None, message=None):
 
86
                 target_branch, patch=None, source_branch=None,
 
87
                 message=None, bundle=None):
57
88
        """Constructor.
58
89
 
59
90
        :param revision_id: The revision to merge
61
92
            merge.
62
93
        :param time: The current POSIX timestamp time
63
94
        :param timezone: The timezone offset
64
 
        :param target_branch: The branch to apply the merge to
 
95
        :param target_branch: Location of branch to apply the merge to
65
96
        :param patch: The text of a diff or bundle
66
 
        :param patch_type: None, "diff" or "bundle", depending on the contents
67
 
            of patch
68
97
        :param source_branch: A public location to merge the revision from
69
98
        :param message: The message to use when committing this merge
70
99
        """
71
 
        assert patch_type in (None, 'diff', 'bundle')
72
 
        if patch_type != 'bundle' and source_branch is None:
73
 
            raise errors.NoMergeSource()
74
 
        if patch_type is not None and patch is None:
75
 
            raise errors.PatchMissing(patch_type)
76
100
        self.revision_id = revision_id
77
101
        self.testament_sha1 = testament_sha1
78
102
        self.time = time
79
103
        self.timezone = timezone
80
104
        self.target_branch = target_branch
81
105
        self.patch = patch
82
 
        self.patch_type = patch_type
83
106
        self.source_branch = source_branch
84
107
        self.message = message
85
108
 
86
 
    @classmethod
87
 
    def from_lines(klass, lines):
88
 
        """Deserialize a MergeRequest from an iterable of lines
89
 
 
90
 
        :param lines: An iterable of lines
91
 
        :return: a MergeRequest
92
 
        """
93
 
        line_iter = iter(lines)
94
 
        for line in line_iter:
95
 
            if line.startswith('# ' + klass._format_string):
96
 
                break
97
 
        else:
98
 
            if len(lines) > 0:
99
 
                raise errors.NotAMergeDirective(lines[0])
100
 
            else:
101
 
                raise errors.NotAMergeDirective('')
102
 
        stanza = rio.read_patch_stanza(line_iter)
103
 
        patch_lines = list(line_iter)
104
 
        if len(patch_lines) == 0:
105
 
            patch = None
106
 
            patch_type = None
107
 
        else:
108
 
            patch = ''.join(patch_lines)
109
 
            try:
110
 
                bundle_serializer.read_bundle(StringIO(patch))
111
 
            except errors.NotABundle:
112
 
                patch_type = 'diff'
113
 
            else:
114
 
                patch_type = 'bundle'
115
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
116
 
        kwargs = {}
117
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
118
 
                    'source_branch', 'message'):
119
 
            try:
120
 
                kwargs[key] = stanza.get(key)
121
 
            except KeyError:
122
 
                pass
123
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
124
 
        return MergeDirective(time=time, timezone=timezone,
125
 
                              patch_type=patch_type, patch=patch, **kwargs)
126
 
 
127
109
    def to_lines(self):
128
110
        """Serialize as a list of lines
129
111
 
130
112
        :return: a list of lines
131
113
        """
 
114
        raise NotImplementedError(self.to_lines)
 
115
 
 
116
    def to_files(self):
 
117
        """Serialize as a set of files.
 
118
 
 
119
        :return: List of tuples with filename and contents as lines
 
120
        """
 
121
        raise NotImplementedError(self.to_files)
 
122
 
 
123
    def get_raw_bundle(self):
 
124
        """Return the bundle for this merge directive.
 
125
 
 
126
        :return: bundle text or None if there is no bundle
 
127
        """
 
128
        return None
 
129
 
 
130
    def _to_lines(self, base_revision=False):
 
131
        """Serialize as a list of lines
 
132
 
 
133
        :return: a list of lines
 
134
        """
132
135
        time_str = timestamp.format_patch_date(self.time, self.timezone)
133
136
        stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
134
137
                            target_branch=self.target_branch,
136
139
        for key in ('source_branch', 'message'):
137
140
            if self.__dict__[key] is not None:
138
141
                stanza.add(key, self.__dict__[key])
 
142
        if base_revision:
 
143
            stanza.add('base_revision_id', self.base_revision_id)
139
144
        lines = ['# ' + self._format_string + '\n']
140
145
        lines.extend(rio.to_patch_lines(stanza))
141
146
        lines.append('# \n')
142
 
        if self.patch is not None:
143
 
            lines.extend(self.patch.splitlines(True))
144
147
        return lines
145
148
 
146
 
    def to_signed(self, branch):
147
 
        """Serialize as a signed string.
148
 
 
149
 
        :param branch: The source branch, to get the signing strategy
150
 
        :return: a string
151
 
        """
152
 
        my_gpg = gpg.GPGStrategy(branch.get_config())
153
 
        return my_gpg.sign(''.join(self.to_lines()))
154
 
 
155
 
    def to_email(self, mail_to, branch, sign=False):
156
 
        """Serialize as an email message.
157
 
 
158
 
        :param mail_to: The address to mail the message to
159
 
        :param branch: The source branch, to get the signing strategy and
160
 
            source email address
161
 
        :param sign: If True, gpg-sign the email
162
 
        :return: an email message
163
 
        """
164
 
        mail_from = branch.get_config().username()
165
 
        message = Message.Message()
166
 
        message['To'] = mail_to
167
 
        message['From'] = mail_from
168
 
        if self.message is not None:
169
 
            message['Subject'] = self.message
170
 
        else:
171
 
            revision = branch.repository.get_revision(self.revision_id)
172
 
            message['Subject'] = revision.message
173
 
        if sign:
174
 
            body = self.to_signed(branch)
175
 
        else:
176
 
            body = ''.join(self.to_lines())
177
 
        message.set_payload(body)
178
 
        return message
 
149
    def write_to_directory(self, path):
 
150
        """Write this merge directive to a series of files in a directory.
 
151
 
 
152
        :param path: Filesystem path to write to
 
153
        """
 
154
        raise NotImplementedError(self.write_to_directory)
179
155
 
180
156
    @classmethod
181
157
    def from_objects(klass, repository, revision_id, time, timezone,
190
166
        :param target_branch: The url of the branch to merge into
191
167
        :param patch_type: 'bundle', 'diff' or None, depending on the type of
192
168
            patch desired.
193
 
        :param local_target_branch: a local copy of the target branch
194
 
        :param public_branch: location of a public branch containing the target
195
 
            revision.
 
169
        :param local_target_branch: the submit branch, either itself or a local copy
 
170
        :param public_branch: location of a public branch containing
 
171
            the target revision.
196
172
        :param message: Message to use when committing the merge
197
173
        :return: The merge directive
198
174
 
202
178
        If the message is not supplied, the message from revision_id will be
203
179
        used for the commit.
204
180
        """
205
 
        t = testament.StrictTestament3.from_revision(repository, revision_id)
206
 
        submit_branch = _mod_branch.Branch.open(target_branch)
 
181
        t_revision_id = revision_id
 
182
        if revision_id == _mod_revision.NULL_REVISION:
 
183
            t_revision_id = None
 
184
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
 
185
        if local_target_branch is None:
 
186
            submit_branch = _mod_branch.Branch.open(target_branch)
 
187
        else:
 
188
            submit_branch = local_target_branch
207
189
        if submit_branch.get_public_branch() is not None:
208
190
            target_branch = submit_branch.get_public_branch()
209
191
        if patch_type is None:
210
192
            patch = None
211
193
        else:
212
194
            submit_revision_id = submit_branch.last_revision()
 
195
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
213
196
            repository.fetch(submit_branch.repository, submit_revision_id)
214
 
            ancestor_id = _mod_revision.common_ancestor(revision_id,
215
 
                                                        submit_revision_id,
216
 
                                                        repository)
 
197
            graph = repository.get_graph()
 
198
            ancestor_id = graph.find_unique_lca(revision_id,
 
199
                                                submit_revision_id)
217
200
            type_handler = {'bundle': klass._generate_bundle,
218
201
                            'diff': klass._generate_diff,
219
202
                            None: lambda x, y, z: None }
220
203
            patch = type_handler[patch_type](repository, revision_id,
221
204
                                             ancestor_id)
222
 
            if patch_type == 'bundle':
223
 
                s = StringIO()
224
 
                bundle_serializer.write_bundle(repository, revision_id,
225
 
                                               ancestor_id, s)
226
 
                patch = s.getvalue()
227
 
            elif patch_type == 'diff':
228
 
                patch = klass._generate_diff(repository, revision_id,
229
 
                                             ancestor_id)
230
 
 
231
 
            if public_branch is not None and patch_type != 'bundle':
232
 
                public_branch_obj = _mod_branch.Branch.open(public_branch)
233
 
                if not public_branch_obj.repository.has_revision(revision_id):
234
 
                    raise errors.PublicBranchOutOfDate(public_branch,
235
 
                                                       revision_id)
236
 
 
237
 
        return MergeDirective(revision_id, t.as_sha1(), time, timezone,
238
 
                              target_branch, patch, patch_type, public_branch,
239
 
                              message)
 
205
 
 
206
        if public_branch is not None and patch_type != 'bundle':
 
207
            public_branch_obj = _mod_branch.Branch.open(public_branch)
 
208
            if not public_branch_obj.repository.has_revision(revision_id):
 
209
                raise errors.PublicBranchOutOfDate(public_branch,
 
210
                                                   revision_id)
 
211
 
 
212
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
213
            patch, patch_type, public_branch, message)
 
214
 
 
215
    def get_disk_name(self, branch):
 
216
        """Generate a suitable basename for storing this directive on disk
 
217
 
 
218
        :param branch: The Branch this merge directive was generated fro
 
219
        :return: A string
 
220
        """
 
221
        revno, revision_id = branch.last_revision_info()
 
222
        if self.revision_id == revision_id:
 
223
            revno = [revno]
 
224
        else:
 
225
            revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
 
226
                ['merge'])
 
227
        nick = re.sub('(\W+)', '-', branch.nick).strip('-')
 
228
        return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
240
229
 
241
230
    @staticmethod
242
231
    def _generate_diff(repository, revision_id, ancestor_id):
253
242
                                       ancestor_id, s)
254
243
        return s.getvalue()
255
244
 
 
245
    def to_signed(self, branch):
 
246
        """Serialize as a signed string.
 
247
 
 
248
        :param branch: The source branch, to get the signing strategy
 
249
        :return: a string
 
250
        """
 
251
        my_gpg = gpg.GPGStrategy(branch.get_config_stack())
 
252
        return my_gpg.sign(''.join(self.to_lines()))
 
253
 
 
254
    def to_email(self, mail_to, branch, sign=False):
 
255
        """Serialize as an email message.
 
256
 
 
257
        :param mail_to: The address to mail the message to
 
258
        :param branch: The source branch, to get the signing strategy and
 
259
            source email address
 
260
        :param sign: If True, gpg-sign the email
 
261
        :return: an email message
 
262
        """
 
263
        mail_from = branch.get_config().username()
 
264
        if self.message is not None:
 
265
            subject = self.message
 
266
        else:
 
267
            revision = branch.repository.get_revision(self.revision_id)
 
268
            subject = revision.message
 
269
        if sign:
 
270
            body = self.to_signed(branch)
 
271
        else:
 
272
            body = ''.join(self.to_lines())
 
273
        message = email_message.EmailMessage(mail_from, mail_to, subject,
 
274
            body)
 
275
        return message
 
276
 
256
277
    def install_revisions(self, target_repo):
257
278
        """Install revisions and return the target revision"""
258
279
        if not target_repo.has_revision(self.revision_id):
259
280
            if self.patch_type == 'bundle':
260
 
                info = bundle_serializer.read_bundle(StringIO(self.patch))
 
281
                info = bundle_serializer.read_bundle(
 
282
                    StringIO(self.get_raw_bundle()))
261
283
                # We don't use the bundle's target revision, because
262
284
                # MergeDirective.revision_id is authoritative.
263
 
                info.install_revisions(target_repo)
 
285
                try:
 
286
                    info.install_revisions(target_repo, stream_input=False)
 
287
                except errors.RevisionNotPresent:
 
288
                    # At least one dependency isn't present.  Try installing
 
289
                    # missing revisions from the submit branch
 
290
                    try:
 
291
                        submit_branch = \
 
292
                            _mod_branch.Branch.open(self.target_branch)
 
293
                    except errors.NotBranchError:
 
294
                        raise errors.TargetNotBranch(self.target_branch)
 
295
                    missing_revisions = []
 
296
                    bundle_revisions = set(r.revision_id for r in
 
297
                                           info.real_revisions)
 
298
                    for revision in info.real_revisions:
 
299
                        for parent_id in revision.parent_ids:
 
300
                            if (parent_id not in bundle_revisions and
 
301
                                not target_repo.has_revision(parent_id)):
 
302
                                missing_revisions.append(parent_id)
 
303
                    # reverse missing revisions to try to get heads first
 
304
                    unique_missing = []
 
305
                    unique_missing_set = set()
 
306
                    for revision in reversed(missing_revisions):
 
307
                        if revision in unique_missing_set:
 
308
                            continue
 
309
                        unique_missing.append(revision)
 
310
                        unique_missing_set.add(revision)
 
311
                    for missing_revision in unique_missing:
 
312
                        target_repo.fetch(submit_branch.repository,
 
313
                                          missing_revision)
 
314
                    info.install_revisions(target_repo, stream_input=False)
264
315
            else:
265
316
                source_branch = _mod_branch.Branch.open(self.source_branch)
266
317
                target_repo.fetch(source_branch.repository, self.revision_id)
267
318
        return self.revision_id
 
319
 
 
320
    def compose_merge_request(self, mail_client, to, body, branch, tree=None):
 
321
        """Compose a request to merge this directive.
 
322
 
 
323
        :param mail_client: The mail client to use for composing this request.
 
324
        :param to: The address to compose the request to.
 
325
        :param branch: The Branch that was used to produce this directive.
 
326
        :param tree: The Tree (if any) for the Branch used to produce this
 
327
            directive.
 
328
        """
 
329
        basename = self.get_disk_name(branch)
 
330
        subject = '[MERGE] '
 
331
        if self.message is not None:
 
332
            subject += self.message
 
333
        else:
 
334
            revision = branch.repository.get_revision(self.revision_id)
 
335
            subject += revision.get_summary()
 
336
        if getattr(mail_client, 'supports_body', False):
 
337
            orig_body = body
 
338
            for hook in self.hooks['merge_request_body']:
 
339
                params = MergeRequestBodyParams(body, orig_body, self,
 
340
                                                to, basename, subject, branch,
 
341
                                                tree)
 
342
                body = hook(params)
 
343
        elif len(self.hooks['merge_request_body']) > 0:
 
344
            trace.warning('Cannot run merge_request_body hooks because mail'
 
345
                          ' client %s does not support message bodies.',
 
346
                        mail_client.__class__.__name__)
 
347
        mail_client.compose_merge_request(to, subject,
 
348
                                          ''.join(self.to_lines()),
 
349
                                          basename, body)
 
350
 
 
351
 
 
352
class MergeDirective(BaseMergeDirective):
 
353
 
 
354
    """A request to perform a merge into a branch.
 
355
 
 
356
    Designed to be serialized and mailed.  It provides all the information
 
357
    needed to perform a merge automatically, by providing at minimum a revision
 
358
    bundle or the location of a branch.
 
359
 
 
360
    The serialization format is robust against certain common forms of
 
361
    deterioration caused by mailing.
 
362
 
 
363
    The format is also designed to be patch-compatible.  If the directive
 
364
    includes a diff or revision bundle, it should be possible to apply it
 
365
    directly using the standard patch program.
 
366
    """
 
367
 
 
368
    _format_string = 'Bazaar merge directive format 1'
 
369
 
 
370
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
371
                 target_branch, patch=None, patch_type=None,
 
372
                 source_branch=None, message=None, bundle=None):
 
373
        """Constructor.
 
374
 
 
375
        :param revision_id: The revision to merge
 
376
        :param testament_sha1: The sha1 of the testament of the revision to
 
377
            merge.
 
378
        :param time: The current POSIX timestamp time
 
379
        :param timezone: The timezone offset
 
380
        :param target_branch: Location of the branch to apply the merge to
 
381
        :param patch: The text of a diff or bundle
 
382
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
383
            of patch
 
384
        :param source_branch: A public location to merge the revision from
 
385
        :param message: The message to use when committing this merge
 
386
        """
 
387
        BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
388
            timezone, target_branch, patch, source_branch, message)
 
389
        if patch_type not in (None, 'diff', 'bundle'):
 
390
            raise ValueError(patch_type)
 
391
        if patch_type != 'bundle' and source_branch is None:
 
392
            raise errors.NoMergeSource()
 
393
        if patch_type is not None and patch is None:
 
394
            raise errors.PatchMissing(patch_type)
 
395
        self.patch_type = patch_type
 
396
 
 
397
    def clear_payload(self):
 
398
        self.patch = None
 
399
        self.patch_type = None
 
400
 
 
401
    def get_raw_bundle(self):
 
402
        return self.bundle
 
403
 
 
404
    def _bundle(self):
 
405
        if self.patch_type == 'bundle':
 
406
            return self.patch
 
407
        else:
 
408
            return None
 
409
 
 
410
    bundle = property(_bundle)
 
411
 
 
412
    @classmethod
 
413
    def from_lines(klass, lines):
 
414
        """Deserialize a MergeRequest from an iterable of lines
 
415
 
 
416
        :param lines: An iterable of lines
 
417
        :return: a MergeRequest
 
418
        """
 
419
        line_iter = iter(lines)
 
420
        firstline = ""
 
421
        for line in line_iter:
 
422
            if line.startswith('# Bazaar merge directive format '):
 
423
                return _format_registry.get(line[2:].rstrip())._from_lines(
 
424
                    line_iter)
 
425
            firstline = firstline or line.strip()
 
426
        raise errors.NotAMergeDirective(firstline)
 
427
 
 
428
    @classmethod
 
429
    def _from_lines(klass, line_iter):
 
430
        stanza = rio.read_patch_stanza(line_iter)
 
431
        patch_lines = list(line_iter)
 
432
        if len(patch_lines) == 0:
 
433
            patch = None
 
434
            patch_type = None
 
435
        else:
 
436
            patch = ''.join(patch_lines)
 
437
            try:
 
438
                bundle_serializer.read_bundle(StringIO(patch))
 
439
            except (errors.NotABundle, errors.BundleNotSupported,
 
440
                    errors.BadBundle):
 
441
                patch_type = 'diff'
 
442
            else:
 
443
                patch_type = 'bundle'
 
444
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
445
        kwargs = {}
 
446
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
447
                    'source_branch', 'message'):
 
448
            try:
 
449
                kwargs[key] = stanza.get(key)
 
450
            except KeyError:
 
451
                pass
 
452
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
453
        return MergeDirective(time=time, timezone=timezone,
 
454
                              patch_type=patch_type, patch=patch, **kwargs)
 
455
 
 
456
    def to_lines(self):
 
457
        lines = self._to_lines()
 
458
        if self.patch is not None:
 
459
            lines.extend(self.patch.splitlines(True))
 
460
        return lines
 
461
 
 
462
    @staticmethod
 
463
    def _generate_bundle(repository, revision_id, ancestor_id):
 
464
        s = StringIO()
 
465
        bundle_serializer.write_bundle(repository, revision_id,
 
466
                                       ancestor_id, s, '0.9')
 
467
        return s.getvalue()
 
468
 
 
469
    def get_merge_request(self, repository):
 
470
        """Provide data for performing a merge
 
471
 
 
472
        Returns suggested base, suggested target, and patch verification status
 
473
        """
 
474
        return None, self.revision_id, 'inapplicable'
 
475
 
 
476
 
 
477
class MergeDirective2(BaseMergeDirective):
 
478
 
 
479
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
480
 
 
481
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
482
                 target_branch, patch=None, source_branch=None, message=None,
 
483
                 bundle=None, base_revision_id=None):
 
484
        if source_branch is None and bundle is None:
 
485
            raise errors.NoMergeSource()
 
486
        BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
487
            timezone, target_branch, patch, source_branch, message)
 
488
        self.bundle = bundle
 
489
        self.base_revision_id = base_revision_id
 
490
 
 
491
    def _patch_type(self):
 
492
        if self.bundle is not None:
 
493
            return 'bundle'
 
494
        elif self.patch is not None:
 
495
            return 'diff'
 
496
        else:
 
497
            return None
 
498
 
 
499
    patch_type = property(_patch_type)
 
500
 
 
501
    def clear_payload(self):
 
502
        self.patch = None
 
503
        self.bundle = None
 
504
 
 
505
    def get_raw_bundle(self):
 
506
        if self.bundle is None:
 
507
            return None
 
508
        else:
 
509
            return self.bundle.decode('base-64')
 
510
 
 
511
    @classmethod
 
512
    def _from_lines(klass, line_iter):
 
513
        stanza = rio.read_patch_stanza(line_iter)
 
514
        patch = None
 
515
        bundle = None
 
516
        try:
 
517
            start = line_iter.next()
 
518
        except StopIteration:
 
519
            pass
 
520
        else:
 
521
            if start.startswith('# Begin patch'):
 
522
                patch_lines = []
 
523
                for line in line_iter:
 
524
                    if line.startswith('# Begin bundle'):
 
525
                        start = line
 
526
                        break
 
527
                    patch_lines.append(line)
 
528
                else:
 
529
                    start = None
 
530
                patch = ''.join(patch_lines)
 
531
            if start is not None:
 
532
                if start.startswith('# Begin bundle'):
 
533
                    bundle = ''.join(line_iter)
 
534
                else:
 
535
                    raise errors.IllegalMergeDirectivePayload(start)
 
536
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
537
        kwargs = {}
 
538
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
539
                    'source_branch', 'message', 'base_revision_id'):
 
540
            try:
 
541
                kwargs[key] = stanza.get(key)
 
542
            except KeyError:
 
543
                pass
 
544
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
545
        kwargs['base_revision_id'] =\
 
546
            kwargs['base_revision_id'].encode('utf-8')
 
547
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
548
                     **kwargs)
 
549
 
 
550
    def to_lines(self):
 
551
        lines = self._to_lines(base_revision=True)
 
552
        if self.patch is not None:
 
553
            lines.append('# Begin patch\n')
 
554
            lines.extend(self.patch.splitlines(True))
 
555
        if self.bundle is not None:
 
556
            lines.append('# Begin bundle\n')
 
557
            lines.extend(self.bundle.splitlines(True))
 
558
        return lines
 
559
 
 
560
    @classmethod
 
561
    def from_objects(klass, repository, revision_id, time, timezone,
 
562
                 target_branch, include_patch=True, include_bundle=True,
 
563
                 local_target_branch=None, public_branch=None, message=None,
 
564
                 base_revision_id=None):
 
565
        """Generate a merge directive from various objects
 
566
 
 
567
        :param repository: The repository containing the revision
 
568
        :param revision_id: The revision to merge
 
569
        :param time: The POSIX timestamp of the date the request was issued.
 
570
        :param timezone: The timezone of the request
 
571
        :param target_branch: The url of the branch to merge into
 
572
        :param include_patch: If true, include a preview patch
 
573
        :param include_bundle: If true, include a bundle
 
574
        :param local_target_branch: the target branch, either itself or a local copy
 
575
        :param public_branch: location of a public branch containing
 
576
            the target revision.
 
577
        :param message: Message to use when committing the merge
 
578
        :return: The merge directive
 
579
 
 
580
        The public branch is always used if supplied.  If no bundle is
 
581
        included, the public branch must be supplied, and will be verified.
 
582
 
 
583
        If the message is not supplied, the message from revision_id will be
 
584
        used for the commit.
 
585
        """
 
586
        locked = []
 
587
        try:
 
588
            repository.lock_write()
 
589
            locked.append(repository)
 
590
            t_revision_id = revision_id
 
591
            if revision_id == 'null:':
 
592
                t_revision_id = None
 
593
            t = testament.StrictTestament3.from_revision(repository,
 
594
                t_revision_id)
 
595
            if local_target_branch is None:
 
596
                submit_branch = _mod_branch.Branch.open(target_branch)
 
597
            else:
 
598
                submit_branch = local_target_branch
 
599
            submit_branch.lock_read()
 
600
            locked.append(submit_branch)
 
601
            if submit_branch.get_public_branch() is not None:
 
602
                target_branch = submit_branch.get_public_branch()
 
603
            submit_revision_id = submit_branch.last_revision()
 
604
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
605
            graph = repository.get_graph(submit_branch.repository)
 
606
            ancestor_id = graph.find_unique_lca(revision_id,
 
607
                                                submit_revision_id)
 
608
            if base_revision_id is None:
 
609
                base_revision_id = ancestor_id
 
610
            if (include_patch, include_bundle) != (False, False):
 
611
                repository.fetch(submit_branch.repository, submit_revision_id)
 
612
            if include_patch:
 
613
                patch = klass._generate_diff(repository, revision_id,
 
614
                                             base_revision_id)
 
615
            else:
 
616
                patch = None
 
617
 
 
618
            if include_bundle:
 
619
                bundle = klass._generate_bundle(repository, revision_id,
 
620
                    ancestor_id).encode('base-64')
 
621
            else:
 
622
                bundle = None
 
623
 
 
624
            if public_branch is not None and not include_bundle:
 
625
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
626
                public_branch_obj.lock_read()
 
627
                locked.append(public_branch_obj)
 
628
                if not public_branch_obj.repository.has_revision(
 
629
                    revision_id):
 
630
                    raise errors.PublicBranchOutOfDate(public_branch,
 
631
                                                       revision_id)
 
632
            testament_sha1 = t.as_sha1()
 
633
        finally:
 
634
            for entry in reversed(locked):
 
635
                entry.unlock()
 
636
        return klass(revision_id, testament_sha1, time, timezone,
 
637
            target_branch, patch, public_branch, message, bundle,
 
638
            base_revision_id)
 
639
 
 
640
    def _verify_patch(self, repository):
 
641
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
642
                                               self.base_revision_id)
 
643
        # Convert line-endings to UNIX
 
644
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
645
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
 
646
        # Strip trailing whitespace
 
647
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
648
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
649
        return (calculated_patch == stored_patch)
 
650
 
 
651
    def get_merge_request(self, repository):
 
652
        """Provide data for performing a merge
 
653
 
 
654
        Returns suggested base, suggested target, and patch verification status
 
655
        """
 
656
        verified = self._maybe_verify(repository)
 
657
        return self.base_revision_id, self.revision_id, verified
 
658
 
 
659
    def _maybe_verify(self, repository):
 
660
        if self.patch is not None:
 
661
            if self._verify_patch(repository):
 
662
                return 'verified'
 
663
            else:
 
664
                return 'failed'
 
665
        else:
 
666
            return 'inapplicable'
 
667
 
 
668
 
 
669
class MergeDirectiveFormatRegistry(registry.Registry):
 
670
 
 
671
    def register(self, directive, format_string=None):
 
672
        if format_string is None:
 
673
            format_string = directive._format_string
 
674
        registry.Registry.register(self, format_string, directive)
 
675
 
 
676
 
 
677
_format_registry = MergeDirectiveFormatRegistry()
 
678
_format_registry.register(MergeDirective)
 
679
_format_registry.register(MergeDirective2)
 
680
# 0.19 never existed.  It got renamed to 0.90.  But by that point, there were
 
681
# already merge directives in the wild that used 0.19. Registering with the old
 
682
# format string to retain compatibility with those merge directives.
 
683
_format_registry.register(MergeDirective2,
 
684
                          'Bazaar merge directive format 2 (Bazaar 0.19)')