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)
 
 
139
    def get_disk_name(self, branch):
 
 
140
        """Generate a suitable basename for storing this directive on disk
 
 
142
        :param branch: The Branch this merge directive was generated fro
 
 
145
        revno, revision_id = branch.last_revision_info()
 
 
146
        if self.revision_id == revision_id:
 
 
149
            revno = branch.get_revision_id_to_revno_map().get(self.revision_id,
 
 
151
        nick = re.sub('(\W+)', '-', branch.nick).strip('-')
 
 
152
        return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
 
 
155
    def _generate_diff(repository, revision_id, ancestor_id):
 
 
156
        tree_1 = repository.revision_tree(ancestor_id)
 
 
157
        tree_2 = repository.revision_tree(revision_id)
 
 
159
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
 
 
163
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
165
        bundle_serializer.write_bundle(repository, revision_id,
 
 
169
    def to_signed(self, branch):
 
 
170
        """Serialize as a signed string.
 
 
172
        :param branch: The source branch, to get the signing strategy
 
 
175
        my_gpg = gpg.GPGStrategy(branch.get_config())
 
 
176
        return my_gpg.sign(''.join(self.to_lines()))
 
 
178
    def to_email(self, mail_to, branch, sign=False):
 
 
179
        """Serialize as an email message.
 
 
181
        :param mail_to: The address to mail the message to
 
 
182
        :param branch: The source branch, to get the signing strategy and
 
 
184
        :param sign: If True, gpg-sign the email
 
 
185
        :return: an email message
 
 
187
        mail_from = branch.get_config().username()
 
 
188
        if self.message is not None:
 
 
189
            subject = self.message
 
 
191
            revision = branch.repository.get_revision(self.revision_id)
 
 
192
            subject = revision.message
 
 
194
            body = self.to_signed(branch)
 
 
196
            body = ''.join(self.to_lines())
 
 
197
        message = EmailMessage(mail_from, mail_to, subject, body)
 
 
200
    def install_revisions(self, target_repo):
 
 
201
        """Install revisions and return the target revision"""
 
 
202
        if not target_repo.has_revision(self.revision_id):
 
 
203
            if self.patch_type == 'bundle':
 
 
204
                info = bundle_serializer.read_bundle(
 
 
205
                    StringIO(self.get_raw_bundle()))
 
 
206
                # We don't use the bundle's target revision, because
 
 
207
                # MergeDirective.revision_id is authoritative.
 
 
209
                    info.install_revisions(target_repo, stream_input=False)
 
 
210
                except errors.RevisionNotPresent:
 
 
211
                    # At least one dependency isn't present.  Try installing
 
 
212
                    # missing revisions from the submit branch
 
 
215
                            _mod_branch.Branch.open(self.target_branch)
 
 
216
                    except errors.NotBranchError:
 
 
217
                        raise errors.TargetNotBranch(self.target_branch)
 
 
218
                    missing_revisions = []
 
 
219
                    bundle_revisions = set(r.revision_id for r in
 
 
221
                    for revision in info.real_revisions:
 
 
222
                        for parent_id in revision.parent_ids:
 
 
223
                            if (parent_id not in bundle_revisions and
 
 
224
                                not target_repo.has_revision(parent_id)):
 
 
225
                                missing_revisions.append(parent_id)
 
 
226
                    # reverse missing revisions to try to get heads first
 
 
228
                    unique_missing_set = set()
 
 
229
                    for revision in reversed(missing_revisions):
 
 
230
                        if revision in unique_missing_set:
 
 
232
                        unique_missing.append(revision)
 
 
233
                        unique_missing_set.add(revision)
 
 
234
                    for missing_revision in unique_missing:
 
 
235
                        target_repo.fetch(submit_branch.repository,
 
 
237
                    info.install_revisions(target_repo, stream_input=False)
 
 
239
                source_branch = _mod_branch.Branch.open(self.source_branch)
 
 
240
                target_repo.fetch(source_branch.repository, self.revision_id)
 
 
241
        return self.revision_id
 
 
244
class MergeDirective(_BaseMergeDirective):
 
 
246
    """A request to perform a merge into a branch.
 
 
248
    Designed to be serialized and mailed.  It provides all the information
 
 
249
    needed to perform a merge automatically, by providing at minimum a revision
 
 
250
    bundle or the location of a branch.
 
 
252
    The serialization format is robust against certain common forms of
 
 
253
    deterioration caused by mailing.
 
 
255
    The format is also designed to be patch-compatible.  If the directive
 
 
256
    includes a diff or revision bundle, it should be possible to apply it
 
 
257
    directly using the standard patch program.
 
 
260
    _format_string = 'Bazaar merge directive format 1'
 
 
262
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
263
                 target_branch, patch=None, patch_type=None,
 
 
264
                 source_branch=None, message=None, bundle=None):
 
 
