/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

Merge from bzr.dev, resolving conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2011 Canonical Ltd
 
1
# Copyright (C) 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
from __future__ import absolute_import
18
 
 
19
 
import base64
20
 
import re
21
 
 
22
 
from . import lazy_import
23
 
lazy_import.lazy_import(globals(), """
24
 
from breezy import (
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
from email import Message
 
19
from StringIO import StringIO
 
20
 
 
21
from bzrlib import (
25
22
    branch as _mod_branch,
26
23
    diff,
27
 
    email_message,
28
24
    errors,
29
25
    gpg,
30
 
    hooks,
31
 
    registry,
32
26
    revision as _mod_revision,
33
27
    rio,
34
28
    testament,
35
29
    timestamp,
36
 
    trace,
37
30
    )
38
 
from breezy.bundle import (
 
31
from bzrlib.bundle import (
39
32
    serializer as bundle_serializer,
40
33
    )
41
 
""")
42
 
from .sixish import (
43
 
    BytesIO,
44
 
    )
45
 
 
46
 
 
47
 
class MergeRequestBodyParams(object):
48
 
    """Parameter object for the merge_request_body hook."""
49
 
 
50
 
    def __init__(self, body, orig_body, directive, to, basename, subject,
51
 
                 branch, tree=None):
52
 
        self.body = body
53
 
        self.orig_body = orig_body
54
 
        self.directive = directive
55
 
        self.branch = branch
56
 
        self.tree = tree
57
 
        self.to = to
58
 
        self.basename = basename
59
 
        self.subject = subject
60
 
 
61
 
 
62
 
class MergeDirectiveHooks(hooks.Hooks):
63
 
    """Hooks for MergeDirective classes."""
64
 
 
65
 
    def __init__(self):
66
 
        hooks.Hooks.__init__(self, "breezy.merge_directive", "BaseMergeDirective.hooks")
67
 
        self.add_hook('merge_request_body',
68
 
            "Called with a MergeRequestBodyParams when a body is needed for"
69
 
            " a merge request.  Callbacks must return a body.  If more"
70
 
            " than one callback is registered, the output of one callback is"
71
 
            " provided to the next.", (1, 15, 0))
72
 
 
73
 
 
74
 
class BaseMergeDirective(object):
 
34
 
 
35
 
 
36
class MergeDirective(object):
 
37
 
75
38
    """A request to perform a merge into a branch.
76
39
 
77
 
    This is the base class that all merge directive implementations 
78
 
    should derive from.
79
 
 
80
 
    :cvar multiple_output_files: Whether or not this merge directive 
81
 
        stores a set of revisions in more than one file
 
40
    Designed to be serialized and mailed.  It provides all the information
 
41
    needed to perform a merge automatically, by providing at minimum a revision
 
42
    bundle or the location of a branch.
 
43
 
 
44
    The serialization format is robust against certain common forms of
 
45
    deterioration caused by mailing.
 
46
 
 
47
    The format is also designed to be patch-compatible.  If the directive
 
48
    includes a diff or revision bundle, it should be possible to apply it
 
49
    directly using the standard patch program.
82
50
    """
83
51
 
84
 
    hooks = MergeDirectiveHooks()
85
 
 
86
 
    multiple_output_files = False
 
52
    _format_string = 'Bazaar merge directive format 1'
87
53
 
88
54
    def __init__(self, revision_id, testament_sha1, time, timezone,
89
 
                 target_branch, patch=None, source_branch=None,
90
 
                 message=None, bundle=None):
 
55
                 target_branch, patch=None, patch_type=None,
 
56
                 source_branch=None, message=None):
91
57
        """Constructor.
92
58
 
93
59
        :param revision_id: The revision to merge
95
61
            merge.
96
62
        :param time: The current POSIX timestamp time
97
63
        :param timezone: The timezone offset
98
 
        :param target_branch: Location of branch to apply the merge to
 
64
        :param target_branch: The branch to apply the merge to
99
65
        :param patch: The text of a diff or bundle
 
66
        :param patch_type: None, "diff" or "bundle", depending on the contents
 
67
            of patch
100
68
        :param source_branch: A public location to merge the revision from
101
69
        :param message: The message to use when committing this merge
102
70
        """
 
