/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: Robert Collins
  • Date: 2005-12-24 02:20:45 UTC
  • mto: (1185.50.57 bzr-jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1550.
  • Revision ID: robertc@robertcollins.net-20051224022045-14efc8dfa0e1a4e9
Start tests for api usage.

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