~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge_directive.py

  • Committer: Martin Pool
  • Date: 2010-01-29 10:36:23 UTC
  • mto: This revision was merged to the branch mainline in revision 4992.
  • Revision ID: mbp@sourcefrog.net-20100129103623-hywka5hymo5z13jw
Change url to canonical.com or wiki, plus some doc improvements in passing

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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
from email import Message
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
19
18
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,
26
28
    revision as _mod_revision,
27
29
    rio,
28
30
    testament,
29
31
    timestamp,
 
32
    trace,
30
33
    )
31
34
from bzrlib.bundle import (
32
35
    serializer as bundle_serializer,
33
36
    )
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'
 
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()
53
70
 
54
71
    def __init__(self, revision_id, testament_sha1, time, timezone,
55
 
                 target_branch, patch=None, patch_type=None,
56
 
                 source_branch=None, message=None):
 
72
                 target_branch, patch=None, source_branch=None, message=None,
 
73
                 bundle=None):
57
74
        """Constructor.
58
75
 
59
76
        :param revision_id: The revision to merge
63
80
        :param timezone: The timezone offset
64
81
        :param target_branch: The branch to apply the merge to
65
82
        :param patch: The text of a diff or bundle
66
 
        :param patch_type: None, "diff" or "bundle", depending on the contents
67
 
            of patch
68
83
        :param source_branch: A public location to merge the revision from
69
84
        :param message: The message to use when committing this merge
70
85
        """
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)
76
86
        self.revision_id = revision_id
77
87
        self.testament_sha1 = testament_sha1
78
88
        self.time = time
79
89
        self.timezone = timezone
80
90
        self.target_branch = target_branch
81
91
        self.patch = patch
82
 
        self.patch_type = patch_type
83
92
        self.source_branch = source_branch
84
93
        self.message = message
85
94
 
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
 
 
127
 
    def to_lines(self):
 
95
    def _to_lines(self, base_revision=False):
128
96
        """Serialize as a list of lines
129
97
 
130
98
        :return: a list of lines
136
104
        for key in ('source_branch', 'message'):
137
105
            if self.__dict__[key] is not None:
138
106
                stanza.add(key, self.__dict__[key])
 
107
        if base_revision:
 
108
            stanza.add('base_revision_id', self.base_revision_id)
139
109
        lines = ['# ' + self._format_string + '\n']
140
110
        lines.extend(rio.to_patch_lines(stanza))
141
111
        lines.append('# \n')
142
 
        if self.patch is not None:
143
 
            lines.extend(self.patch.splitlines(True))
144
112
        return lines
145
113
 
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
179
 
 
180
114
    @classmethod
181
115
    def from_objects(klass, repository, revision_id, time, timezone,
182
116
                 target_branch, patch_type='bundle',
202
136
        If the message is not supplied, the message from revision_id will be
203
137
        used for the commit.
204
138
        """
205
 
        t = testament.StrictTestament3.from_revision(repository, revision_id)
 
139
        t_revision_id = revision_id
 
140
        if revision_id == _mod_revision.NULL_REVISION:
 
141
            t_revision_id = None
 
142
        t = testament.StrictTestament3.from_revision(repository, t_revision_id)
206
143
        submit_branch = _mod_branch.Branch.open(target_branch)
207
144
        if submit_branch.get_public_branch() is not None:
208
145
            target_branch = submit_branch.get_public_branch()
210
147
            patch = None
211
148
        else:
212
149
            submit_revision_id = submit_branch.last_revision()
 
150
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
213
151
            repository.fetch(submit_branch.repository, submit_revision_id)
214
 
            ancestor_id = _mod_revision.common_ancestor(revision_id,
215
 
                                                        submit_revision_id,
216
 
                                                        repository)
 
152
            graph = repository.get_graph()
 
153
            ancestor_id = graph.find_unique_lca(revision_id,
 
154
                                                submit_revision_id)
217
155
            type_handler = {'bundle': klass._generate_bundle,
218
156
                            'diff': klass._generate_diff,
219
157
                            None: lambda x, y, z: None }
220
158
            patch = type_handler[patch_type](repository, revision_id,
221
159
                                             ancestor_id)
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)
 
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))
240
184
 
241
185
    @staticmethod
242
186
    def _generate_diff(repository, revision_id, ancestor_id):
253
197
                                       ancestor_id, s)
254
198
        return s.getvalue()
255
199
 
 
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
 
256
231
    def install_revisions(self, target_repo):
257
232
        """Install revisions and return the target revision"""
258
233
        if not target_repo.has_revision(self.revision_id):
259
234
            if self.patch_type == 'bundle':
260
 
                info = bundle_serializer.read_bundle(StringIO(self.patch))
 
235
                info = bundle_serializer.read_bundle(
 
236
                    StringIO(self.get_raw_bundle()))
