/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: 2010-03-12 19:59:50 UTC
  • mto: This revision was merged to the branch mainline in revision 5089.
  • Revision ID: jelmer@samba.org-20100312195950-wwufs49rlkf0s471
``bzrlib.merge_directive._BaseMergeDirective`` has been renamed to 
``bzrlib.merge_directive.BaseMergeDirective`` and is now public.

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