/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-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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