261
237
                # We don't use the bundle's target revision, because
262
238
                # MergeDirective.revision_id is authoritative.
263
 
                info.install_revisions(target_repo)
 
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)
264
269
            else:
265
270
                source_branch = _mod_branch.Branch.open(self.source_branch)
266
271
                target_repo.fetch(source_branch.repository, self.revision_id)
267
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
        firstline = ""
 
375
        for line in line_iter:
 
376
            if line.startswith('# Bazaar merge directive format '):
 
377
                return _format_registry.get(line[2:].rstrip())._from_lines(
 
378
                    line_iter)
 
379
            firstline = firstline or line.strip()
 
380
        raise errors.NotAMergeDirective(firstline)
 
381
 
 
382
    @classmethod
 
383
    def _from_lines(klass, line_iter):
 
384
        stanza = rio.read_patch_stanza(line_iter)
 
385
        patch_lines = list(line_iter)
 
386
        if len(patch_lines) == 0:
 
387
            patch = None
 
388
            patch_type = None
 
389
        else:
 
390
            patch = ''.join(patch_lines)
 
391
            try:
 
392
                bundle_serializer.read_bundle(StringIO(patch))
 
393
            except (errors.NotABundle, errors.BundleNotSupported,
 
394
                    errors.BadBundle):
 
395
                patch_type = 'diff'
 
396
            else:
 
397
                patch_type = 'bundle'
 
398
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
399
        kwargs = {}
 
400
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
401
                    'source_branch', 'message'):
 
402
            try:
 
403
                kwargs[key] = stanza.get(key)
 
404
            except KeyError:
 
405
                pass
 
406
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
407
        return MergeDirective(time=time, timezone=timezone,
 
408
                              patch_type=patch_type, patch=patch, **kwargs)
 
409
 
 
410
    def to_lines(self):
 
411
        lines = self._to_lines()
 
412
        if self.patch is not None:
 
413
            lines.extend(self.patch.splitlines(True))
 
414
        return lines
 
415
 
 
416
    @staticmethod
 
417
    def _generate_bundle(repository, revision_id, ancestor_id):
 
418
        s = StringIO()
 
419
        bundle_serializer.write_bundle(repository, revision_id,
 
420
                                       ancestor_id, s, '0.9')
 
421
        return s.getvalue()
 
422
 
 
423
    def get_merge_request(self, repository):
 
424
        """Provide data for performing a merge
 
425
 
 
426
        Returns suggested base, suggested target, and patch verification status
 
427
        """
 
428
        return None, self.revision_id, 'inapplicable'
 
429
 
 
430
 
 
431
class MergeDirective2(_BaseMergeDirective):
 
432
 
 
433
    _format_string = 'Bazaar merge directive format 2 (Bazaar 0.90)'
 
434
 
 
435
    def __init__(self, revision_id, testament_sha1, time, timezone,
 
436
                 target_branch, patch=None, source_branch=None, message=None,
 
437
                 bundle=None, base_revision_id=None):
 
438
        if source_branch is None and bundle is None:
 
439
            raise errors.NoMergeSource()
 
440
        _BaseMergeDirective.__init__(self, revision_id, testament_sha1, time,
 
441
            timezone, target_branch, patch, source_branch, message)
 
442
        self.bundle = bundle
 
443
        self.base_revision_id = base_revision_id
 
444
 
 
445
    def _patch_type(self):
 
446
        if self.bundle is not None:
 
447
            return 'bundle'
 
448
        elif self.patch is not None:
 
449
            return 'diff'
 
450
        else:
 
451
            return None
 
452
 
 
453
    patch_type = property(_patch_type)
 
454
 
 
455
    def clear_payload(self):
 
456
        self.patch = None
 
457
        self.bundle = None
 
458
 
 
459
    def get_raw_bundle(self):
 
460
        if self.bundle is None:
 
461
            return None
 
462
        else:
 
463
            return self.bundle.decode('base-64')
 
464
 
 
465
    @classmethod
 
466
    def _from_lines(klass, line_iter):
 
467
        stanza = rio.read_patch_stanza(line_iter)
 
468
        patch = None
 
469
        bundle = None
 
470
        try:
 
471
            start = line_iter.next()
 
472
        except StopIteration:
 
473
            pass
 
474
        else:
 
475
            if start.startswith('# Begin patch'):
 
476
                patch_lines = []
 
477
                for line in line_iter:
 
478
                    if line.startswith('# Begin bundle'):
 
479
                        start = line
 
480
                        break
 
481
                    patch_lines.append(line)
 
482
                else:
 
483
                    start = None
 
484
                patch = ''.join(patch_lines)
 
485
            if start is not None:
 
486
                if start.startswith('# Begin bundle'):
 
487
                    bundle = ''.join(line_iter)
 
488
                else:
 