71
        assert patch_type in (None, 'diff', 'bundle')
 
72
        if patch_type != 'bundle' and source_branch is None:
 
73
            raise errors.NoMergeSource()
 
74
        if patch_type is not None and patch is None:
 
75
            raise errors.PatchMissing(patch_type)
103
76
        self.revision_id = revision_id
104
77
        self.testament_sha1 = testament_sha1
105
78
        self.time = time
106
79
        self.timezone = timezone
107
80
        self.target_branch = target_branch
108
81
        self.patch = patch
 
82
        self.patch_type = patch_type
109
83
        self.source_branch = source_branch
110
84
        self.message = message
111
85
 
 
86
    @classmethod
 
87
    def from_lines(klass, lines):
 
88
        """Deserialize a MergeRequest from an iterable of lines
 
89
 
 
90
        :param lines: An iterable of lines
 
91
        :return: a MergeRequest
 
92
        """
 
93
        line_iter = iter(lines)
 
94
        for line in line_iter:
 
95
            if line.startswith('# ' + klass._format_string):
 
96
                break
 
97
        else:
 
98
            if len(lines) > 0:
 
99
                raise errors.NotAMergeDirective(lines[0])
 
100
            else:
 
101
                raise errors.NotAMergeDirective('')
 
102
        stanza = rio.read_patch_stanza(line_iter)
 
103
        patch_lines = list(line_iter)
 
104
        if len(patch_lines) == 0:
 
105
            patch = None
 
106
            patch_type = None
 
107
        else:
 
108
            patch = ''.join(patch_lines)
 
109
            try:
 
110
                bundle_serializer.read_bundle(StringIO(patch))
 
111
            except errors.NotABundle:
 
112
                patch_type = 'diff'
 
113
            else:
 
114
                patch_type = 'bundle'
 
115
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
116
        kwargs = {}
 
117
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
118
                    'source_branch', 'message'):
 
119
            try:
 
120
                kwargs[key] = stanza.get(key)
 
121
            except KeyError:
 
122
                pass
 
123
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
124
        return MergeDirective(time=time, timezone=timezone,
 
125
                              patch_type=patch_type, patch=patch, **kwargs)
 
126
 
112
127
    def to_lines(self):
113
128
        """Serialize as a list of lines
114
129
 
115
130
        :return: a list of lines
116
131
        """
117
 
        raise NotImplementedError(self.to_lines)
118
 
 
119
 
    def to_files(self):
120
 
        """Serialize as a set of files.
121
 
 
122
 
        :return: List of tuples with filename and contents as lines
123
 
        """
124
 
        raise NotImplementedError(self.to_files)
125
 
 
126
 
    def get_raw_bundle(self):
127
 
        """Return the bundle for this merge directive.
128
 
 
129
 
        :return: bundle text or None if there is no bundle
130
 
        """
131
 
        return None
132
 
 
133
 
    def _to_lines(self, base_revision=False):
134
 
        """Serialize as a list of lines
135
 
 
136
 
        :return: a list of lines
137
 
        """
138
132
        time_str = timestamp.format_patch_date(self.time, self.timezone)
