~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Pool
  • Date: 2005-07-21 21:32:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050721213213-c6ac0e8b06eaad0f
- bzr update-hashes shows some stats on what it did

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