~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: John Arbash Meinel
  • Date: 2010-08-02 17:16:12 UTC
  • mto: This revision was merged to the branch mainline in revision 5369.
  • Revision ID: john@arbash-meinel.com-20100802171612-rdh5ods70w2bl3j7
We also have to re-implement it for _simple_set_pyx.pyx

Show diffs side-by-side

added added

removed removed

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