~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-24 00:44:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050324004418-b4a050f656c07f5f
show space usage for various stores in the info command

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