~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Robert Collins
  • Date: 2005-11-27 22:38:34 UTC
  • mfrom: (1185.33.36 bzr.dev)
  • Revision ID: robertc@robertcollins.net-20051127223834-d00ecca0d0b9384a
Merge from mpool, adjusting check to retain HTTP support.

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 StringIO import StringIO
19
 
import re
20
 
 
21
 
from bzrlib import (
22
 
    branch as _mod_branch,
23
 
    diff,
24
 
    errors,
25
 
    gpg,
26
 
    registry,
27
 
    revision as _mod_revision,
28
 
    rio,
29
 
    testament,
30
 
    timestamp,
31
 
    )
32
 
from bzrlib.bundle import (
33
 
    serializer as bundle_serializer,
34
 
    )
35
 
from bzrlib.email_message import EmailMessage
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
 
        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
 
 
185
 
    def install_revisions(self, target_repo):
186
 
        """Install revisions and return the target revision"""
187
 
        if not target_repo.has_revision(self.revision_id):
188
 
            if self.patch_type == 'bundle':
189
 
                info = bundle_serializer.read_bundle(
190
 
                    StringIO(self.get_raw_bundle()))
191
 
                # We don't use the bundle's target revision, because
192
 
                # MergeDirective.revision_id is authoritative.
193
 
                try:
194
 
                    info.install_revisions(target_repo, stream_input=False)
195
 
                except errors.RevisionNotPresent:
196
 
                    # At least one dependency isn't present.  Try installing
197
 
                    # missing revisions from the submit branch
198
 
                    submit_branch = _mod_branch.Branch.open(self.target_branch)
199
 
                    missing_revisions = []
200
 
                    bundle_revisions = set(r.revision_id for r in
201
 
                                           info.real_revisions)
202
 
                    for revision in info.real_revisions:
203
 
                        for parent_id in revision.parent_ids:
204
 
                            if (parent_id not in bundle_revisions and
205
 
                                not target_repo.has_revision(parent_id)):
206
 
                                missing_revisions.append(parent_id)
207
 
                    # reverse missing revisions to try to get heads first
208
 
                    unique_missing = []
209
 
                    unique_missing_set = set()
210
 
                    for revision in reversed(missing_revisions):
211
 
                        if revision in unique_missing_set:
212
 
                            continue
213
 
                        unique_missing.append(revision)
214
 
                        unique_missing_set.add(revision)
215
 
                    for missing_revision in unique_missing:
216
 
                        target_repo.fetch(submit_branch.repository,
217
 
                                          missing_revision)
218
 
                    info.install_revisions(target_repo, stream_input=False)
219
 
            else:
220
 
                source_branch = _mod_branch.Branch.open(self.source_branch)
221
 
                target_repo.fetch(source_branch.repository, self.revision_id)
222
 
        return self.revision_id
223
 
 
224
 
 
225
 
class MergeDirective(_BaseMergeDirective):
226
 
 
227
 
    """A request to perform a merge into a branch.
228
 
 
229
 
    Designed to be serialized and mailed.  It provides all the information
230
 
    needed to perform a merge automatically, by providing at minimum a revision
231
 
    bundle or the location of a branch.
232
 
 
233
 
    The serialization format is robust against certain common forms of
234
 
    deterioration caused by mailing.
235
 
 
236
 
    The format is also designed to be patch-compatible.  If the directive
237
 
    includes a diff or revision bundle, it should be possible to apply it
238
 
    directly using the standard patch program.
239
 
    """
240
 
 
241
 
    _format_string = 'Bazaar merge directive format 1'
242
 
 
243
 
    def __init__(self, revision_id, testament_sha1, time, timezone,
244
 
                 target_branch, patch=None, patch_type=None,
245
 
                 source_branch=None, message=None, bundle=None):
