~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Packman
  • Date: 2011-12-23 19:38:22 UTC
  • mto: This revision was merged to the branch mainline in revision 6405.
  • Revision ID: martin.packman@canonical.com-20111223193822-hesheea4o8aqwexv
Accept and document passing the medium rather than transport for smart connections

Show diffs side-by-side

added added

removed removed

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