/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: 2018-05-07 15:27:39 UTC
  • mto: This revision was merged to the branch mainline in revision 6958.
  • Revision ID: jelmer@jelmer.uk-20180507152739-fuv9z9r0yzi7ln3t
Specify source in .coveragerc.

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