~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Robert Collins
  • Date: 2007-07-15 15:40:37 UTC
  • mto: (2592.3.33 repository)
  • mto: This revision was merged to the branch mainline in revision 2624.
  • Revision ID: robertc@robertcollins.net-20070715154037-3ar8g89decddc9su
Make GraphIndex accept nodes as key, value, references, so that the method
signature is closer to what a simple key->value index delivers. Also
change the behaviour when the reference list count is zero to accept
key, value as nodes, and emit key, value to make it identical in that case
to a simple key->value index. This may not be a good idea, but for now it
seems ok.

Show diffs side-by-side

added added

removed removed

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