~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-03-15 22:34:18 UTC
  • mfrom: (3280.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080315223418-lzsk2wwoz9f56awd
(Martin Albisetti) Change backup dir from .bzr.backup to backup.bzr

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)')