~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: John Arbash Meinel
  • Date: 2007-07-20 14:28:59 UTC
  • mfrom: (2625.6.3 bzr.email_message)
  • mto: This revision was merged to the branch mainline in revision 2640.
  • Revision ID: john@arbash-meinel.com-20070720142859-a24s0khul0yw91bh
(Adeodato Simó) EmailMessage class, allowing much nicer access to Email object than stdlib

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
from email import Message
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
    registry,
26
27
    revision as _mod_revision,
27
28
    rio,
28
29
    testament,
31
32
from bzrlib.bundle import (
32
33
    serializer as bundle_serializer,
33
34
    )
34
 
 
35
 
 
36
 
class MergeDirective(object):
37
 
 
38
 
    """A request to perform a merge into a branch.
39
 
 
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.
50
 
    """
51
 
 
52
 
    _format_string = 'Bazaar merge directive format 1'
 
35
from bzrlib.email_message import EmailMessage
 
36
 
 
37
 
 
38
class _BaseMergeDirective(object):
53
39
 
54
40
    def __init__(self, revision_id, testament_sha1, time, timezone,
55
 
                 target_branch, patch=None, patch_type=None,
56
 
                 source_branch=None, message=None):
 
41
                 target_branch, patch=None, source_branch=None, message=None,
 
42
                 bundle=None):
57
43
        """Constructor.
58
44
 
59
45
        :param revision_id: The revision to merge
63
49
        :param timezone: The timezone offset
64
50
        :param target_branch: The branch to apply the merge to
65
51
        :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
52
        :param source_branch: A public location to merge the revision from
69
53
        :param message: The message to use when committing this merge
70
54
        """
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
55
        self.revision_id = revision_id
77
56
        self.testament_sha1 = testament_sha1
78
57
        self.time = time
79
58
        self.timezone = timezone
80
59
        self.target_branch = target_branch
81
60
        self.patch = patch
82
 
        self.patch_type = patch_type
83
61
        self.source_branch = source_branch
84
62
        self.message = message
85
63
 
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
 
    def to_lines(self):
 
64
    def _to_lines(self, base_revision=False):
129
65
        """Serialize as a list of lines
130
66
 
131
67
        :return: a list of lines
137
73
        for key in ('source_branch', 'message'):
138
74
            if self.__dict__[key] is not None:
139
75
                stanza.add(key, self.__dict__[key])
 
76
        if base_revision:
 
77
            stanza.add('base_revision_id', self.base_revision_id)
140
78
        lines = ['# ' + self._format_string + '\n']
141
79
        lines.extend(rio.to_patch_lines(stanza))
142
80
        lines.append('# \n')
143
 
        if self.patch is not None:
144
 
            lines.extend(self.patch.splitlines(True))
145
81
        return lines
146
82
 
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
180
 
 
181
83
    @classmethod
182
84
    def from_objects(klass, repository, revision_id, time, timezone,
183
85
                 target_branch, patch_type='bundle',
204
106
        used for the commit.
205
107
        """
206
108
        t_revision_id = revision_id
207
 
        if revision_id == 'null:':
 
109
        if revision_id == _mod_revision.NULL_REVISION:
208
110
            t_revision_id = None
209
111
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
210
112
        submit_branch = _mod_branch.Branch.open(target_branch)
224
126
                            None: lambda x, y, z: None }
225
127
            patch = type_handler[patch_type](repository, revision_id,
226
128
                                             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)
 
129
 
 
130
        if public_branch is not None and patch_type != 'bundle':
 
131
            public_branch_obj = _mod_branch.Branch.open(public_branch)
 
132
            if not public_branch_obj.repository.has_revision(revision_id):
 
133
                raise errors.PublicBranchOutOfDate(public_branch,
 
134
                                                   revision_id)
 
135
 
 
136
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
137
            patch, patch_type, public_branch, message)
245
138
 
246
139
    @staticmethod
247
140
    def _generate_diff(repository, revision_id, ancestor_id):
258
151
                                       ancestor_id, s)
259
152
        return s.getvalue()
260
153
 
 
154
    def to_signed(self, branch):
 
155
        """Serialize as a signed string.
 
156
 
 
157
        :param branch: The source branch, to get the signing strategy
 
158
        :return: a string
 
159
        """
 
160
        my_gpg = gpg.GPGStrategy(branch.get_config())
 
161
        return my_gpg.sign(''.join(self.to_lines()))
 
162
 
 
163
    def to_email(self, mail_to, branch, sign=False):
 
164
        """Serialize as an email message.
 
165
 
 
166
        :param mail_to: The address to mail the message to
 
167
        :param branch: The source branch, to get the signing strategy and
 
168
            source email address
 
169
        :param sign: If True, gpg-sign the email
 
170
        :return: an email message
 
171
        """
 