246
 
        """Constructor.
247
 
 
248
 
        :param revision_id: The revision to merge
249
 
        :param testament_sha1: The sha1 of the testament of the revision to
250
 
            merge.
251
 
        :param time: The current POSIX timestamp time
252
 
        :param timezone: The timezone offset
253
 
        :param target_branch: The branch to apply the merge to
254
 
        :param patch: The text of a diff or bundle
255
 
        :param patch_type: None, "diff" or "bundle", depending on the contents
256
 
            of patch
257
 
        :param source_branch: A public location to merge the revision from
258
 
        :param message: The message to use when committing this merge
259
 
        """
260
 
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
261
 
            timezone, target_branch, patch, source_branch, message)
262
 
        assert patch_type in (None, 'diff', 'bundle'), patch_type
263
 
        if patch_type != 'bundle' and source_branch is None:
264
 
            raise errors.NoMergeSource()
265
 
        if patch_type is not None and patch is None:
266
 
            raise errors.PatchMissing(patch_type)
267
 
        self.patch_type = patch_type
268
 
 
269
 
    def clear_payload(self):
270
 
        self.patch = None
271
 
        self.patch_type = None
272
 
 
273
 
    def get_raw_bundle(self):
274
 
        return self.bundle
275
 
 
276
 
    def _bundle(self):
277
 
        if self.patch_type == 'bundle':
278
 
            return self.patch
279
 
        else:
280
 
            return None
281
 
 
282
 
    bundle = property(_bundle)
283
 
 
284
 
    @classmethod
285
 
    def from_lines(klass, lines):
286
 
        """Deserialize a MergeRequest from an iterable of lines
287
 
 
288
 
        :param lines: An iterable of lines
289
 
        :return: a MergeRequest
290
 
        """
291
 
        line_iter = iter(lines)
292
 
        for line in line_iter:
293
 
            if line.startswith('# Bazaar merge directive format '):
294
 
                break
295
 
        else:
296
 
            if len(lines) > 0:
297
 
                raise errors.NotAMergeDirective(lines[0])
298
 
            else:
299
 
                raise errors.NotAMergeDirective('')
300
 
        return _format_registry.get(line[2:].rstrip())._from_lines(line_iter)
301
 
 
302
 
    @classmethod
303
 
    def _from_lines(klass, line_iter):
304
 
        stanza = rio.read_patch_stanza(line_iter)
305
 
        patch_lines = list(line_iter)
306
 
        if len(patch_lines) == 0:
307
 
            patch = None
308
 
            patch_type = None
309
 
        else:
310
 
            patch = ''.join(patch_lines)
311
 
            try:
312
 
                bundle_serializer.read_bundle(StringIO(patch))
313
 
            except (errors.NotABundle, errors.BundleNotSupported,
314
 
                    errors.BadBundle):
315
 
                patch_type = 'diff'
316
 
            else:
317
 
                patch_type = 'bundle'
318
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
319
 
        kwargs = {}
320
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
321
 
                    'source_branch', 'message'):
322
 
            try:
323
 
                kwargs[key] = stanza.get(key)
324
 
            except KeyError:
325
 
                pass
326
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
327
 
        return MergeDirective(time=time, timezone=timezone,
328
 
                              patch_type=patch_type, patch=patch, **kwargs)
329
 
 
330
 
    def to_lines(self):
331
 
        lines = self._to_lines()
332
 
        if self.patch is not None:
333
 
            lines.extend(self.patch.splitlines(True))
334
 
        return lines
335
 
 
336
 
    @staticmethod
337
 
    def _generate_bundle(repository, revision_id, ancestor_id):
338
 
        s = StringIO()
339
 
        bundle_serializer.write_bundle(repository, revision_id,
340
 
                                       ancestor_id, s, '0.9')
341
 
        return s.getvalue()
342
 
 
343
 
    def get_merge_request(self, repository):
344
 
        """Provide data for performing a merge
345
 
 
346
 
        Returns suggested base, suggested target, and patch verification status
347
 
        """
348
 
        return None, self.revision_id, 'inapplicable'
349
 
 
350
 
 
351
 
class MergeDirective2(_BaseMergeDirective):
352
 
 
353
 
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
354
 
 
355
 
    def __init__(self, revision_id, testament_sha1, time, timezone,
356
 
                 target_branch, patch=None, source_branch=None, message=None,
357
 
                 bundle=None, base_revision_id=None):