267
        :param revision_id: The revision to merge
 
 
268
        :param testament_sha1: The sha1 of the testament of the revision to
 
 
270
        :param time: The current POSIX timestamp time
 
 
271
        :param timezone: The timezone offset
 
 
272
        :param target_branch: The branch to apply the merge to
 
 
273
        :param patch: The text of a diff or bundle
 
 
274
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
 
276
        :param source_branch: A public location to merge the revision from
 
 
277
        :param message: The message to use when committing this merge
 
 
279
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
280
            timezone, target_branch, patch, source_branch, message)
 
 
281
        if patch_type not in (None, 'diff', 'bundle'):
 
 
282
            raise ValueError(patch_type)
 
 
283
        if patch_type != 'bundle' and source_branch is None:
 
 
284
            raise errors.NoMergeSource()
 
 
285
        if patch_type is not None and patch is None:
 
 
286
            raise errors.PatchMissing(patch_type)
 
 
287
        self.patch_type = patch_type
 
 
289
    def clear_payload(self):
 
 
291
        self.patch_type = None
 
 
293
    def get_raw_bundle(self):
 
 
297
        if self.patch_type == 'bundle':
 
 
302
    bundle = property(_bundle)
 
 
305
    def from_lines(klass, lines):
 
 
306
        """Deserialize a MergeRequest from an iterable of lines
 
 
308
        :param lines: An iterable of lines
 
 
309
        :return: a MergeRequest
 
 
311
        line_iter = iter(lines)
 
 
312
        for line in line_iter:
 
 
313
            if line.startswith('# Bazaar merge directive format '):
 
 
317
                raise errors.NotAMergeDirective(lines[0])
 
 
319
                raise errors.NotAMergeDirective('')
 
 
320
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
 
323
    def _from_lines(klass, line_iter):
 
 
324
        stanza = rio.read_patch_stanza(line_iter)
 
 
325
        patch_lines = list(line_iter)
 
 
326
        if len(patch_lines) == 0:
 
 
330
            patch = ''.join(patch_lines)
 
 
332
                bundle_serializer.read_bundle(StringIO(patch))
 
 
333
            except (errors.NotABundle, errors.BundleNotSupported,
 
 
337
                patch_type = 'bundle'
 
 
338
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
340
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
341
                    'source_branch', 'message'):
 
 
343
                kwargs[key] = stanza.get(key)
 
 
346
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
347
        return MergeDirective(time=time, timezone=timezone,
 
 
348
                              patch_type=patch_type, patch=patch, **kwargs)
 
 
351
        lines = self._to_lines()
 
 
352
        if self.patch is not None:
 
 
353
            lines.extend(self.patch.splitlines(True))
 
 
357
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
359
        bundle_serializer.write_bundle(repository, revision_id,
 
 
360
                                       ancestor_id, s, '0.9')
 
 
363
    def get_merge_request(self, repository):
 
 
364
        """Provide data for performing a merge
 
 
366
        Returns suggested base, suggested target, and patch verification status
 
 
368
        return None, self.revision_id, 'inapplicable'
 
 
371
class MergeDirective2(_BaseMergeDirective):
 
 
373
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
 
375
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
376
                 target_branch, patch=None, source_branch=None, message=None,
 
 
377
                 bundle=None, base_revision_id=None):
 
 
378
        if source_branch is None and bundle is None:
 
 
379
            raise errors.NoMergeSource()
 
 
380
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
381
            timezone, target_branch, patch, source_branch, message)
 
 
383
        self.base_revision_id = base_revision_id
 
 
385
    def _patch_type(self):
 
 
386
        if self.bundle is not None:
 
 
388
        elif self.patch is not None:
 
 
393
    patch_type = property(_patch_type)
 
 
395
    def clear_payload(self):
 
 
399
    def get_raw_bundle(self):
 
 
400
        if self.bundle is None:
 
 
403
            return self.bundle.decode('base-64')
 
 
406
    def _from_lines(klass, line_iter):
 
 
407
        stanza = rio.read_patch_stanza(line_iter)
 
 
411
            start = line_iter.next()
 
 
412
        except StopIteration:
 
 
415
            if start.startswith('# Begin patch'):
 
 
417
                for line in line_iter:
 
 
418
                    if line.startswith('# Begin bundle'):
 
 
421
                    patch_lines.append(line)
 
 
424
                patch = ''.join(patch_lines)
 
 
425
            if start is not None:
 
 
426
                if start.startswith('# Begin bundle'):
 
 
427
                    bundle = ''.join(line_iter)
 
 
429
                    raise errors.IllegalMergeDirectivePayload(start)
 
 
430
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
432
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
433
                    'source_branch', 'message', 'base_revision_id'):
 
 
435
                kwargs[key] = stanza.get(key)
 
 