172
        mail_from = branch.get_config().username()
 
173
        if self.message is not None:
 
174
            subject = self.message
 
175
        else:
 
176
            revision = branch.repository.get_revision(self.revision_id)
 
177
            subject = revision.message
 
178
        if sign:
 
179
            body = self.to_signed(branch)
 
180
        else:
 
181
            body = ''.join(self.to_lines())
 
182
        message = EmailMessage(mail_from, mail_to, subject, body)
 
183
        return message
 
184
 
261
185
    def install_revisions(self, target_repo):
262
186
        """Install revisions and return the target revision"""
263
187
        if not target_repo.has_revision(self.revision_id):
264
188
            if self.patch_type == 'bundle':
265
 
                info = bundle_serializer.read_bundle(StringIO(self.patch))
 
189
                info = bundle_serializer.read_bundle(
 
190
                    StringIO(self.get_raw_bundle()))
266
191
                # We don't use the bundle's target revision, because
267
192
                # MergeDirective.revision_id is authoritative.
268
193
                info.install_revisions(target_repo)
270
195
                source_branch = _mod_branch.Branch.open(self.source_branch)
271
196
                target_repo.fetch(source_branch.repository, self.revision_id)
272
197
        return self.revision_id
 
198
 
 
199
 
 
200
class MergeDirective(_BaseMergeDirective):
 
201
 
 
202
    """A request to perform a merge into a branch.
 
203
 
 
204
    Designed to be serialized and mailed.  It provides all the information
 
205
    needed to perform a merge automatically, by providing at minimum a revision
 
206
    bundle or the location of a branch.
 
207
 
 
208
    The serialization format is robust against certain common forms of
 
209
    deterioration caused by mailing.
 
210
 
 
211
    The format is also designed to be patch-compatible.  If the directive
 
212
    includes a diff or revision bundle, it should be possible to apply it
 
213
    directly using the standard patch program.
 
214
    """
 
215
 
 
216
    _format_string = 'Bazaar merge directive format 1'
 
217
 
 
218
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
219
                 target_branch, patch=None, patch_type=None,
 
220
                 source_branch=None, message=None, bundle=None):
 
221
        """Constructor.
 
222
 
 
223
        :param revision_id: The revision to merge
 
224
        :param testament_sha1: The sha1 of the testament of the revision to
 
225
            merge.
 
226
        :param time: The current POSIX timestamp time
 
227
        :param timezone: The timezone offset
 
228
        :param target_branch: The branch to apply the merge to
 
229
        :param patch: The text of a diff or bundle
 
230
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
231
            of patch
 
232
        :param source_branch: A public location to merge the revision from
 
233
        :param message: The message to use when committing this merge
 
234
        """
 
235
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
236
            timezone, target_branch, patch, source_branch, message)
 
237
        assert patch_type in (None, 'diff', 'bundle'), patch_type
 
238
        if patch_type != 'bundle' and source_branch is None:
 
239
            raise errors.NoMergeSource()
 
240
        if patch_type is not None and patch is None:
 
241
            raise errors.PatchMissing(patch_type)
 
242
        self.patch_type = patch_type
 
243
 
 
244
    def clear_payload(self):
 
245
        self.patch = None
 
246
        self.patch_type = None
 
247
 
 
248
    def get_raw_bundle(self):
 
249
        return self.bundle
 
250
 
 
251
    def _bundle(self):
 
252
        if self.patch_type == 'bundle':
 
253
            return self.patch
 
254
        else:
 
255
            return None
 
256
 
 
257
    bundle = property(_bundle)
 
258
 
 
259
    @classmethod
 
260
    def from_lines(klass, lines):
 
261
        """Deserialize a MergeRequest from an iterable of lines
 
262
 
 
263
        :param lines: An iterable of lines
 
264
        :return: a MergeRequest
 
265
        """
 
266
        line_iter = iter(lines)
 
267
        for line in line_iter:
 
268
            if line.startswith('# Bazaar merge directive format '):
 
269
                break
 
270
        else:
 
271
            if len(lines) > 0:
 
272
                raise errors.NotAMergeDirective(lines[0])
 
273
            else:
 
274
                raise errors.NotAMergeDirective('')
 
275
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
 
276
 
 
277
    @classmethod
 
278
    def _from_lines(klass, line_iter):
 
279
        stanza = rio.read_patch_stanza(line_iter)
 
280
        patch_lines = list(line_iter)
 
281
        if len(patch_lines) == 0:
 
282
            patch = None
 
283
            patch_type = None
 