358
 
        if source_branch is None and bundle is None:
359
 
            raise errors.NoMergeSource()
360
 
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
361
 
            timezone, target_branch, patch, source_branch, message)
362
 
        self.bundle = bundle
363
 
        self.base_revision_id = base_revision_id
364
 
 
365
 
    def _patch_type(self):
366
 
        if self.bundle is not None:
367
 
            return 'bundle'
368
 
        elif self.patch is not None:
369
 
            return 'diff'
370
 
        else:
371
 
            return None
372
 
 
373
 
    patch_type = property(_patch_type)
374
 
 
375
 
    def clear_payload(self):
376
 
        self.patch = None
377
 
        self.bundle = None
378
 
 
379
 
    def get_raw_bundle(self):
380
 
        if self.bundle is None:
381
 
            return None
382
 
        else:
383
 
            return self.bundle.decode('base-64')
384
 
 
385
 
    @classmethod
386
 
    def _from_lines(klass, line_iter):
387
 
        stanza = rio.read_patch_stanza(line_iter)
388
 
        patch = None
389
 
        bundle = None
390
 
        try:
391
 
            start = line_iter.next()
392
 
        except StopIteration:
393
 
            pass
394
 
        else:
395
 
            if start.startswith('# Begin patch'):
396
 
                patch_lines = []
397
 
                for line in line_iter:
398
 
                    if line.startswith('# Begin bundle'):
399
 
                        start = line
400
 
                        break
401
 
                    patch_lines.append(line)
402
 
                else:
403
 
                    start = None
404
 
                patch = ''.join(patch_lines)
405
 
            if start is not None:
406
 
                if start.startswith('# Begin bundle'):
407
 
                    bundle = ''.join(line_iter)
408
 
                else:
409
 
                    raise errors.IllegalMergeDirectivePayload(start)
410
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
411
 
        kwargs = {}
412
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
413
 
                    'source_branch', 'message', 'base_revision_id'):
414
 
            try:
415
 
                kwargs[key] = stanza.get(key)
416
 
            except KeyError:
417
 
                pass
418
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
419
 
        kwargs['base_revision_id'] =\
420
 
            kwargs['base_revision_id'].encode('utf-8')
421
 
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
422
 
                     **kwargs)
423
 
 
424
 
    def to_lines(self):
425
 
        lines = self._to_lines(base_revision=True)
426
 
        if self.patch is not None:
427
 
            lines.append('# Begin patch\n')
428
 
            lines.extend(self.patch.splitlines(True))
429
 
        if self.bundle is not None:
430
 
            lines.append('# Begin bundle\n')
431
 
            lines.extend(self.bundle.splitlines(True))
432
 
        return lines
433
 
 
434
 
    @classmethod
435
 
    def from_objects(klass, repository, revision_id, time, timezone,
436
 
                 target_branch, include_patch=True, include_bundle=True,
437
 
                 local_target_branch=None, public_branch=None, message=None,
438
 
                 base_revision_id=None):
439
 
        """Generate a merge directive from various objects
440
 
 
441
 
        :param repository: The repository containing the revision
442
 
        :param revision_id: The revision to merge
443
 
        :param time: The POSIX timestamp of the date the request was issued.
444
 
        :param timezone: The timezone of the request
445
 
        :param target_branch: The url of the branch to merge into
446
 
        :param include_patch: If true, include a preview patch
447
 
        :param include_bundle: If true, include a bundle
448
 
        :param local_target_branch: a local copy of the target branch
449
 
        :param public_branch: location of a public branch containing the target
450
 
            revision.
451
 
        :param message: Message to use when committing the merge
452
 
        :return: The merge directive
453
 
 
454
 
        The public branch is always used if supplied.  If no bundle is
455
 
        included, the public branch must be supplied, and will be verified.
456
 
 
457
 
        If the message is not supplied, the message from revision_id will be
458
 
        used for the commit.
459
 
        """
