~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-03-28 06:58:22 UTC
  • mfrom: (2379.2.3 hpss-chroot)
  • Revision ID: pqm@pqm.ubuntu.com-20070328065822-999550a858a3ced3
(robertc) Fix chroot urls to not expose the url of the transport they are protecting, allowing regular url operations to work on them. (Robert Collins, Andrew Bennetts)

Show diffs side-by-side

added added

removed removed

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