284
        else:
 
285
            patch = ''.join(patch_lines)
 
286
            try:
 
287
                bundle_serializer.read_bundle(StringIO(patch))
 
288
            except (errors.NotABundle, errors.BundleNotSupported,
 
289
                    errors.BadBundle):
 
290
                patch_type = 'diff'
 
291
            else:
 
292
                patch_type = 'bundle'
 
293
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
294
        kwargs = {}
 
295
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
296
                    'source_branch', 'message'):
 
297
            try:
 
298
                kwargs[key] = stanza.get(key)
 
299
            except KeyError:
 
300
                pass
 
301
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
302
        return MergeDirective(time=time, timezone=timezone,
 
303
                              patch_type=patch_type, patch=patch, **kwargs)
 
304
 
 
305
    def to_lines(self):
 
306
        lines = self._to_lines()
 
307
        if self.patch is not None:
 
308
            lines.extend(self.patch.splitlines(True))
 
309
        return lines
 
310
 
 
311
    @staticmethod
 
312
    def _generate_bundle(repository, revision_id, ancestor_id):
 
313
        s = StringIO()
 
314
        bundle_serializer.write_bundle(repository, revision_id,
 
315
                                       ancestor_id, s, '0.9')
 
316
        return s.getvalue()
 
317
 
 
318
    def get_merge_request(self, repository):
 
319
        """Provide data for performing a merge
 
320
 
 
321
        Returns suggested base, suggested target, and patch verification status
 
322
        """
 
323
        return None, self.revision_id, 'inapplicable'
 
324
 
 
325
 
 
326
class MergeDirective2(_BaseMergeDirective):
 
327
 
 
328
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.19)'
 
329
 
 
330
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
331
                 target_branch, patch=None, source_branch=None, message=None,
 
332
                 bundle=None, base_revision_id=None):
 
333
        if source_branch is None and bundle is None:
 
334
            raise errors.NoMergeSource()
 
335
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
336
            timezone, target_branch, patch, source_branch, message)
 
337
        self.bundle = bundle
 
338
        self.base_revision_id = base_revision_id
 
339
 
 
340
    def _patch_type(self):
 
341
        if self.bundle is not None:
 
342
            return 'bundle'
 
343
        elif self.patch is not None:
 
344
            return 'diff'
 
345
        else:
 
346
            return None
 
347
 
 
348
    patch_type = property(_patch_type)
 
349
 
 
350
    def clear_payload(self):
 
351
        self.patch = None
 
352
        self.bundle = None
 
353
 
 
354
    def get_raw_bundle(self):
 
355
        if self.bundle is None:
 
356
            return None
 
357
        else:
 
358
            return self.bundle.decode('base-64')
 
359
 
 
360
    @classmethod
 
361
    def _from_lines(klass, line_iter):
 
362
        stanza = rio.read_patch_stanza(line_iter)
 
363
        patch = None
 
364
        bundle = None
 
365
        try:
 
366
            start = line_iter.next()
 
367
        except StopIteration:
 
368
            pass
 
369
        else:
 
370
            if start.startswith('# Begin patch'):
 
371
                patch_lines = []
 
372
                for line in line_iter:
 
373
                    if line.startswith('# Begin bundle'):
 
374
                        start = line
 
375
                        break
 
376
                    patch_lines.append(line)
 
377
                else:
 
378
                    start = None
 
379
                patch = ''.join(patch_lines)
 
380
            if start is not None:
 
381
                if start.startswith('# Begin bundle'):
 
382
                    bundle = ''.join(line_iter)
 
383
                else:
 
384
                    raise errors.IllegalMergeDirectivePayload(start)
 
385
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
386
        kwargs = {}
 
387
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
388
                    'source_branch', 'message', 'base_revision_id'):
 
389
            try:
 
390
                kwargs[key] = stanza.get(key)
 
391
            except KeyError:
 
392
                pass
 
393
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
394
        kwargs['base_revision_id'] =\
 
395
            kwargs['base_revision_id'].encode('utf-8')
 
396
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
397
                     **kwargs)
 
398
 
 
399
    def to_lines(self):
 
400
        lines = self._to_lines(base_revision=True)
 
401
        if self.patch is not None:
 
402
            lines.append('# Begin patch\n')
 
403
            lines.extend(self.patch.splitlines(True))
 
404
        if self.bundle is not None:
 
405
            lines.append('# Begin bundle\n')
 
406
            lines.extend(self.bundle.splitlines(True))
 
407
        return lines
 
408
 
 
409
    @classmethod
 