139
133
        stanza = rio.Stanza(revision_id=self.revision_id, timestamp=time_str,
140
134
                            target_branch=self.target_branch,
142
136
        for key in ('source_branch', 'message'):
143
137
            if self.__dict__[key] is not None:
144
138
                stanza.add(key, self.__dict__[key])
145
 
        if base_revision:
146
 
            stanza.add('base_revision_id', self.base_revision_id)
147
 
        lines = [b'# ' + self._format_string + b'\n']
 
139
        lines = ['# ' + self._format_string + '\n']
148
140
        lines.extend(rio.to_patch_lines(stanza))
149
 
        lines.append(b'# \n')
 
141
        lines.append('# \n')
 
142
        if self.patch is not None:
 
143
            lines.extend(self.patch.splitlines(True))
150
144
        return lines
151
145
 
152
 
    def write_to_directory(self, path):
153
 
        """Write this merge directive to a series of files in a directory.
154
 
 
155
 
        :param path: Filesystem path to write to
156
 
        """
157
 
        raise NotImplementedError(self.write_to_directory)
 
146
    def to_signed(self, branch):
 
147
        """Serialize as a signed string.
 
148
 
 
149
        :param branch: The source branch, to get the signing strategy
 
150
        :return: a string
 
151
        """
 
152
        my_gpg = gpg.GPGStrategy(branch.get_config())
 
153
        return my_gpg.sign(''.join(self.to_lines()))
 
154
 
 
155
    def to_email(self, mail_to, branch, sign=False):
 
156
        """Serialize as an email message.
 
157
 
 
158
        :param mail_to: The address to mail the message to
 
159
        :param branch: The source branch, to get the signing strategy and
 
160
            source email address
 
161
        :param sign: If True, gpg-sign the email
 
162
        :return: an email message
 
163
        """
 
164
        mail_from = branch.get_config().username()
 
165
        message = Message.Message()
 
166
        message['To'] = mail_to
 
167
        message['From'] = mail_from
 
168
        if self.message is not None:
 
169
            message['Subject'] = self.message
 
170
        else:
 
171
            revision = branch.repository.get_revision(self.revision_id)
 
172
            message['Subject'] = revision.message
 
173
        if sign:
 
174
            body = self.to_signed(branch)
 
175
        else:
 
176
            body = ''.join(self.to_lines())
 
177
        message.set_payload(body)
 
178
        return message
158
179
 
159
180
    @classmethod
160
181
    def from_objects(klass, repository, revision_id, time, timezone,
169
190
        :param target_branch: The url of the branch to merge into
170
191
        :param patch_type: 'bundle', 'diff' or None, depending on the type of
171
192
            patch desired.
172
 
        :param local_target_branch: the submit branch, either itself or a local copy
173
 
        :param public_branch: location of a public branch containing
174
 
            the target revision.
 
193
        :param local_target_branch: a local copy of the target branch
 
194
        :param public_branch: location of a public branch containing the target
 
195
            revision.
175
196
        :param message: Message to use when committing the merge
176
197
        :return: The merge directive
177
198
 
181
202
        If the message is not supplied, the message from revision_id will be
182
203
        used for the commit.
183
204
        """
184
 
        t_revision_id = revision_id
185
 
        if revision_id == _mod_revision.NULL_REVISION:
186
 
            t_revision_id = None
187
 
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
188
 
        if local_target_branch is None:
189
 
            submit_branch = _mod_branch.Branch.open(target_branch)
190
 
        else:
191
 
            submit_branch = local_target_branch
 
205
        t = testament.StrictTestament3.from_revision(repository, revision_id)
 
206
        submit_branch = _mod_branch.Branch.open(target_branch)
192
207
        if submit_branch.get_public_branch() is not None:
193
208
            target_branch = submit_branch.get_public_branch()
194
209
        if patch_type is None:
195
210
            patch = None
196
211
        else:
197
212
            submit_revision_id = submit_branch.last_revision()
198
 
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
199
213
            repository.fetch(submit_branch.repository, submit_revision_id)
200
 
            graph = repository.get_graph()
201
 
            ancestor_id = graph.find_unique_lca(revision_id,
202
 
                                                submit_revision_id)
 
214
            ancestor_id = _mod_revision.common_ancestor(revision_id,
 
215
                                                        submit_revision_id,
 
216
                                                        repository)
203
217
            type_handler = {'bundle': klass._generate_bundle,
204
218
                            'diff': klass._generate_diff,
205
219
                            None: lambda x, y, z: None }
206
220
            patch = type_handler[patch_type](repository, revision_id,
207
221
                                             ancestor_id)
208
 
 
209
 
        if public_branch is not None and patch_type != 'bundle':
210
 
            public_branch_obj = _mod_branch.Branch.open(public_branch)
211
 
            if not public_branch_obj.repository.has_revision(revision_id):
212
 
                raise errors.PublicBranchOutOfDate(public_branch,
213
 
                                                   revision_id)
214
 
 
215
 
        return klass(revision_id, t.as_sha1(), time, timezone, target_branch,
216
 
            patch, patch_type, public_branch, message)
217
 
 
218
 
    def get_disk_name(self, branch):
219
 
        """Generate a suitable basename for storing this directive on disk
220
 
 
221
 
        :param branch: The Branch this merge directive was generated fro
222
 
        :return: A string
223
 
        """
224
 
        revno, revision_id = branch.last_revision_info()
225
 
        if self.revision_id == revision_id:
226
 
            revno = [revno]
227
 
        else:
228
 
            try:
229
 
                revno = branch.revision_id_to_dotted_revno(self.revision_id)
230
 
            except errors.NoSuchRevision:
231
 
                revno = ['merge']
232
 
        nick = re.sub('(\\W+)', '-', branch.nick).strip('-')
233
 
        return '%s-%s' % (nick, '.'.join(str(n) for n in revno))
 
222
            if patch_type == 'bundle':
 
223
                s = StringIO()
 
224
                bundle_serializer.write_bundle(repository, revision_id,
 
225
                                               ancestor_id, s)
 
226
                patch = s.getvalue()
 
227
            elif patch_type == 'diff':
 
228
                patch = klass._generate_diff(repository, revision_id,
 
229
                                             ancestor_id)
 
230
 
 
231
            if public_branch is not None and patch_type != 'bundle':
 
232
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
233
                if not public_branch_obj.repository.has_revision(revision_id):
 
234
                    raise errors.PublicBranchOutOfDate(public_branch,
 
235
                                                       revision_id)
 
236
 
 
237
        return MergeDirective(revision_id, t.as_sha1(), time, timezone,
 
238
                              target_branch, patch, patch_type, public_branch,
 
239
                              message)
234
240
 
235
241
    @staticmethod
236
242
    def _generate_diff(repository, revision_id, ancestor_id):
237
243
        tree_1 = repository.revision_tree(ancestor_id)
238
244
        tree_2 = repository.revision_tree(revision_id)
239
 
        s = BytesIO()
 
245
        s = StringIO()
240
246
        diff.show_diff_trees(tree_1, tree_2, s, old_label='', new_label='')
241
247
        return s.getvalue()
242
248
 
243
249
    @staticmethod
244
250
    def _generate_bundle(repository, revision_id, ancestor_id):
245
 
        s = BytesIO()
 
251
        s = StringIO()
246
252
        bundle_serializer.write_bundle(repository, revision_id,
247
253
                                       ancestor_id, s)
248
254
        return s.getvalue()
249
255
 
250
 
    def to_signed(self, branch):
251
 
        """Serialize as a signed string.
252
 
 
253
 
        :param branch: The source branch, to get the signing strategy
254
 
        :return: a string
255
 
        """
256
 
        my_gpg = gpg.GPGStrategy(branch.get_config_stack())
257
 
        return my_gpg.sign(b''.join(self.to_lines()), gpg.MODE_CLEAR)
258
 
 
259
 
    def to_email(self, mail_to, branch, sign=False):
260
 
        """Serialize as an email message.
261
 
 
262
 
        :param mail_to: The address to mail the message to
263
 
        :param branch: The source branch, to get the signing strategy and
264
 
            source email address
265
 
        :param sign: If True, gpg-sign the email
266
 
        :return: an email message
267
 
        """
268
 
        mail_from = branch.get_config_stack().get('email')
269
 
        if self.message is not None:
270
 
            subject = self.message
271
 
        else:
272
 
            revision = branch.repository.get_revision(self.revision_id)
273
 
            subject = revision.message
274
 
        if sign:
275
 
            body = self.to_signed(branch)
276
 
        else:
277
 
            body = b''.join(self.to_lines())
278
 
        message = email_message.EmailMessage(mail_from, mail_to, subject,
279
 
            body)
280
 
        return message
281
 
 
282
256
    def install_revisions(self, target_repo):
283
257
        """Install revisions and return the target revision"""
284
258
        if not target_repo.has_revision(self.revision_id):
285
259
            if self.patch_type == 'bundle':
286
 
                info = bundle_serializer.read_bundle(
287
 
                    BytesIO(self.get_raw_bundle()))
 
260
                info = bundle_serializer.read_bundle(StringIO(self.patch))
288
261
                # We don't use the bundle's target revision, because
289
262
                # MergeDirective.revision_id is authoritative.
290
 
                try:
291
 
                    info.install_revisions(target_repo, stream_input=False)
292
 
                except errors.RevisionNotPresent:
293
 
                    # At least one dependency isn't present.  Try installing
294
 
                    # missing revisions from the submit branch
295
 
                    try:
296
 
                        submit_branch = \
297
 
                            _mod_branch.Branch.open(self.target_branch)
298
 
                    except errors.NotBranchError:
299
 
                        raise errors.TargetNotBranch(self.target_branch)
300
 
                    missing_revisions = []
301
 
                    bundle_revisions = set(r.revision_id for r in
302
 
                                           info.real_revisions)
303
 
                    for revision in info.real_revisions:
304
 
                        for parent_id in revision.parent_ids:
305
 
                            if (parent_id not in bundle_revisions and
306
 
                                not target_repo.has_revision(parent_id)):
307
 
                                missing_revisions.append(parent_id)
308
 
                    # reverse missing revisions to try to get heads first
309
 
                    unique_missing = []
310
 
                    unique_missing_set = set()
311
 
                    for revision in reversed(missing_revisions):
312
 
                        if revision in unique_missing_set:
313
 
                            continue
314
 
                        unique_missing.append(revision)
315
 
                        unique_missing_set.add(revision)
316
 
                    for missing_revision in unique_missing:
317
 
                        target_repo.fetch(submit_branch.repository,
318
 
                                          missing_revision)
319
 
                    info.install_revisions(target_repo, stream_input=False)
 
263
                info.install_revisions(target_repo)
320
264
            else:
321
265
                source_branch = _mod_branch.Branch.open(self.source_branch)
322
266
                target_repo.fetch(source_branch.repository, self.revision_id)
323
267
        return self.revision_id
324
 
 
325
 
    def compose_merge_request(self, mail_client, to, body, branch, tree=None):
326
 
        """Compose a request to merge this directive.
327
 
 
328
 
        :param mail_client: The mail client to use for composing this request.
329
 
        :param to: The address to compose the request to.
330
 
        :param branch: The Branch that was used to produce this directive.
331
 
        :param tree: The Tree (if any) for the Branch used to produce this
332
 
            directive.
333
 
        """
334
 
        basename = self.get_disk_name(branch)
335
 
        subject = '[MERGE] '
336
 
        if self.message is not None:
337
 
            subject += self.message
338
 
        else:
339
 
            revision = branch.repository.get_revision(self.revision_id)
340
 
            subject += revision.get_summary()
341
 
        if getattr(mail_client, 'supports_body', False):
342
 
            orig_body = body
343
 
            for hook in self.hooks['merge_request_body']:
344
 
                params = MergeRequestBodyParams(body, orig_body, self,
345
 
                                                to, basename, subject, branch,
346
 
                                                tree)
347
 
                body = hook(params)
348
 
        elif len(self.hooks['merge_request_body']) > 0:
349
 
            trace.warning('Cannot run merge_request_body hooks because mail'
350
 
                          ' client %s does not support message bodies.',
351
 
                        mail_client.__class__.__name__)
352
 
        mail_client.compose_merge_request(to, subject,
353
 
                                          b''.join(self.to_lines()),
354
 
                                          basename, body)
355
 
 
356
 
 
357
 
class MergeDirective(BaseMergeDirective):
358
 
 
359
 
    """A request to perform a merge into a branch.
360
 
 
361
 
    Designed to be serialized and mailed.  It provides all the information
362
 
    needed to perform a merge automatically, by providing at minimum a revision
363
 
    bundle or the location of a branch.
364
 
 
365
 
    The serialization format is robust against certain common forms of
366
 
    deterioration caused by mailing.
367
 
 
368
 
    The format is also designed to be patch-compatible.  If the directive
369
 
    includes a diff or revision bundle, it should be possible to apply it
370
 
    directly using the standard patch program.
371
 
    """
372
 
 
373
 
    _format_string = b'Bazaar merge directive format 1'
374
 
 
375
 
    def __init__(self, revision_id, testament_sha1, time, timezone,
376
 
                 target_branch, patch=None, patch_type=None,
377
 
                 source_branch=None, message=None, bundle=None):
378
 
        """Constructor.
379
 
 
380
 
        :param revision_id: The revision to merge
381
 
        :param testament_sha1: The sha1 of the testament of the revision to
382
 
            merge.
383
 
        :param time: The current POSIX timestamp time
384
 
        :param timezone: The timezone offset
385
 
        :param target_branch: Location of the branch to apply the merge to
386
 
        :param patch: The text of a diff or bundle
387
 
        :param patch_type: None, "diff" or "bundle", depending on the contents
388
 
            of patch
389
 
        :param source_branch: A public location to merge the revision from
390
 
        :param message: The message to use when committing this merge
391
 
        """
392
 
        BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
393
 
            timezone, target_branch, patch, source_branch, message)
394
 
        if patch_type not in (None, 'diff', 'bundle'):
395
 
            raise ValueError(patch_type)
396
 
        if patch_type != 'bundle' and source_branch is None:
397
 
            raise errors.NoMergeSource()
398
 
        if patch_type is not None and patch is None:
399
 
            raise errors.PatchMissing(patch_type)
400
 
        self.patch_type = patch_type
401
 
 
402
 
    def clear_payload(self):
403
 
        self.patch = None
404
 
        self.patch_type = None
405
 
 
406
 
    def get_raw_bundle(self):
407
 
        return self.bundle
408
 
 
409
 
    def _bundle(self):
410
 
        if self.patch_type == 'bundle':
411
 
            return self.patch
412
 
        else:
413
 
            return None
414
 
 
415
 
    bundle = property(_bundle)
416
 
 
417
 
    @classmethod
418
 
    def from_lines(klass, lines):
419
 
        """Deserialize a MergeRequest from an iterable of lines
420
 
 
421
 
        :param lines: An iterable of lines
422
 
        :return: a MergeRequest
423
 
        """
424
 
        line_iter = iter(lines)
425
 
        firstline = b""
426
 
        for line in line_iter:
427
 
            if line.startswith(b'# Bazaar merge directive format '):
428
 
                return _format_registry.get(line[2:].rstrip())._from_lines(
429
 
                    line_iter)
430
 
            firstline = firstline or line.strip()
431
 
        raise errors.NotAMergeDirective(firstline)
432
 
 
433
 
    @classmethod
434
 
    def _from_lines(klass, line_iter):
435
 
        stanza = rio.read_patch_stanza(line_iter)
436
 
        patch_lines = list(line_iter)
437
 
        if len(patch_lines) == 0:
438
 
            patch = None
439
 
            patch_type = None
440
 
        else:
441
 
            patch = b''.join(patch_lines)
442
 
            try:
443
 
                bundle_serializer.read_bundle(BytesIO(patch))
444
 
            except (errors.NotABundle, errors.BundleNotSupported,
445
 
                    errors.BadBundle):
446
 
                patch_type = 'diff'
447
 
            else:
448
 
                patch_type = 'bundle'
449
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
450
 
        kwargs = {}
451
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
452
 
                    'source_branch', 'message'):
453
 
            try:
454
 
                kwargs[key] = stanza.get(key)
455
 
            except KeyError:
456
 
                pass
457
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
458
 
        return MergeDirective(time=time, timezone=timezone,
459
 
                              patch_type=patch_type, patch=patch, **kwargs)
460
 
 
461
 
    def to_lines(self):
462
 
        lines = self._to_lines()
463
 
        if self.patch is not None:
464
 
            lines.extend(self.patch.splitlines(True))
465
 
        return lines
466
 
 
467
 
    @staticmethod
468
 
    def _generate_bundle(repository, revision_id, ancestor_id):
469
 
        s = BytesIO()
470
 
        bundle_serializer.write_bundle(repository, revision_id,
471
 
                                       ancestor_id, s, '0.9')
472
 
        return s.getvalue()
473
 
 
474
 
    def get_merge_request(self, repository):
475
 
        """Provide data for performing a merge
476
 
 
477
 
        Returns suggested base, suggested target, and patch verification status
478
 
        """
479
 
        return None, self.revision_id, 'inapplicable'
480
 
 
481
 
 
482
 
class MergeDirective2(BaseMergeDirective):
483
 
 
484
 
    _format_string = b'Bazaar merge directive format 2 (Bazaar 0.90)'
485
 
 
486
 
    def __init__(self, revision_id, testament_sha1, time, timezone,
487
 
                 target_branch, patch=None, source_branch=None, message=None,
488
 
                 bundle=None, base_revision_id=None):
489
 
        if source_branch is None and bundle is None:
490
 
            raise errors.NoMergeSource()
491
 
        BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
492
 
            timezone, target_branch, patch, source_branch, message)
493
 
        self.bundle = bundle
494
 
        self.base_revision_id = base_revision_id
495
 
 
496
 
    def _patch_type(self):
497
 
        if self.bundle is not None:
498
 
            return 'bundle'
499
 
        elif self.patch is not None:
500
 
            return 'diff'
501
 
        else:
502
 
            return None
503
 
 
504
 
    patch_type = property(_patch_type)
505
 
 
506
 
    def clear_payload(self):
507
 
        self.patch = None
508
 
        self.bundle = None
509
 
 
510
 
    def get_raw_bundle(self):
511
 
        if self.bundle is None:
512
 
            return None
513
 
        else:
514
 
            return base64.b64decode(self.bundle)
515
 
 
516
 
    @classmethod
517
 
    def _from_lines(klass, line_iter):
518
 
        stanza = rio.read_patch_stanza(line_iter)
519
 
        patch = None
520
 
        bundle = None
521
 
        try:
522
 
            start = next(line_iter)
523
 
        except StopIteration:
524
 
            pass
525
 
        else:
526
 
            if start.startswith(b'# Begin patch'):
527
 
                patch_lines = []
528
 
                for line in line_iter:
529
 
                    if line.startswith(b'# Begin bundle'):
530
 
                        start = line
531
 
                        break
532
 
                    patch_lines.append(line)
533
 
                else:
534
 
                    start = None
535
 
                patch = b''.join(patch_lines)
536
 
            if start is not None:
537
 
                if start.startswith(b'# Begin bundle'):
538
 
                    bundle = b''.join(line_iter)
539
 
                else:
540
 
                    raise errors.IllegalMergeDirectivePayload(start)
541
 
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
542
 
        kwargs = {}
543
 
        for key in ('revision_id', 'testament_sha1', 'target_branch',
544
 
                    'source_branch', 'message', 'base_revision_id'):
545
 
            try:
546
 
                kwargs[key] = stanza.get(key)
547
 
            except KeyError:
548
 
                pass
549
 
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
550
 
        kwargs['base_revision_id'] =\
551
 
            kwargs['base_revision_id'].encode('utf-8')
552
 
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
553
 
                     **kwargs)
554
 
 
555
 
    def to_lines(self):
556
 
        lines = self._to_lines(base_revision=True)
557
 
        if self.patch is not None:
558
 
            lines.append(b'# Begin patch\n')
559
 
            lines.extend(self.patch.splitlines(True))
560
 
        if self.bundle is not None:
561
 
            lines.append(b'# Begin bundle\n')
562
 
            lines.extend(self.bundle.splitlines(True))
563
 
        return lines
564
 
 
565
 
    @classmethod
566
 
    def from_objects(klass, repository, revision_id, time, timezone,
567
 
                 target_branch, include_patch=True, include_bundle=True,
568
 
                 local_target_branch=None, public_branch=None, message=None,
569
 
                 base_revision_id=None):
570
 
        """Generate a merge directive from various objects
571
 
 
572
 
        :param repository: The repository containing the revision
573
 
        :param revision_id: The revision to merge
574
 
        :param time: The POSIX timestamp of the date the request was issued.
575
 
        :param timezone: The timezone of the request
576
 
        :param target_branch: The url of the branch to merge into
577
 
        :param include_patch: If true, include a preview patch
578
 
        :param include_bundle: If true, include a bundle
579
 
        :param local_target_branch: the target branch, either itself or a local copy
580
 
        :param public_branch: location of a public branch containing
581
 
            the target revision.
582
 
        :param message: Message to use when committing the merge
583
 
        :return: The merge directive
584
 
 
585
 
        The public branch is always used if supplied.  If no bundle is
586
 
        included, the public branch must be supplied, and will be verified.
587
 
 
588
 
        If the message is not supplied, the message from revision_id will be
589
 
        used for the commit.
590
 
        """
591
 
        locked = []
592
 
        try:
593
 
            repository.lock_write()
594
 
            locked.append(repository)
595
 
            t_revision_id = revision_id
596
 
            if revision_id == b'null:':
597
 
                t_revision_id = None
598
 
            t = testament.StrictTestament3.from_revision(repository,
599
 
                t_revision_id)
600
 
            if local_target_branch is None:
601
 
                submit_branch = _mod_branch.Branch.open(target_branch)
602
 
            else:
603
 
                submit_branch = local_target_branch
604
 
            submit_branch.lock_read()
605
 
            locked.append(submit_branch)
606
 
            if submit_branch.get_public_branch() is not None:
607
 
                target_branch = submit_branch.get_public_branch()
608
 
            submit_revision_id = submit_branch.last_revision()
609
 
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
610
 
            graph = repository.get_graph(submit_branch.repository)
611
 
            ancestor_id = graph.find_unique_lca(revision_id,
612
 
                                                submit_revision_id)
613
 
            if base_revision_id is None:
614
 
                base_revision_id = ancestor_id
615
 
            if (include_patch, include_bundle) != (False, False):
616
 
                repository.fetch(submit_branch.repository, submit_revision_id)
617
 
            if include_patch:
618
 
                patch = klass._generate_diff(repository, revision_id,
619
 
                                             base_revision_id)
620
 
            else:
621
 
                patch = None
622
 
 
623
 
            if include_bundle:
624
 
                bundle = base64.b64encode(klass._generate_bundle(repository, revision_id,
625
 
                    ancestor_id))
626
 
            else:
627
 
                bundle = None
628
 
 
629
 
            if public_branch is not None and not include_bundle:
630
 
                public_branch_obj = _mod_branch.Branch.open(public_branch)
631
 
                public_branch_obj.lock_read()
632
 
                locked.append(public_branch_obj)
633
 
                if not public_branch_obj.repository.has_revision(
634
 
                    revision_id):
635
 
                    raise errors.PublicBranchOutOfDate(public_branch,
636
 
                                                       revision_id)
637
 
            testament_sha1 = t.as_sha1()
638
 
        finally:
639
 
            for entry in reversed(locked):
640
 
                entry.unlock()
641
 
        return klass(revision_id, testament_sha1, time, timezone,
642
 
            target_branch, patch, public_branch, message, bundle,
643
 
            base_revision_id)
644
 
 
645
 
    def _verify_patch(self, repository):
646
 
        calculated_patch = self._generate_diff(repository, self.revision_id,
647
 
                                               self.base_revision_id)
648
 
        # Convert line-endings to UNIX
649
 
        stored_patch = re.sub(b'\r\n?', b'\n', self.patch)
650
 
        calculated_patch = re.sub(b'\r\n?', b'\n', calculated_patch)
651
 
        # Strip trailing whitespace
652
 
        calculated_patch = re.sub(b' *\n', b'\n', calculated_patch)
653
 
        stored_patch = re.sub(b' *\n', b'\n', stored_patch)
654
 
        return (calculated_patch == stored_patch)
655
 
 
656
 
    def get_merge_request(self, repository):
657
 
        """Provide data for performing a merge
658
 
 
659
 
        Returns suggested base, suggested target, and patch verification status
660
 
        """
661
 
        verified = self._maybe_verify(repository)
662
 
        return self.base_revision_id, self.revision_id, verified
663
 
 
664
 
    def _maybe_verify(self, repository):
665
 
        if self.patch is not None:
666
 
            if self._verify_patch(repository):
667
 
                return 'verified'
668
 
            else:
669
 
                return 'failed'
670
 
        else:
671
 
            return 'inapplicable'
672
 
 
673
 
 
674
 
class MergeDirectiveFormatRegistry(registry.Registry):
675
 
 
676
 
    def register(self, directive, format_string=None):
677
 
        if format_string is None:
678
 
            format_string = directive._format_string
679
 
        registry.Registry.register(self, format_string, directive)
680
 
 
681
 
 
682
 
_format_registry = MergeDirectiveFormatRegistry()
683
 
_format_registry.register(MergeDirective)
684
 
_format_registry.register(MergeDirective2)
685
 
# 0.19 never existed.  It got renamed to 0.90.  But by that point, there were
686
 
# already merge directives in the wild that used 0.19. Registering with the old
687
 
# format string to retain compatibility with those merge directives.
688
 
_format_registry.register(MergeDirective2,
689
 
                          b'Bazaar merge directive format 2 (Bazaar 0.19)')