/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

Return mapping in revision_id_bzr_to_foreign() as required by the interface.

Show diffs side-by-side

added added

removed removed

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