489
                    raise errors.IllegalMergeDirectivePayload(start)
 
490
        time, timezone = timestamp.parse_patch_date(stanza.get('timestamp'))
 
491
        kwargs = {}
 
492
        for key in ('revision_id', 'testament_sha1', 'target_branch',
 
493
                    'source_branch', 'message', 'base_revision_id'):
 
494
            try:
 
495
                kwargs[key] = stanza.get(key)
 
496
            except KeyError:
 
497
                pass
 
498
        kwargs['revision_id'] = kwargs['revision_id'].encode('utf-8')
 
499
        kwargs['base_revision_id'] =\
 
500
            kwargs['base_revision_id'].encode('utf-8')
 
501
        return klass(time=time, timezone=timezone, patch=patch, bundle=bundle,
 
502
                     **kwargs)
 
503
 
 
504
    def to_lines(self):
 
505
        lines = self._to_lines(base_revision=True)
 
506
        if self.patch is not None:
 
507
            lines.append('# Begin patch\n')
 
508
            lines.extend(self.patch.splitlines(True))
 
509
        if self.bundle is not None:
 
510
            lines.append('# Begin bundle\n')
 
511
            lines.extend(self.bundle.splitlines(True))
 
512
        return lines
 
513
 
 
514
    @classmethod
 
515
    def from_objects(klass, repository, revision_id, time, timezone,
 
516
                 target_branch, include_patch=True, include_bundle=True,
 
517
                 local_target_branch=None, public_branch=None, message=None,
 
518
                 base_revision_id=None):
 
519
        """Generate a merge directive from various objects
 
520
 
 
521
        :param repository: The repository containing the revision
 
522
        :param revision_id: The revision to merge
 
523
        :param time: The POSIX timestamp of the date the request was issued.
 
524
        :param timezone: The timezone of the request
 
525
        :param target_branch: The url of the branch to merge into
 
526
        :param include_patch: If true, include a preview patch
 
527
        :param include_bundle: If true, include a bundle
 
528
        :param local_target_branch: a local copy of the target branch
 
529
        :param public_branch: location of a public branch containing the target
 
530
            revision.
 
531
        :param message: Message to use when committing the merge
 
532
        :return: The merge directive
 
533
 
 
534
        The public branch is always used if supplied.  If no bundle is
 
535
        included, the public branch must be supplied, and will be verified.
 
536
 
 
537
        If the message is not supplied, the message from revision_id will be
 
538
        used for the commit.
 
539
        """
 
540
        locked = []
 
541
        try:
 
542
            repository.lock_write()
 
543
            locked.append(repository)
 
544
            t_revision_id = revision_id
 
545
            if revision_id == 'null:':
 
546
                t_revision_id = None
 
547
            t = testament.StrictTestament3.from_revision(repository,
 
548
                t_revision_id)
 
549
            submit_branch = _mod_branch.Branch.open(target_branch)
 
550
            submit_branch.lock_read()
 
551
            locked.append(submit_branch)
 
552
            if submit_branch.get_public_branch() is not None:
 
553
                target_branch = submit_branch.get_public_branch()
 
554
            submit_revision_id = submit_branch.last_revision()
 
555
            submit_revision_id = _mod_revision.ensure_null(submit_revision_id)
 
556
            graph = repository.get_graph(submit_branch.repository)
 
557
            ancestor_id = graph.find_unique_lca(revision_id,
 
558
                                                submit_revision_id)
 
559
            if base_revision_id is None:
 
560
                base_revision_id = ancestor_id
 
561
            if (include_patch, include_bundle) != (False, False):
 
562
                repository.fetch(submit_branch.repository, submit_revision_id)
 
563
            if include_patch:
 
564
                patch = klass._generate_diff(repository, revision_id,
 
565
                                             base_revision_id)
 
566
            else:
 
567
                patch = None
 
568
 
 
569
            if include_bundle:
 
570
                bundle = klass._generate_bundle(repository, revision_id,
 
571
                    ancestor_id).encode('base-64')
 
572
            else:
 
573
                bundle = None
 
574
 
 
575
            if public_branch is not None and not include_bundle:
 
576
                public_branch_obj = _mod_branch.Branch.open(public_branch)
 
577
                public_branch_obj.lock_read()
 
578
                locked.append(public_branch_obj)
 
579
                if not public_branch_obj.repository.has_revision(
 
580
                    revision_id):
 
581
                    raise errors.PublicBranchOutOfDate(public_branch,
 
582
                                                       revision_id)
 
583
            testament_sha1 = t.as_sha1()
 
584
        finally:
 
585
            for entry in reversed(locked):
 
586
                entry.unlock()
 
587
        return klass(revision_id, testament_sha1, time, timezone,
 
588
            target_branch, patch, public_branch, message, bundle,
 
589
            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)')