/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

  • Committer: Jelmer Vernooij
  • Date: 2020-01-31 17:43:44 UTC
  • mto: This revision was merged to the branch mainline in revision 7478.
  • Revision ID: jelmer@jelmer.uk-20200131174344-qjhgqm7bdkuqj9sj
Default to running Python 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)')