/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 bzrlib/merge_directive.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-18 12:46:49 UTC
  • mto: This revision was merged to the branch mainline in revision 6386.
  • Revision ID: jelmer@samba.org-20111218124649-nf7i69ocg3k2roz3
Import absolute_import in a few places.

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