/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-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

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