~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

(jelmer) Add RepositoryFormat.is_deprecated(). (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

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