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 email import Message
 
 
19
from StringIO import StringIO
 
 
23
    branch as _mod_branch,
 
 
28
    revision as _mod_revision,
 
 
33
from bzrlib.bundle import (
 
 
34
    serializer as bundle_serializer,
 
 
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
        message = Message.Message()
 
 
174
        message['To'] = mail_to
 
 
175
        message['From'] = mail_from
 
 
176
        if self.message is not None:
 
 
177
            message['Subject'] = self.message
 
 
179
            revision = branch.repository.get_revision(self.revision_id)
 
 
180
            message['Subject'] = revision.message
 
 
182
            body = self.to_signed(branch)
 
 
184
            body = ''.join(self.to_lines())
 
 
185
        message.set_payload(body)
 
 
188
    def install_revisions(self, target_repo):
 
 
189
        """Install revisions and return the target revision"""
 
 
190
        if not target_repo.has_revision(self.revision_id):
 
 
191
            if self.patch_type == 'bundle':
 
 
192
                info = bundle_serializer.read_bundle(
 
 
193
                    StringIO(self.get_raw_bundle()))
 
 
194
                # We don't use the bundle's target revision, because
 
 
195
                # MergeDirective.revision_id is authoritative.
 
 
196
                info.install_revisions(target_repo)
 
 
198
                source_branch = _mod_branch.Branch.open(self.source_branch)
 
 
199
                target_repo.fetch(source_branch.repository, self.revision_id)
 
 
200
        return self.revision_id
 
 
203
class MergeDirective(_BaseMergeDirective):
 
 
205
    """A request to perform a merge into a branch.
 
 
207
    Designed to be serialized and mailed.  It provides all the information
 
 
208
    needed to perform a merge automatically, by providing at minimum a revision
 
 
209
    bundle or the location of a branch.
 
 
211
    The serialization format is robust against certain common forms of
 
 
212
    deterioration caused by mailing.
 
 
214
    The format is also designed to be patch-compatible.  If the directive
 
 
215
    includes a diff or revision bundle, it should be possible to apply it
 
 
216
    directly using the standard patch program.
 
 
219
    _format_string = 'Bazaar merge directive format 1'
 
 
221
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
222
                 target_branch, patch=None, patch_type=None,
 
 
223
                 source_branch=None, message=None, bundle=None):
 
 
226
        :param revision_id: The revision to merge
 
 
227
        :param testament_sha1: The sha1 of the testament of the revision to
 
 
229
        :param time: The current POSIX timestamp time
 
 
230
        :param timezone: The timezone offset
 
 
231
        :param target_branch: The branch to apply the merge to
 
 
232
        :param patch: The text of a diff or bundle
 
 
233
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
 
235
        :param source_branch: A public location to merge the revision from
 
 
236
        :param message: The message to use when committing this merge
 
 
238
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
239
            timezone, target_branch, patch, source_branch, message)
 
 
240
        assert patch_type in (None, 'diff', 'bundle'), patch_type
 
 
241
        if patch_type != 'bundle' and source_branch is None:
 
 
242
            raise errors.NoMergeSource()
 
 
243
        if patch_type is not None and patch is None:
 
 
244
            raise errors.PatchMissing(patch_type)
 
 
245
        self.patch_type = patch_type
 
 
247
    def clear_payload(self):
 
 
249
        self.patch_type = None
 
 
251
    def get_raw_bundle(self):
 
 
255
        if self.patch_type == 'bundle':
 
 
260
    bundle = property(_bundle)
 
 
263
    def from_lines(klass, lines):
 
 
264
        """Deserialize a MergeRequest from an iterable of lines
 
 
266
        :param lines: An iterable of lines
 
 
267
        :return: a MergeRequest
 
 
269
        line_iter = iter(lines)
 
 
270
        for line in line_iter:
 
 
271
            if line.startswith('# Bazaar merge directive format '):
 
 
275
                raise errors.NotAMergeDirective(lines[0])
 
 
277
                raise errors.NotAMergeDirective('')
 
 
278
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
 
281
    def _from_lines(klass, line_iter):
 
 
282
        stanza = rio.read_patch_stanza(line_iter)
 
 
283
        patch_lines = list(line_iter)
 
 
284
        if len(patch_lines) == 0:
 
 
288
            patch = ''.join(patch_lines)
 
 
290
                bundle_serializer.read_bundle(StringIO(patch))
 
 
291
            except (errors.NotABundle, errors.BundleNotSupported,
 
 
295
                patch_type = 'bundle'
 
 
296
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
298
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
299
                    'source_branch', 'message'):
 
 
301
                kwargs[key] = stanza.get(key)
 
 
304
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
305
        return MergeDirective(time=time, timezone=timezone,
 
 
306
                              patch_type=patch_type, patch=patch, **kwargs)
 
 
309
        lines = self._to_lines()
 
 
310
        if self.patch is not None:
 
 
311
            lines.extend(self.patch.splitlines(True))
 
 
315
    def _generate_bundle(repository, revision_id, ancestor_id):
 
 
317
        bundle_serializer.write_bundle(repository, revision_id,
 
 
318
                                       ancestor_id, s, '0.9')
 
 
321
    def get_merge_request(self, repository):
 
 
322
        """Provide data for performing a merge
 
 
324
        Returns suggested base, suggested target, and patch verification status
 
 
326
        return None, self.revision_id, 'inapplicable'
 
 
329
class MergeDirective2(_BaseMergeDirective):
 
 
331
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.19)'
 
 
333
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
 
334
                 target_branch, patch=None, source_branch=None, message=None,
 
 
335
                 bundle=None, base_revision_id=None):
 
 
336
        if source_branch is None and bundle is None:
 
 
337
            raise errors.NoMergeSource()
 
 
338
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
 
339
            timezone, target_branch, patch, source_branch, message)
 
 
341
        self.base_revision_id = base_revision_id
 
 
343
    def _patch_type(self):
 
 
344
        if self.bundle is not None:
 
 
346
        elif self.patch is not None:
 
 
351
    patch_type = property(_patch_type)
 
 
353
    def clear_payload(self):
 
 
357
    def get_raw_bundle(self):
 
 
358
        if self.bundle is None:
 
 
361
            return self.bundle.decode('base-64')
 
 
364
    def _from_lines(klass, line_iter):
 
 
365
        stanza = rio.read_patch_stanza(line_iter)
 
 
369
            start = line_iter.next()
 
 
370
        except StopIteration:
 
 
373
            if start.startswith('# Begin patch'):
 
 
375
                for line in line_iter:
 
 
376
                    if line.startswith('# Begin bundle'):
 
 
379
                    patch_lines.append(line)
 
 
382
                patch = ''.join(patch_lines)
 
 
383
            if start is not None:
 
 
384
                if start.startswith('# Begin bundle'):
 
 
385
                    bundle = ''.join(line_iter)
 
 
387
                    raise errors.IllegalMergeDirectivePayload(start)
 
 
388
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
 
390
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
 
391
                    'source_branch', 'message', 'base_revision_id'):
 
 
393
                kwargs[key] = stanza.get(key)
 
 
396
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
 
397
        kwargs['base_revision_id'] =\
 
 
398
            kwargs['base_revision_id'].encode('utf-8')
 
 
399
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
 
403
        lines = self._to_lines(base_revision=True)
 
 
404
        if self.patch is not None:
 
 
405
            lines.append('# Begin patch\n')
 
 
406
            lines.extend(self.patch.splitlines(True))
 
 
407
        if self.bundle is not None:
 
 
408
            lines.append('# Begin bundle\n')
 
 
409
            lines.extend(self.bundle.splitlines(True))
 
 
413
    def from_objects(klass, repository, revision_id, time, timezone,
 
 
414
                 target_branch, include_patch=True, include_bundle=True,
 
 
415
                 local_target_branch=None, public_branch=None, message=None,
 
 
416
                 base_revision_id=None):
 
 
417
        """Generate a merge directive from various objects
 
 
419
        :param repository: The repository containing the revision
 
 
420
        :param revision_id: The revision to merge
 
 
421
        :param time: The POSIX timestamp of the date the request was issued.
 
 
422
        :param timezone: The timezone of the request
 
 
423
        :param target_branch: The url of the branch to merge into
 
 
424
        :param include_patch: If true, include a preview patch
 
 
425
        :param include_bundle: If true, include a bundle
 
 
426
        :param local_target_branch: a local copy of the target branch
 
 
427
        :param public_branch: location of a public branch containing the target
 
 
429
        :param message: Message to use when committing the merge
 
 
430
        :return: The merge directive
 
 
432
        The public branch is always used if supplied.  If no bundle is
 
 
433
        included, the public branch must be supplied, and will be verified.
 
 
435
        If the message is not supplied, the message from revision_id will be
 
 
440
            repository.lock_write()
 
 
441
            locked.append(repository)
 
 
442
            t_revision_id = revision_id
 
 
443
            if revision_id == 'null:':
 
 
445
            t = testament.StrictTestament3.from_revision(repository,
 
 
447
            submit_branch = _mod_branch.Branch.open(target_branch)
 
 
448
            submit_branch.lock_read()
 
 
449
            locked.append(submit_branch)
 
 
450
            if submit_branch.get_public_branch() is not None:
 
 
451
                target_branch = submit_branch.get_public_branch()
 
 
452
            submit_revision_id = submit_branch.last_revision()
 
 
453
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
 
454
            graph = repository.get_graph(submit_branch.repository)
 
 
455
            ancestor_id = graph.find_unique_lca(revision_id,
 
 
457
            if base_revision_id is None:
 
 
458
                base_revision_id = ancestor_id
 
 
459
            if (include_patch, include_bundle) != (False, False):
 
 
460
                repository.fetch(submit_branch.repository, submit_revision_id)
 
 
462
                patch = klass._generate_diff(repository, revision_id,
 
 
468
                bundle = klass._generate_bundle(repository, revision_id,
 
 
469
                    ancestor_id).encode('base-64')
 
 
473
            if public_branch is not None and not include_bundle:
 
 
474
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
 
475
                public_branch_obj.lock_read()
 
 
476
                locked.append(public_branch_obj)
 
 
477
                if not public_branch_obj.repository.has_revision(
 
 
479
                    raise errors.PublicBranchOutOfDate(public_branch,
 
 
482
            for entry in reversed(locked):
 
 
484
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
 
485
            patch, public_branch, message, bundle, base_revision_id)
 
 
487
    def _verify_patch(self, repository):
 
 
488
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
 
489
                                               self.base_revision_id)
 
 
490
        # Convert line-endings to UNIX
 
 
491
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
 
492
        # Strip trailing whitespace
 
 
493
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
 
494
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
 
495
        return (calculated_patch == stored_patch)
 
 
497
    def get_merge_request(self, repository):
 
 
498
        """Provide data for performing a merge
 
 
500
        Returns suggested base, suggested target, and patch verification status
 
 
502
        verified = self._maybe_verify(repository)
 
 
503
        return self.base_revision_id, self.revision_id, verified
 
 
505
    def _maybe_verify(self, repository):
 
 
506
        if self.patch is not None:
 
 
507
            if self._verify_patch(repository):
 
 
512
            return 'inapplicable'
 
 
515
class MergeDirectiveFormatRegistry(registry.Registry):
 
 
517
    def register(self, directive):
 
 
518
        registry.Registry.register(self, directive._format_string, directive)
 
 
521
_format_registry = MergeDirectiveFormatRegistry()
 
 
522
_format_registry.register(MergeDirective)
 
 
523
_format_registry.register(MergeDirective2)