~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Pool
  • Date: 2008-07-03 10:44:34 UTC
  • mfrom: (3512.3.2 setlocale.mini)
  • mto: This revision was merged to the branch mainline in revision 3518.
  • Revision ID: mbp@sourcefrog.net-20080703104434-v4qgzvxd2wxg8etl
Set locale from environment for third party libs and day of week.

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