/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-08-12 20:24:50 UTC
  • mto: (7290.1.35 work)
  • mto: This revision was merged to the branch mainline in revision 7405.
  • Revision ID: jelmer@jelmer.uk-20190812202450-vdpamxay6sebo93w
Fix path to brz.

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