410
    def from_objects(klass, repository, revision_id, time, timezone,
 
411
                 target_branch, include_patch=True, include_bundle=True,
 
412
                 local_target_branch=None, public_branch=None, message=None,
 
413
                 base_revision_id=None):
 
414
        """Generate a merge directive from various objects
 
415
 
 
416
        :param repository: The repository containing the revision
 
417
        :param revision_id: The revision to merge
 
418
        :param time: The POSIX timestamp of the date the request was issued.
 
419
        :param timezone: The timezone of the request
 
420
        :param target_branch: The url of the branch to merge into
 
421
        :param include_patch: If true, include a preview patch
 
422
        :param include_bundle: If true, include a bundle
 
423
        :param local_target_branch: a local copy of the target branch
 
424
        :param public_branch: location of a public branch containing the target
 
425
            revision.
 
426
        :param message: Message to use when committing the merge
 
427
        :return: The merge directive
 
428
 
 
429
        The public branch is always used if supplied.  If no bundle is
 
430
        included, the public branch must be supplied, and will be verified.
 
431
 
 
432
        If the message is not supplied, the message from revision_id will be
 
433
        used for the commit.
 
434
        """
 
435
        locked = []
 
436
        try:
 
437
            repository.lock_write()
 
438
            locked.append(repository)
 
439
            t_revision_id = revision_id
 
440
            if revision_id == 'null:':
 
441
                t_revision_id = None
 
442
            t = testament.StrictTestament3.from_revision(repository,
 
443
                t_revision_id)
 
444
            submit_branch = _mod_branch.Branch.open(target_branch)
 
445
            submit_branch.lock_read()
 
446
            locked.append(submit_branch)
 
447
            if submit_branch.get_public_branch() is not None:
 
448
                target_branch = submit_branch.get_public_branch()
 
449
            submit_revision_id = submit_branch.last_revision()
 
450
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
451
            graph = repository.get_graph(submit_branch.repository)
 
452
            ancestor_id = graph.find_unique_lca(revision_id,
 
453
                                                submit_revision_id)
 
454
            if base_revision_id is None:
 
455
                base_revision_id = ancestor_id
 
456
            if (include_patch, include_bundle) != (False, False):
 
457
                repository.fetch(submit_branch.repository, submit_revision_id)
 
458
            if include_patch:
 
459
                patch = klass._generate_diff(repository, revision_id,
 
460
                                             base_revision_id)
 
461
            else:
 
462
                patch = None
 
463
 
 
464
            if include_bundle:
 
465
                bundle = klass._generate_bundle(repository, revision_id,
 
466
                    ancestor_id).encode('base-64')
 
467
            else:
 
468
                bundle = None
 
469
 
 
470
            if public_branch is not None and not include_bundle:
 
471
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
472
                public_branch_obj.lock_read()
 
473
                locked.append(public_branch_obj)
 
474
                if not public_branch_obj.repository.has_revision(
 
475
                    revision_id):
 
476
                    raise errors.PublicBranchOutOfDate(public_branch,
 
477
                                                       revision_id)
 
478
        finally:
 
479
            for entry in reversed(locked):
 
480
                entry.unlock()
 
481
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
 
482
            patch, public_branch, message, bundle, base_revision_id)
 
483
 
 
484
    def _verify_patch(self, repository):
 
485
        calculated_patch = self._generate_diff(repository, self.revision_id,
 
486
                                               self.base_revision_id)
 
487
        # Convert line-endings to UNIX
 
488
        stored_patch = re.sub('\r\n?', '\n', self.patch)
 
489
        # Strip trailing whitespace
 
490
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
 
491
        stored_patch = re.sub(' *\n', '\n', stored_patch)
 
492
        return (calculated_patch == stored_patch)
 
493
 
 
494
    def get_merge_request(self, repository):
 
495
        """Provide data for performing a merge
 
496
 
 
497
        Returns suggested base, suggested target, and patch verification status
 
498
        """
 
499
        verified = self._maybe_verify(repository)
 
500
        return self.base_revision_id, self.revision_id, verified
 
501
 
 
502
    def _maybe_verify(self, repository):
 
503
        if self.patch is not None:
 
504
            if self._verify_patch(repository):
 
505
                return 'verified'
 
506
            else:
 
507
                return 'failed'
 
508
        else:
 
509
            return 'inapplicable'
 
510
 
 
511
 
 
512
class MergeDirectiveFormatRegistry(registry.Registry):
 
513
 
 
514
    def register(self, directive):
 
515
        registry.Registry.register(self, directive._format_string, directive)
 
516
 
 
517
 
 
518
_format_registry = MergeDirectiveFormatRegistry()
 
519
_format_registry.register(MergeDirective)
 
520
_format_registry.register(MergeDirective2)