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
        return '%s-%s' % (branch.nick, '.'.join(str(n) for n in revno))
 
 
154
    def _generate_diff(repository, revision_id, ancestor_id):
 
 
155
        tree_1 = repository.revision_tree(ancestor_id)
 
 
156
        tree_2 = repository.revision_tree(revision_id)
 
 
158
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
 
 
162
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
164
        bundle_serializer.write_bundle(repository, revision_id,
 
 
168
    def to_signed(self, branch):
 
 
169
        """Serialize as a signed string.
 
 
171
        :param branch: The source branch, to get the signing strategy
 
 
174
        my_gpg = gpg.GPGStrategy(branch.get_config())
 
 
175
        return my_gpg.sign(''.join(self.to_lines()))
 
 
177
    def to_email(self, mail_to, branch, sign=False):
 
 
178
        """Serialize as an email message.
 
 
180
        :param mail_to: The address to mail the message to
 
 
181
        :param branch: The source branch, to get the signing strategy and
 
 
183
        :param sign: If True, gpg-sign the email
 
 
184
        :return: an email message
 
 
186
        mail_from = branch.get_config().username()
 
 
187
        if self.message is not None:
 
 
188
            subject = self.message
 
 
190
            revision = branch.repository.get_revision(self.revision_id)
 
 
191
            subject = revision.message
 
 
193
            body = self.to_signed(branch)
 
 
195
            body = ''.join(self.to_lines())
 
 
196
        message = EmailMessage(mail_from, mail_to, subject, body)
 
 
199
    def install_revisions(self, target_repo):
 
 
200
        """Install revisions and return the target revision"""
 
 
201
        if not target_repo.has_revision(self.revision_id):
 
 
202
            if self.patch_type == 'bundle':
 
 
203
                info = bundle_serializer.read_bundle(
 
 
204
                    StringIO(self.get_raw_bundle()))
 
 
205
                # We don't use the bundle's target revision, because
 
 
206
                # MergeDirective.revision_id is authoritative.
 
 
208
                    info.install_revisions(target_repo, stream_input=False)
 
 
209
                except errors.RevisionNotPresent:
 
 
210
                    # At least one dependency isn't present.  Try installing
 
 
211
                    # missing revisions from the submit branch
 
 
212
                    submit_branch = _mod_branch.Branch.open(self.target_branch)
 
 
213
                    missing_revisions = []
 
 
214
                    bundle_revisions = set(r.revision_id for r in
 
 
216
                    for revision in info.real_revisions:
 
 
217
                        for parent_id in revision.parent_ids:
 
 
218
                            if (parent_id not in bundle_revisions and
 
 
219
                                not target_repo.has_revision(parent_id)):
 
 
220
                                missing_revisions.append(parent_id)
 
 
221
                    # reverse missing revisions to try to get heads first
 
 
223
                    unique_missing_set = set()
 
 
224
                    for revision in reversed(missing_revisions):
 
 
225
                        if revision in unique_missing_set:
 
 
227
                        unique_missing.append(revision)
 
 
228
                        unique_missing_set.add(revision)
 
 
229
                    for missing_revision in unique_missing:
 
 
230
                        target_repo.fetch(submit_branch.repository,
 
 
232
                    info.install_revisions(target_repo, stream_input=False)
 
 
234
                source_branch = _mod_branch.Branch.open(self.source_branch)
 
 
235
                target_repo.fetch(source_branch.repository, self.revision_id)
 
 
236
        return self.revision_id
 
 
239
class MergeDirective(_BaseMergeDirective):
 
 
241
    """A request to perform a merge into a branch.
 
 
243
    Designed to be serialized and mailed.  It provides all the information
 
 
244
    needed to perform a merge automatically, by providing at minimum a revision
 
 
245
    bundle or the location of a branch.
 
 
247
    The serialization format is robust against certain common forms of
 
 
248
    deterioration caused by mailing.
 
 
250
    The format is also designed to be patch-compatible.  If the directive
 
 
251
    includes a diff or revision bundle, it should be possible to apply it
 
 
252
    directly using the standard patch program.
 
 
255
    _format_string = 'Bazaar merge directive format 1'
 
 
257
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
258
                 target_branch, patch=None, patch_type=None,
 
 
259
                 source_branch=None, message=None, bundle=None):
 
 
262
        :param revision_id: The revision to merge
 
 
263
        :param testament_sha1: The sha1 of the testament of the revision to
 
 
265
        :param time: The current POSIX timestamp time
 
 
266
        :param timezone: The timezone offset
 
 
267
        :param target_branch: The branch to apply the merge to
 
 
268
        :param patch: The text of a diff or bundle
 
 
269
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
 
271
        :param source_branch: A public location to merge the revision from
 
 
272
        :param message: The message to use when committing this merge
 
 
274
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
275
            timezone, target_branch, patch, source_branch, message)
 
 
276
        if patch_type not in (None, 'diff', 'bundle'):
 
 
277
            raise ValueError(patch_type)
 
 
278
        if patch_type != 'bundle' and source_branch is None:
 
 
279
            raise errors.NoMergeSource()
 
 
280
        if patch_type is not None and patch is None:
 
 
281
            raise errors.PatchMissing(patch_type)
 
 
282
        self.patch_type = patch_type
 
 
284
    def clear_payload(self):
 
 
286
        self.patch_type = None
 
 
288
    def get_raw_bundle(self):
 
 
292
        if self.patch_type == 'bundle':
 
 
297
    bundle = property(_bundle)
 
 
300
    def from_lines(klass, lines):
 
 
301
        """Deserialize a MergeRequest from an iterable of lines
 
 
303
        :param lines: An iterable of lines
 
 
304
        :return: a MergeRequest
 
 
306
        line_iter = iter(lines)
 
 
307
        for line in line_iter:
 
 
308
            if line.startswith('# Bazaar merge directive format '):
 
 
312
                raise errors.NotAMergeDirective(lines[0])
 
 
314
                raise errors.NotAMergeDirective('')
 
 
315
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
 
318
    def _from_lines(klass, line_iter):
 
 
319
        stanza = rio.read_patch_stanza(line_iter)
 
 
320
        patch_lines = list(line_iter)
 
 
321
        if len(patch_lines) == 0:
 
 
325
            patch = ''.join(patch_lines)
 
 
327
                bundle_serializer.read_bundle(StringIO(patch))
 
 
328
            except (errors.NotABundle, errors.BundleNotSupported,
 
 
332
                patch_type = 'bundle'
 
 
333
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
335
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
336
                    'source_branch', 'message'):
 
 
338
                kwargs[key] = stanza.get(key)
 
 
341
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
342
        return MergeDirective(time=time, timezone=timezone,
 
 
343
                              patch_type=patch_type, patch=patch, **kwargs)
 
 
346
        lines = self._to_lines()
 
 
347
        if self.patch is not None:
 
 
348
            lines.extend(self.patch.splitlines(True))
 
 
352
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
354
        bundle_serializer.write_bundle(repository, revision_id,
 
 
355
                                       ancestor_id, s, '0.9')
 
 
358
    def get_merge_request(self, repository):
 
 
359
        """Provide data for performing a merge
 
 
361
        Returns suggested base, suggested target, and patch verification status
 
 
363
        return None, self.revision_id, 'inapplicable'
 
 
366
class MergeDirective2(_BaseMergeDirective):
 
 
368
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
 
370
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
371
                 target_branch, patch=None, source_branch=None, message=None,
 
 
372
                 bundle=None, base_revision_id=None):
 
 
373
        if source_branch is None and bundle is None:
 
 
374
            raise errors.NoMergeSource()
 
 
375
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
376
            timezone, target_branch, patch, source_branch, message)
 
 
378
        self.base_revision_id = base_revision_id
 
 
380
    def _patch_type(self):
 
 
381
        if self.bundle is not None:
 
 
383
        elif self.patch is not None:
 
 
388
    patch_type = property(_patch_type)
 
 
390
    def clear_payload(self):
 
 
394
    def get_raw_bundle(self):
 
 
395
        if self.bundle is None:
 
 
398
            return self.bundle.decode('base-64')
 
 
401
    def _from_lines(klass, line_iter):
 
 
402
        stanza = rio.read_patch_stanza(line_iter)
 
 
406
            start = line_iter.next()
 
 
407
        except StopIteration:
 
 
410
            if start.startswith('# Begin patch'):
 
 
412
                for line in line_iter:
 
 
413
                    if line.startswith('# Begin bundle'):
 
 
416
                    patch_lines.append(line)
 
 
419
                patch = ''.join(patch_lines)
 
 
420
            if start is not None:
 
 
421
                if start.startswith('# Begin bundle'):
 
 
422
                    bundle = ''.join(line_iter)
 
 
424
                    raise errors.IllegalMergeDirectivePayload(start)
 
 
425
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
427
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
428
                    'source_branch', 'message', 'base_revision_id'):
 
 
430
                kwargs[key] = stanza.get(key)
 
 
433
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
434
        kwargs['base_revision_id'] =\
 
 
435
            kwargs['base_revision_id'].encode('utf-8')
 
 
436
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
 
440
        lines = self._to_lines(base_revision=True)
 
 
441
        if self.patch is not None:
 
 
442
            lines.append('# Begin patch\n')
 
 
443
            lines.extend(self.patch.splitlines(True))
 
 
444
        if self.bundle is not None:
 
 
445
            lines.append('# Begin bundle\n')
 
 
446
            lines.extend(self.bundle.splitlines(True))
 
 
450
    def from_objects(klass, repository, revision_id, time, timezone,
 
 
451
                 target_branch, include_patch=True, include_bundle=True,
 
 
452
                 local_target_branch=None, public_branch=None, message=None,
 
 
453
                 base_revision_id=None):
 
 
454
        """Generate a merge directive from various objects
 
 
456
        :param repository: The repository containing the revision
 
 
457
        :param revision_id: The revision to merge
 
 
458
        :param time: The POSIX timestamp of the date the request was issued.
 
 
459
        :param timezone: The timezone of the request
 
 
460
        :param target_branch: The url of the branch to merge into
 
 
461
        :param include_patch: If true, include a preview patch
 
 
462
        :param include_bundle: If true, include a bundle
 
 
463
        :param local_target_branch: a local copy of the target branch
 
 
464
        :param public_branch: location of a public branch containing the target
 
 
466
        :param message: Message to use when committing the merge
 
 
467
        :return: The merge directive
 
 
469
        The public branch is always used if supplied.  If no bundle is
 
 
470
        included, the public branch must be supplied, and will be verified.
 
 
472
        If the message is not supplied, the message from revision_id will be
 
 
477
            repository.lock_write()
 
 
478
            locked.append(repository)
 
 
479
            t_revision_id = revision_id
 
 
480
            if revision_id == 'null:':
 
 
482
            t = testament.StrictTestament3.from_revision(repository,
 
 
484
            submit_branch = _mod_branch.Branch.open(target_branch)
 
 
485
            submit_branch.lock_read()
 
 
486
            locked.append(submit_branch)
 
 
487
            if submit_branch.get_public_branch() is not None:
 
 
488
                target_branch = submit_branch.get_public_branch()
 
 
489
            submit_revision_id = submit_branch.last_revision()
 
 
490
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
 
491
            graph = repository.get_graph(submit_branch.repository)
 
 
492
            ancestor_id = graph.find_unique_lca(revision_id,
 
 
494
            if base_revision_id is None:
 
 
495
                base_revision_id = ancestor_id
 
 
496
            if (include_patch, include_bundle) != (False, False):
 
 
497
                repository.fetch(submit_branch.repository, submit_revision_id)
 
 
499
                patch = klass._generate_diff(repository, revision_id,
 
 
505
                bundle = klass._generate_bundle(repository, revision_id,
 
 
506
                    ancestor_id).encode('base-64')
 
 
510
            if public_branch is not None and not include_bundle:
 
 
511
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
 
512
                public_branch_obj.lock_read()
 
 
513
                locked.append(public_branch_obj)
 
 
514
                if not public_branch_obj.repository.has_revision(
 
 
516
                    raise errors.PublicBranchOutOfDate(public_branch,
 
 
519
            for entry in reversed(locked):
 
 
521
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
 
522
            patch, public_branch, message, bundle, base_revision_id)
 
 
524
    def _verify_patch(self, repository):
 
 
525
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
 
526
                                               self.base_revision_id)
 
 
527
        # Convert line-endings to UNIX
 
 
528
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
 
529
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
 
 
530
        # Strip trailing whitespace
 
 
531
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
 
532
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
 
533
        return (calculated_patch == stored_patch)
 
 
535
    def get_merge_request(self, repository):
 
 
536
        """Provide data for performing a merge
 
 
538
        Returns suggested base, suggested target, and patch verification status
 
 
540
        verified = self._maybe_verify(repository)
 
 
541
        return self.base_revision_id, self.revision_id, verified
 
 
543
    def _maybe_verify(self, repository):
 
 
544
        if self.patch is not None:
 
 
545
            if self._verify_patch(repository):
 
 
550
            return 'inapplicable'
 
 
553
class MergeDirectiveFormatRegistry(registry.Registry):
 
 
555
    def register(self, directive, format_string=None):
 
 
556
        if format_string is None:
 
 
557
            format_string = directive._format_string
 
 
558
        registry.Registry.register(self, format_string, directive)
 
 
561
_format_registry = MergeDirectiveFormatRegistry()
 
 
562
_format_registry.register(MergeDirective)
 
 
563
_format_registry.register(MergeDirective2)
 
 
564
_format_registry.register(MergeDirective2,
 
 
565
                          'Bazaar merge directive format 2 (Bazaar 0.19)')