/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: 2019-01-01 21:08:01 UTC
  • mto: This revision was merged to the branch mainline in revision 7231.
  • Revision ID: jelmer@jelmer.uk-20190101210801-2dlsv7b1lvydmpkl
Fix tests.

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