1
# Copyright (C) 2007 Canonical Ltd
 
 
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.
 
 
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.
 
 
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
18
from StringIO import StringIO
 
 
22
    branch as _mod_branch,
 
 
27
    revision as _mod_revision,
 
 
32
from bzrlib.bundle import (
 
 
33
    serializer as bundle_serializer,
 
 
35
from bzrlib.email_message import EmailMessage
 
 
38
class _BaseMergeDirective(object):
 
 
40
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
41
                 target_branch, patch=None, source_branch=None, message=None,
 
 
45
        :param revision_id: The revision to merge
 
 
46
        :param testament_sha1: The sha1 of the testament of the revision to
 
 
48
        :param time: The current POSIX timestamp time
 
 
49
        :param timezone: The timezone offset
 
 
50
        :param target_branch: The branch to apply the merge to
 
 
51
        :param patch: The text of a diff or bundle
 
 
52
        :param source_branch: A public location to merge the revision from
 
 
53
        :param message: The message to use when committing this merge
 
 
55
        self.revision_id = revision_id
 
 
56
        self.testament_sha1 = testament_sha1
 
 
58
        self.timezone = timezone
 
 
59
        self.target_branch = target_branch
 
 
61
        self.source_branch = source_branch
 
 
62
        self.message = message
 
 
64
    def _to_lines(self, base_revision=False):
 
 
65
        """Serialize as a list of lines
 
 
67
        :return: a list of lines
 
 
69
        time_str = timestamp.format_patch_date(self.time, self.timezone)
 
 
70
        stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
 
 
71
                            target_branch=self.target_branch,
 
 
72
                            testament_sha1=self.testament_sha1)
 
 
73
        for key in ('source_branch', 'message'):
 
 
74
            if self.__dict__[key] is not None:
 
 
75
                stanza.add(key, self.__dict__[key])
 
 
77
            stanza.add('base_revision_id', self.base_revision_id)
 
 
78
        lines = ['# ' + self._format_string + '\n']
 
 
79
        lines.extend(rio.to_patch_lines(stanza))
 
 
84
    def from_objects(klass, repository, revision_id, time, timezone,
 
 
85
                 target_branch, patch_type='bundle',
 
 
86
                 local_target_branch=None, public_branch=None, message=None):
 
 
87
        """Generate a merge directive from various objects
 
 
89
        :param repository: The repository containing the revision
 
 
90
        :param revision_id: The revision to merge
 
 
91
        :param time: The POSIX timestamp of the date the request was issued.
 
 
92
        :param timezone: The timezone of the request
 
 
93
        :param target_branch: The url of the branch to merge into
 
 
94
        :param patch_type: 'bundle', 'diff' or None, depending on the type of
 
 
96
        :param local_target_branch: a local copy of the target branch
 
 
97
        :param public_branch: location of a public branch containing the target
 
 
99
        :param message: Message to use when committing the merge
 
 
100
        :return: The merge directive
 
 
102
        The public branch is always used if supplied.  If the patch_type is
 
 
103
        not 'bundle', the public branch must be supplied, and will be verified.
 
 
105
        If the message is not supplied, the message from revision_id will be
 
 
108
        t_revision_id = revision_id
 
 
109
        if revision_id == _mod_revision.NULL_REVISION:
 
 
111
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
 
 
112
        submit_branch = _mod_branch.Branch.open(target_branch)
 
 
113
        if submit_branch.get_public_branch() is not None:
 
 
114
            target_branch = submit_branch.get_public_branch()
 
 
115
        if patch_type is None:
 
 
118
            submit_revision_id = submit_branch.last_revision()
 
 
119
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
 
120
            repository.fetch(submit_branch.repository, submit_revision_id)
 
 
121
            graph = repository.get_graph()
 
 
122
            ancestor_id = graph.find_unique_lca(revision_id,
 
 
124
            type_handler = {'bundle': klass._generate_bundle,
 
 
125
                            'diff': klass._generate_diff,
 
 
126
                            None: lambda x, y, z: None }
 
 
127
            patch = type_handler[patch_type](repository, revision_id,
 
 
130
        if public_branch is not None and patch_type != 'bundle':
 
 
131
            public_branch_obj = _mod_branch.Branch.open(public_branch)
 
 
132
            if not public_branch_obj.repository.has_revision(revision_id):
 
 
133
                raise errors.PublicBranchOutOfDate(public_branch,
 
 
136
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
 
137
            patch, patch_type, public_branch, message)
 
 
140
    def _generate_diff(repository, revision_id, ancestor_id):
 
 
141
        tree_1 = repository.revision_tree(ancestor_id)
 
 
142
        tree_2 = repository.revision_tree(revision_id)
 
 
144
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
 
 
148
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
150
        bundle_serializer.write_bundle(repository, revision_id,
 
 
154
    def to_signed(self, branch):
 
 
155
        """Serialize as a signed string.
 
 
157
        :param branch: The source branch, to get the signing strategy
 
 
160
        my_gpg = gpg.GPGStrategy(branch.get_config())
 
 
161
        return my_gpg.sign(''.join(self.to_lines()))
 
 
163
    def to_email(self, mail_to, branch, sign=False):
 
 
164
        """Serialize as an email message.
 
 
166
        :param mail_to: The address to mail the message to
 
 
167
        :param branch: The source branch, to get the signing strategy and
 
 
169
        :param sign: If True, gpg-sign the email
 
 
170
        :return: an email message
 
 
172
        mail_from = branch.get_config().username()
 
 
173
        if self.message is not None:
 
 
174
            subject = self.message
 
 
176
            revision = branch.repository.get_revision(self.revision_id)
 
 
177
            subject = revision.message
 
 
179
            body = self.to_signed(branch)
 
 
181
            body = ''.join(self.to_lines())
 
 
182
        message = EmailMessage(mail_from, mail_to, subject, body)
 
 
185
    def install_revisions(self, target_repo):
 
 
186
        """Install revisions and return the target revision"""
 
 
187
        if not target_repo.has_revision(self.revision_id):
 
 
188
            if self.patch_type == 'bundle':
 
 
189
                info = bundle_serializer.read_bundle(
 
 
190
                    StringIO(self.get_raw_bundle()))
 
 
191
                # We don't use the bundle's target revision, because
 
 
192
                # MergeDirective.revision_id is authoritative.
 
 
194
                    info.install_revisions(target_repo, stream_input=False)
 
 
195
                except errors.RevisionNotPresent:
 
 
196
                    # At least one dependency isn't present.  Try installing
 
 
197
                    # missing revisions from the submit branch
 
 
198
                    submit_branch = _mod_branch.Branch.open(self.target_branch)
 
 
199
                    missing_revisions = []
 
 
200
                    bundle_revisions = set(r.revision_id for r in
 
 
202
                    for revision in info.real_revisions:
 
 
203
                        for parent_id in revision.parent_ids:
 
 
204
                            if (parent_id not in bundle_revisions and
 
 
205
                                not target_repo.has_revision(parent_id)):
 
 
206
                                missing_revisions.append(parent_id)
 
 
207
                    # reverse missing revisions to try to get heads first
 
 
209
                    unique_missing_set = set()
 
 
210
                    for revision in reversed(missing_revisions):
 
 
211
                        if revision in unique_missing_set:
 
 
213
                        unique_missing.append(revision)
 
 
214
                        unique_missing_set.add(revision)
 
 
215
                    for missing_revision in unique_missing:
 
 
216
                        target_repo.fetch(submit_branch.repository,
 
 
218
                    info.install_revisions(target_repo, stream_input=False)
 
 
220
                source_branch = _mod_branch.Branch.open(self.source_branch)
 
 
221
                target_repo.fetch(source_branch.repository, self.revision_id)
 
 
222
        return self.revision_id
 
 
225
class MergeDirective(_BaseMergeDirective):
 
 
227
    """A request to perform a merge into a branch.
 
 
229
    Designed to be serialized and mailed.  It provides all the information
 
 
230
    needed to perform a merge automatically, by providing at minimum a revision
 
 
231
    bundle or the location of a branch.
 
 
233
    The serialization format is robust against certain common forms of
 
 
234
    deterioration caused by mailing.
 
 
236
    The format is also designed to be patch-compatible.  If the directive
 
 
237
    includes a diff or revision bundle, it should be possible to apply it
 
 
238
    directly using the standard patch program.
 
 
241
    _format_string = 'Bazaar merge directive format 1'
 
 
243
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
244
                 target_branch, patch=None, patch_type=None,
 
 
245
                 source_branch=None, message=None, bundle=None):
 
 
248
        :param revision_id: The revision to merge
 
 
249
        :param testament_sha1: The sha1 of the testament of the revision to
 
 
251
        :param time: The current POSIX timestamp time
 
 
252
        :param timezone: The timezone offset
 
 
253
        :param target_branch: The branch to apply the merge to
 
 
254
        :param patch: The text of a diff or bundle
 
 
255
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
 
257
        :param source_branch: A public location to merge the revision from
 
 
258
        :param message: The message to use when committing this merge
 
 
260
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
261
            timezone, target_branch, patch, source_branch, message)
 
 
262
        assert patch_type in (None, 'diff', 'bundle'), patch_type
 
 
263
        if patch_type != 'bundle' and source_branch is None:
 
 
264
            raise errors.NoMergeSource()
 
 
265
        if patch_type is not None and patch is None:
 
 
266
            raise errors.PatchMissing(patch_type)
 
 
267
        self.patch_type = patch_type
 
 
269
    def clear_payload(self):
 
 
271
        self.patch_type = None
 
 
273
    def get_raw_bundle(self):
 
 
277
        if self.patch_type == 'bundle':
 
 
282
    bundle = property(_bundle)
 
 
285
    def from_lines(klass, lines):
 
 
286
        """Deserialize a MergeRequest from an iterable of lines
 
 
288
        :param lines: An iterable of lines
 
 
289
        :return: a MergeRequest
 
 
291
        line_iter = iter(lines)
 
 
292
        for line in line_iter:
 
 
293
            if line.startswith('# Bazaar merge directive format '):
 
 
297
                raise errors.NotAMergeDirective(lines[0])
 
 
299
                raise errors.NotAMergeDirective('')
 
 
300
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
 
303
    def _from_lines(klass, line_iter):
 
 
304
        stanza = rio.read_patch_stanza(line_iter)
 
 
305
        patch_lines = list(line_iter)
 
 
306
        if len(patch_lines) == 0:
 
 
310
            patch = ''.join(patch_lines)
 
 
312
                bundle_serializer.read_bundle(StringIO(patch))
 
 
313
            except (errors.NotABundle, errors.BundleNotSupported,
 
 
317
                patch_type = 'bundle'
 
 
318
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
320
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
321
                    'source_branch', 'message'):
 
 
323
                kwargs[key] = stanza.get(key)
 
 
326
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
327
        return MergeDirective(time=time, timezone=timezone,
 
 
328
                              patch_type=patch_type, patch=patch, **kwargs)
 
 
331
        lines = self._to_lines()
 
 
332
        if self.patch is not None:
 
 
333
            lines.extend(self.patch.splitlines(True))
 
 
337
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
339
        bundle_serializer.write_bundle(repository, revision_id,
 
 
340
                                       ancestor_id, s, '0.9')
 
 
343
    def get_merge_request(self, repository):
 
 
344
        """Provide data for performing a merge
 
 
346
        Returns suggested base, suggested target, and patch verification status
 
 
348
        return None, self.revision_id, 'inapplicable'
 
 
351
class MergeDirective2(_BaseMergeDirective):
 
 
353
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
 
355
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
356
                 target_branch, patch=None, source_branch=None, message=None,
 
 
357
                 bundle=None, base_revision_id=None):
 
 
358
        if source_branch is None and bundle is None:
 
 
359
            raise errors.NoMergeSource()
 
 
360
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
361
            timezone, target_branch, patch, source_branch, message)
 
 
363
        self.base_revision_id = base_revision_id
 
 
365
    def _patch_type(self):
 
 
366
        if self.bundle is not None:
 
 
368
        elif self.patch is not None:
 
 
373
    patch_type = property(_patch_type)
 
 
375
    def clear_payload(self):
 
 
379
    def get_raw_bundle(self):
 
 
380
        if self.bundle is None:
 
 
383
            return self.bundle.decode('base-64')
 
 
386
    def _from_lines(klass, line_iter):
 
 
387
        stanza = rio.read_patch_stanza(line_iter)
 
 
391
            start = line_iter.next()
 
 
392
        except StopIteration:
 
 
395
            if start.startswith('# Begin patch'):
 
 
397
                for line in line_iter:
 
 
398
                    if line.startswith('# Begin bundle'):
 
 
401
                    patch_lines.append(line)
 
 
404
                patch = ''.join(patch_lines)
 
 
405
            if start is not None:
 
 
406
                if start.startswith('# Begin bundle'):
 
 
407
                    bundle = ''.join(line_iter)
 
 
409
                    raise errors.IllegalMergeDirectivePayload(start)
 
 
410
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
412
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
413
                    'source_branch', 'message', 'base_revision_id'):
 
 
415
                kwargs[key] = stanza.get(key)
 
 
418
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
419
        kwargs['base_revision_id'] =\
 
 
420
            kwargs['base_revision_id'].encode('utf-8')
 
 
421
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
 
425
        lines = self._to_lines(base_revision=True)
 
 
426
        if self.patch is not None:
 
 
427
            lines.append('# Begin patch\n')
 
 
428
            lines.extend(self.patch.splitlines(True))
 
 
429
        if self.bundle is not None:
 
 
430
            lines.append('# Begin bundle\n')
 
 
431
            lines.extend(self.bundle.splitlines(True))
 
 
435
    def from_objects(klass, repository, revision_id, time, timezone,
 
 
436
                 target_branch, include_patch=True, include_bundle=True,
 
 
437
                 local_target_branch=None, public_branch=None, message=None,
 
 
438
                 base_revision_id=None):
 
 
439
        """Generate a merge directive from various objects
 
 
441
        :param repository: The repository containing the revision
 
 
442
        :param revision_id: The revision to merge
 
 
443
        :param time: The POSIX timestamp of the date the request was issued.
 
 
444
        :param timezone: The timezone of the request
 
 
445
        :param target_branch: The url of the branch to merge into
 
 
446
        :param include_patch: If true, include a preview patch
 
 
447
        :param include_bundle: If true, include a bundle
 
 
448
        :param local_target_branch: a local copy of the target branch
 
 
449
        :param public_branch: location of a public branch containing the target
 
 
451
        :param message: Message to use when committing the merge
 
 
452
        :return: The merge directive
 
 
454
        The public branch is always used if supplied.  If no bundle is
 
 
455
        included, the public branch must be supplied, and will be verified.
 
 
457
        If the message is not supplied, the message from revision_id will be
 
 
462
            repository.lock_write()
 
 
463
            locked.append(repository)
 
 
464
            t_revision_id = revision_id
 
 
465
            if revision_id == 'null:':
 
 
467
            t = testament.StrictTestament3.from_revision(repository,
 
 
469
            submit_branch = _mod_branch.Branch.open(target_branch)
 
 
470
            submit_branch.lock_read()
 
 
471
            locked.append(submit_branch)
 
 
472
            if submit_branch.get_public_branch() is not None:
 
 
473
                target_branch = submit_branch.get_public_branch()
 
 
474
            submit_revision_id = submit_branch.last_revision()
 
 
475
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
 
476
            graph = repository.get_graph(submit_branch.repository)
 
 
477
            ancestor_id = graph.find_unique_lca(revision_id,
 
 
479
            if base_revision_id is None:
 
 
480
                base_revision_id = ancestor_id
 
 
481
            if (include_patch, include_bundle) != (False, False):
 
 
482
                repository.fetch(submit_branch.repository, submit_revision_id)
 
 
484
                patch = klass._generate_diff(repository, revision_id,
 
 
490
                bundle = klass._generate_bundle(repository, revision_id,
 
 
491
                    ancestor_id).encode('base-64')
 
 
495
            if public_branch is not None and not include_bundle:
 
 
496
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
 
497
                public_branch_obj.lock_read()
 
 
498
                locked.append(public_branch_obj)
 
 
499
                if not public_branch_obj.repository.has_revision(
 
 
501
                    raise errors.PublicBranchOutOfDate(public_branch,
 
 
504
            for entry in reversed(locked):
 
 
506
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
 
507
            patch, public_branch, message, bundle, base_revision_id)
 
 
509
    def _verify_patch(self, repository):
 
 
510
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
 
511
                                               self.base_revision_id)
 
 
512
        # Convert line-endings to UNIX
 
 
513
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
 
514
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
 
 
515
        # Strip trailing whitespace
 
 
516
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
 
517
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
 
518
        return (calculated_patch == stored_patch)
 
 
520
    def get_merge_request(self, repository):
 
 
521
        """Provide data for performing a merge
 
 
523
        Returns suggested base, suggested target, and patch verification status
 
 
525
        verified = self._maybe_verify(repository)
 
 
526
        return self.base_revision_id, self.revision_id, verified
 
 
528
    def _maybe_verify(self, repository):
 
 
529
        if self.patch is not None:
 
 
530
            if self._verify_patch(repository):
 
 
535
            return 'inapplicable'
 
 
538
class MergeDirectiveFormatRegistry(registry.Registry):
 
 
540
    def register(self, directive, format_string=None):
 
 
541
        if format_string is None:
 
 
542
            format_string = directive._format_string
 
 
543
        registry.Registry.register(self, format_string, directive)
 
 
546
_format_registry = MergeDirectiveFormatRegistry()
 
 
547
_format_registry.register(MergeDirective)
 
 
548
_format_registry.register(MergeDirective2)
 
 
549
_format_registry.register(MergeDirective2,
 
 
550
                          'Bazaar merge directive format 2 (Bazaar 0.19)')