~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Pool
  • Date: 2005-07-22 22:37:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050722223753-7dced4e32d3ce21d
- add the start of a test for inventory file-id matching

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