438
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
439
        kwargs['base_revision_id'] =\
 
 
440
            kwargs['base_revision_id'].encode('utf-8')
 
 
441
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
 
445
        lines = self._to_lines(base_revision=True)
 
 
446
        if self.patch is not None:
 
 
447
            lines.append('# Begin patch\n')
 
 
448
            lines.extend(self.patch.splitlines(True))
 
 
449
        if self.bundle is not None:
 
 
450
            lines.append('# Begin bundle\n')
 
 
451
            lines.extend(self.bundle.splitlines(True))
 
 
455
    def from_objects(klass, repository, revision_id, time, timezone,
 
 
456
                 target_branch, include_patch=True, include_bundle=True,
 
 
457
                 local_target_branch=None, public_branch=None, message=None,
 
 
458
                 base_revision_id=None):
 
 
459
        """Generate a merge directive from various objects
 
 
461
        :param repository: The repository containing the revision
 
 
462
        :param revision_id: The revision to merge
 
 
463
        :param time: The POSIX timestamp of the date the request was issued.
 
 
464
        :param timezone: The timezone of the request
 
 
465
        :param target_branch: The url of the branch to merge into
 
 
466
        :param include_patch: If true, include a preview patch
 
 
467
        :param include_bundle: If true, include a bundle
 
 
468
        :param local_target_branch: a local copy of the target branch
 
 
469
        :param public_branch: location of a public branch containing the target
 
 
471
        :param message: Message to use when committing the merge
 
 
472
        :return: The merge directive
 
 
474
        The public branch is always used if supplied.  If no bundle is
 
 
475
        included, the public branch must be supplied, and will be verified.
 
 
477
        If the message is not supplied, the message from revision_id will be
 
 
482
            repository.lock_write()
 
 
483
            locked.append(repository)
 
 
484
            t_revision_id = revision_id
 
 
485
            if revision_id == 'null:':
 
 
487
            t = testament.StrictTestament3.from_revision(repository,
 
 
489
            submit_branch = _mod_branch.Branch.open(target_branch)
 
 
490
            submit_branch.lock_read()
 
 
491
            locked.append(submit_branch)
 
 
492
            if submit_branch.get_public_branch() is not None:
 
 
493
                target_branch = submit_branch.get_public_branch()
 
 
494
            submit_revision_id = submit_branch.last_revision()
 
 
495
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
 
496
            graph = repository.get_graph(submit_branch.repository)
 
 
497
            ancestor_id = graph.find_unique_lca(revision_id,
 
 
499
            if base_revision_id is None:
 
 
500
                base_revision_id = ancestor_id
 
 
501
            if (include_patch, include_bundle) != (False, False):
 
 
502
                repository.fetch(submit_branch.repository, submit_revision_id)
 
 
504
                patch = klass._generate_diff(repository, revision_id,
 
 
510
                bundle = klass._generate_bundle(repository, revision_id,
 
 
511
                    ancestor_id).encode('base-64')
 
 
515
            if public_branch is not None and not include_bundle:
 
 
516
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
 
517
                public_branch_obj.lock_read()
 
 
518
                locked.append(public_branch_obj)
 
 
519
                if not public_branch_obj.repository.has_revision(
 
 
521
                    raise errors.PublicBranchOutOfDate(public_branch,
 
 
524
            for entry in reversed(locked):
 
 
526
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
 
527
            patch, public_branch, message, bundle, base_revision_id)
 
 
529
    def _verify_patch(self, repository):
 
 
530
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
 
531
                                               self.base_revision_id)
 
 
532
        # Convert line-endings to UNIX
 
 
533
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
 
534
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
 
 
535
        # Strip trailing whitespace
 
 
536
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
 
537
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
 
538
        return (calculated_patch == stored_patch)
 
 
540
    def get_merge_request(self, repository):
 
 
541
        """Provide data for performing a merge
 
 
543
        Returns suggested base, suggested target, and patch verification status
 
 
545
        verified = self._maybe_verify(repository)
 
 
546
        return self.base_revision_id, self.revision_id, verified
 
 
548
    def _maybe_verify(self, repository):
 
 
549
        if self.patch is not None:
 
 
550
            if self._verify_patch(repository):
 
 
555
            return 'inapplicable'
 
 
558
class MergeDirectiveFormatRegistry(registry.Registry):
 
 
560
    def register(self, directive, format_string=None):
 
 
561
        if format_string is None:
 
 
562
            format_string = directive._format_string
 
 
563
        registry.Registry.register(self, format_string, directive)
 
 
566
_format_registry = MergeDirectiveFormatRegistry()
 
 
567
_format_registry.register(MergeDirective)
 
 
568
_format_registry.register(MergeDirective2)
 
 
569
_format_registry.register(MergeDirective2,
 
 
570
                          'Bazaar merge directive format 2 (Bazaar 0.19)')