~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Pool
  • Date: 2005-11-04 01:46:31 UTC
  • mto: (1185.33.49 bzr.dev)
  • mto: This revision was merged to the branch mainline in revision 1512.
  • Revision ID: mbp@sourcefrog.net-20051104014631-750e0ad4172c952c
Make biobench directly executable

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