/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 breezy/merge_directive.py

[merge] robertc's integration, updated tests to check for retcode=3

Show diffs side-by-side

added added

removed removed

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