460
 
        locked = []
461
 
        try:
462
 
            repository.lock_write()
463
 
            locked.append(repository)
464
 
            t_revision_id = revision_id
465
 
            if revision_id == 'null:':
466
 
                t_revision_id = None
467
 
            t = testament.StrictTestament3.from_revision(repository,
468
 
                t_revision_id)
469
 
            submit_branch = _mod_branch.Branch.open(target_branch)
470
 
            submit_branch.lock_read()
471
 
            locked.append(submit_branch)
472
 
            if submit_branch.get_public_branch() is not None:
473
 
                target_branch = submit_branch.get_public_branch()
474
 
            submit_revision_id = submit_branch.last_revision()
475
 
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
476
 
            graph = repository.get_graph(submit_branch.repository)
477
 
            ancestor_id = graph.find_unique_lca(revision_id,
478
 
                                                submit_revision_id)
479
 
            if base_revision_id is None:
480
 
                base_revision_id = ancestor_id
481
 
            if (include_patch, include_bundle) != (False, False):
482
 
                repository.fetch(submit_branch.repository, submit_revision_id)
483
 
            if include_patch:
484
 
                patch = klass._generate_diff(repository, revision_id,
485
 
                                             base_revision_id)
486
 
            else:
487
 
                patch = None
488
 
 
489
 
            if include_bundle:
490
 
                bundle = klass._generate_bundle(repository, revision_id,
491
 
                    ancestor_id).encode('base-64')
492
 
            else:
493
 
                bundle = None
494
 
 
495
 
            if public_branch is not None and not include_bundle:
496
 
                public_branch_obj = _mod_branch.Branch.open(public_branch)
497
 
                public_branch_obj.lock_read()
498
 
                locked.append(public_branch_obj)
499
 
                if not public_branch_obj.repository.has_revision(
500
 
                    revision_id):
501
 
                    raise errors.PublicBranchOutOfDate(public_branch,
502
 
                                                       revision_id)
503
 
        finally:
504
 
            for entry in reversed(locked):
505
 
                entry.unlock()
506
 
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
507
 
            patch, public_branch, message, bundle, base_revision_id)
508
 
 
509
 
    def _verify_patch(self, repository):
510
 
        calculated_patch = self._generate_diff(repository, self.revision_id,
511
 
                                               self.base_revision_id)
512
 
        # Convert line-endings to UNIX
513
 
        stored_patch = re.sub('\r\n?', '\n', self.patch)
514
 
        calculated_patch = re.sub('\r\n?', '\n', calculated_patch)
515
 
        # Strip trailing whitespace
516
 
        calculated_patch = re.sub(' *\n', '\n', calculated_patch)
517
 
        stored_patch = re.sub(' *\n', '\n', stored_patch)
518
 
        return (calculated_patch == stored_patch)
519
 
 
520
 
    def get_merge_request(self, repository):
521
 
        """Provide data for performing a merge
522
 
 
523
 
        Returns suggested base, suggested target, and patch verification status
524
 
        """
525
 
        verified = self._maybe_verify(repository)
526
 
        return self.base_revision_id, self.revision_id, verified
527
 
 
528
 
    def _maybe_verify(self, repository):
529
 
        if self.patch is not None:
530
 
            if self._verify_patch(repository):
531
 
                return 'verified'
532
 
            else:
533
 
                return 'failed'
534
 
        else:
535
 
            return 'inapplicable'
536
 
 
537
 
 
538
 
class MergeDirectiveFormatRegistry(registry.Registry):
539
 
 
540
 
    def register(self, directive, format_string=None):
541
 
        if format_string is None:
542
 
            format_string = directive._format_string
543
 
        registry.Registry.register(self, format_string, directive)
544
 
 
545
 
 
546
 
_format_registry = MergeDirectiveFormatRegistry()
547
 
_format_registry.register(MergeDirective)
548
 
_format_registry.register(MergeDirective2)
549
 
_format_registry.register(MergeDirective2,
550
 
                          'Bazaar merge directive format 2 (Bazaar 0.19)')