/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Robert Collins
  • Date: 2007-07-20 03:20:20 UTC
  • mfrom: (2592 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2635.
  • Revision ID: robertc@robertcollins.net-20070720032020-xiftpb5gqeebo861
(robertc) Reinstate the accidentally backed out external_url patch.

Show diffs side-by-side

added added

removed removed

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