~launchpad-pqm/launchpad/devel

7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
1
# Copyright 2008 Canonical Ltd.  All rights reserved.
2
3
__metaclass__ = type
4
5
import operator
6
import re
7
import transaction
8
7659.3.24 by Aaron Bentley
Apply bundles to hosted location, not target.
9
from bzrlib.branch import Branch
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
10
from bzrlib.errors import NotAMergeDirective, NotBranchError
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
11
from bzrlib.merge_directive import MergeDirective
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
12
from bzrlib.transport import get_transport
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
13
from sqlobject import SQLObjectNotFound
14
15
from zope.component import getUtility
16
from zope.interface import implements
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
17
from zope.security.proxy import removeSecurityProxy
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
18
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
19
from lp.code.interfaces.branch import BranchType
20
from lp.code.interfaces.branchlookup import IBranchLookup
21
from lp.code.interfaces.branchmergeproposal import (
7573.2.2 by Paul Hummer
Added try/except block, catching the oops
22
    BranchMergeProposalExists, IBranchMergeProposalGetter,
7659.3.6 by Aaron Bentley
Use Job to create MP
23
    ICreateMergeProposalJobSource, UserNotBranchReviewer)
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
24
from lp.code.interfaces.branchnamespace import (
7659.3.26 by Aaron Bentley
Cleanup, refactoring
25
    lookup_branch_namespace, split_unique_name)
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
26
from lp.code.interfaces.codereviewcomment import CodeReviewVote
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
27
from canonical.launchpad.interfaces.diff import IStaticDiffSource
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
28
from canonical.launchpad.interfaces.mail import (
29
    IMailHandler, EmailProcessingError)
30
from canonical.launchpad.interfaces.message import IMessageSet
31
from canonical.launchpad.mail.commands import (
32
    EmailCommand, EmailCommandCollection)
33
from canonical.launchpad.mail.helpers import (
34
    ensure_not_weakly_authenticated, get_error_message, get_main_body,
35
    get_person_or_team, IncomingEmailError, parse_commands)
7573.2.4 by Paul Hummer
Fixed little bugs to get the test failing properly
36
from canonical.launchpad.mail.sendmail import simple_sendmail
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
37
from canonical.launchpad.mailnotification import (
38
    send_process_error_notification)
39
from canonical.launchpad.webapp import urlparse
40
from canonical.launchpad.webapp.interfaces import ILaunchBag
7864.2.1 by Leonard Richardson
Started using lazr.uri instead of the built-in uri library.
41
from lazr.uri import URI
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
42
43
44
class BadBranchMergeProposalAddress(Exception):
45
    """The user-supplied address is not an acceptable value."""
46
47
class InvalidBranchMergeProposalAddress(BadBranchMergeProposalAddress):
48
    """The user-supplied address is not an acceptable value."""
49
50
class NonExistantBranchMergeProposalAddress(BadBranchMergeProposalAddress):
51
    """The BranchMergeProposal specified by the address does not exist."""
52
53
class InvalidVoteString(Exception):
54
    """The user-supplied vote is not an acceptable value."""
55
56
57
class NonLaunchpadTarget(Exception):
58
    """Target branch is not registered with Launchpad."""
59
60
61
class MissingMergeDirective(Exception):
62
    """Emailed merge proposal lacks a merge directive"""
63
64
65
class CodeReviewEmailCommandExecutionContext:
7372.2.15 by Tim Penhey
Add docstring.
66
    """Passed as the only parameter to each code review email command.
67
68
    The execution context is created once for each email and then passed to
69
    each command object as the execution parameter.  The resulting vote and
70
    vote tags in the context are used in the final code review comment
71
    creation.
72
    """
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
73
7658.3.16 by Stuart Bishop
Reapply backed out db changes
74
    def __init__(self, merge_proposal, user, notify_event_listeners=True):
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
75
        self.merge_proposal = merge_proposal
76
        self.user = user
77
        self.vote = None
78
        self.vote_tags = None
7658.3.16 by Stuart Bishop
Reapply backed out db changes
79
        self.notify_event_listeners = notify_event_listeners
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
80
81
82
class CodeReviewEmailCommand(EmailCommand):
83
    """Commands specific to code reviews."""
84
85
    # Some code commands need to happen before others, so we order them.
86
    sort_order = 1
87
88
    def execute(self, context):
89
        raise NotImplementedError
90
91
92
class VoteEmailCommand(CodeReviewEmailCommand):
93
    """Record the vote to add to the comment."""
94
95
    # Votes should happen first, so set the order lower than
96
    # status updates.
97
    sort_order = 0
98
99
    _vote_alias = {
100
        '+1': CodeReviewVote.APPROVE,
101
        '+0': CodeReviewVote.ABSTAIN,
102
        '0': CodeReviewVote.ABSTAIN,
103
        '-0': CodeReviewVote.ABSTAIN,
104
        '-1': CodeReviewVote.DISAPPROVE,
7573.2.1 by Paul Hummer
needs_fixing review command is now aliased to needsfixing and needs-fixing
105
        'needsfixing': CodeReviewVote.NEEDS_FIXING,
106
        'needs-fixing': CodeReviewVote.NEEDS_FIXING,
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
107
        }
108
109
    def execute(self, context):
110
        """Extract the vote and tags from the args."""
111
        if len(self.string_args) == 0:
112
            raise EmailProcessingError(
113
                get_error_message(
114
                    'num-arguments-mismatch.txt',
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
115
                    command_name='review',
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
116
                    num_arguments_expected='one or more',
117
                    num_arguments_got='0'))
118
119
        vote_string = self.string_args[0]
120
        vote_tag_list = self.string_args[1:]
121
        try:
122
            context.vote = CodeReviewVote.items[vote_string.upper()]
123
        except KeyError:
124
            # If the word doesn't match, check aliases that we allow.
125
            context.vote = self._vote_alias.get(vote_string)
126
            if context.vote is None:
7372.2.6 by Tim Penhey
Fix existing tests.
127
                valid_votes = ', '.join(sorted(
128
                    v.name.lower() for v in CodeReviewVote.items.items))
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
129
                raise EmailProcessingError(
130
                    get_error_message(
131
                        'dbschema-command-wrong-argument.txt',
132
                        command_name='review',
133
                        arguments=valid_votes,
134
                        example_argument='needs_fixing'))
135
136
        if len(vote_tag_list) > 0:
137
            context.vote_tags = ' '.join(vote_tag_list)
138
139
140
class UpdateStatusEmailCommand(CodeReviewEmailCommand):
141
    """Update the status of the merge proposal."""
142
143
    _numberOfArguments = 1
144
145
    def execute(self, context):
146
        """Update the status of the merge proposal."""
147
        # Only accepts approved, and rejected for now.
148
        self._ensureNumberOfArguments()
149
        new_status = self.string_args[0].lower()
150
        # Grab the latest rev_id from the source branch.
151
        # This is what the browser code does right now.
152
        rev_id = context.merge_proposal.source_branch.last_scanned_id
7372.2.9 by Tim Penhey
More tests.
153
        try:
154
            if new_status in ('approved', 'approve'):
155
                if context.vote is None:
156
                    context.vote = CodeReviewVote.APPROVE
157
                context.merge_proposal.approveBranch(context.user, rev_id)
158
            elif new_status in ('rejected', 'reject'):
159
                if context.vote is None:
160
                    context.vote = CodeReviewVote.DISAPPROVE
161
                context.merge_proposal.rejectBranch(context.user, rev_id)
162
            else:
163
                raise EmailProcessingError(
164
                    get_error_message(
165
                        'dbschema-command-wrong-argument.txt',
166
                        command_name=self.name,
167
                        arguments='approved, rejected',
168
                        example_argument='approved'))
169
        except UserNotBranchReviewer:
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
170
            raise EmailProcessingError(
171
                get_error_message(
7372.2.9 by Tim Penhey
More tests.
172
                    'user-not-reviewer.txt',
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
173
                    command_name=self.name,
7372.2.9 by Tim Penhey
More tests.
174
                    target=context.merge_proposal.target_branch.bzr_identity))
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
175
176
177
class AddReviewerEmailCommand(CodeReviewEmailCommand):
178
    """Add a new reviewer."""
179
180
    def execute(self, context):
181
        if len(self.string_args) == 0:
182
            raise EmailProcessingError(
183
                get_error_message(
184
                    'num-arguments-mismatch.txt',
185
                    command_name=self.name,
186
                    num_arguments_expected='one or more',
187
                    num_arguments_got='0'))
188
7372.2.9 by Tim Penhey
More tests.
189
        # Pop the first arg as the reviewer.
190
        reviewer = get_person_or_team(self.string_args.pop(0))
191
        if len(self.string_args) > 0:
192
            review_tags = ' '.join(self.string_args)
193
        else:
194
            review_tags = None
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
195
7372.2.9 by Tim Penhey
More tests.
196
        context.merge_proposal.nominateReviewer(
7658.3.16 by Stuart Bishop
Reapply backed out db changes
197
            reviewer, context.user, review_tags,
198
            _notify_listeners=context.notify_event_listeners)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
199
200
201
class CodeEmailCommands(EmailCommandCollection):
202
    """A colleciton of email commands for code."""
203
204
    _commands = {
205
        'vote': VoteEmailCommand,
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
206
        'review': VoteEmailCommand,
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
207
        'status': UpdateStatusEmailCommand,
208
        'reviewer': AddReviewerEmailCommand,
209
        }
210
7372.2.3 by Tim Penhey
Get the commands directly from the CodeEmailCommands class.
211
    @classmethod
212
    def getCommands(klass, message_body):
213
        """Extract the commands from the message body."""
214
        if message_body is None:
215
            return []
216
        commands = [klass.get(name=name, string_args=args) for
217
                    name, args in parse_commands(message_body,
218
                                                 klass._commands.keys())]
219
        return sorted(commands, key=operator.attrgetter('sort_order'))
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
220
221
222
class CodeHandler:
223
    """Mail handler for the code domain."""
224
    implements(IMailHandler)
225
226
    addr_pattern = re.compile(r'(mp\+)([^@]+).*')
227
    allow_unknown_users = False
228
229
    def process(self, mail, email_addr, file_alias):
230
        """Process an email for the code domain.
231
232
        Emails may be converted to CodeReviewComments, and / or
7659.3.16 by Aaron Bentley
Updates from review
233
        deferred to jobs to create BranchMergeProposals.
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
234
        """
7676.2.8 by Paul Hummer
Dialed in the exception handling to now support the job infrastructure as well
235
        if email_addr.startswith('merge@'):
7675.6.1 by Tim Penhey
Require merge directive emails to be signed.
236
            return self.createMergeProposalJob(mail, email_addr, file_alias)
7676.2.8 by Paul Hummer
Dialed in the exception handling to now support the job infrastructure as well
237
        else:
238
            try:
7676.2.5 by Paul Hummer
Fixed the missing subject bug
239
                return self.processComment(mail, email_addr, file_alias)
7676.2.8 by Paul Hummer
Dialed in the exception handling to now support the job infrastructure as well
240
            except AssertionError:
241
                body = get_error_message('messagemissingsubject.txt')
242
                simple_sendmail('merge@code.launchpad.net',
243
                    [mail.get('from')],
244
                    'Error Creating Merge Proposal', body)
7675.6.5 by Tim Penhey
Merge db-devel and resolve conflicts.
245
                return True
7675.6.1 by Tim Penhey
Require merge directive emails to be signed.
246
247
    def createMergeProposalJob(self, mail, email_addr, file_alias):
248
        """Check that the message is signed and create the job."""
7675.60.13 by Tim Penhey
Re-enable the signed merge directive requirement.
249
        try:
250
            ensure_not_weakly_authenticated(
251
                mail, email_addr, 'not-signed-md.txt',
252
                'key-not-registered-md.txt')
253
        except IncomingEmailError, error:
254
            user = getUtility(ILaunchBag).user
255
            send_process_error_notification(
256
                str(user.preferredemail.email),
257
                'Submit Request Failure',
258
                error.message, mail, error.failing_command)
259
            transaction.abort()
260
        else:
261
            getUtility(ICreateMergeProposalJobSource).create(file_alias)
7675.6.1 by Tim Penhey
Require merge directive emails to be signed.
262
        return True
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
263
7658.3.16 by Stuart Bishop
Reapply backed out db changes
264
    def processCommands(self, context, email_body_text):
265
        """Process the commadns in the email_body_text against the context."""
266
        commands = CodeEmailCommands.getCommands(email_body_text)
267
268
        processing_errors = []
269
270
        for command in commands:
271
            try:
272
                command.execute(context)
273
            except EmailProcessingError, error:
274
                processing_errors.append((error, command))
275
276
        if len(processing_errors) > 0:
277
            errors, commands = zip(*processing_errors)
278
            raise IncomingEmailError(
279
                '\n'.join(str(error) for error in errors),
280
                list(commands))
281
282
        return len(commands)
283
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
284
    def processComment(self, mail, email_addr, file_alias):
285
        """Process an email and create a CodeReviewComment.
286
287
        The only mail command understood is 'vote', which takes 'approve',
288
        'disapprove', or 'abstain' as values.  Specifically, it takes
289
        any CodeReviewVote item value, case-insensitively.
290
        :return: True.
291
        """
292
        try:
293
            merge_proposal = self.getBranchMergeProposal(email_addr)
294
        except BadBranchMergeProposalAddress:
295
            return False
296
297
        user = getUtility(ILaunchBag).user
298
        context = CodeReviewEmailCommandExecutionContext(merge_proposal, user)
299
        try:
7658.3.16 by Stuart Bishop
Reapply backed out db changes
300
            email_body_text = get_main_body(mail)
301
            processed_count = self.processCommands(context, email_body_text)
302
303
            # Make sure that the email is in fact signed.
304
            if processed_count > 0:
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
305
                ensure_not_weakly_authenticated(mail, 'code review')
306
307
            message = getUtility(IMessageSet).fromEmail(
308
                mail.parsed_string,
309
                owner=getUtility(ILaunchBag).user,
310
                filealias=file_alias,
311
                parsed_message=mail)
312
            comment = merge_proposal.createCommentFromMessage(
7407.1.14 by Tim Penhey
Merge RF and resolve conflict.
313
                message, context.vote, context.vote_tags, mail)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
314
315
        except IncomingEmailError, error:
316
            send_process_error_notification(
317
                str(user.preferredemail.email),
318
                'Submit Request Failure',
319
                error.message, mail, error.failing_command)
7372.2.6 by Tim Penhey
Fix existing tests.
320
            transaction.abort()
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
321
        return True
322
323
    @staticmethod
324
    def _getReplyAddress(mail):
325
        """The address to use for automatic replies."""
326
        return mail.get('Reply-to', mail['From'])
327
328
    @classmethod
329
    def getBranchMergeProposal(klass, email_addr):
330
        """Return branch merge proposal designated by email_addr.
331
332
        Addresses are of the form mp+5@code.launchpad.net, where 5 is the
333
        database id of the related branch merge proposal.
334
335
        The inverse operation is BranchMergeProposal.address.
336
        """
337
        match = klass.addr_pattern.match(email_addr)
338
        if match is None:
339
            raise InvalidBranchMergeProposalAddress(email_addr)
340
        try:
341
            merge_proposal_id = int(match.group(2))
342
        except ValueError:
343
            raise InvalidBranchMergeProposalAddress(email_addr)
344
        getter = getUtility(IBranchMergeProposalGetter)
345
        try:
346
            return getter.get(merge_proposal_id)
347
        except SQLObjectNotFound:
348
            raise NonExistantBranchMergeProposalAddress(email_addr)
349
350
    def _acquireBranchesForProposal(self, md, submitter):
351
        """Find or create DB Branches from a MergeDirective.
352
353
        If the target is not a Launchpad branch, NonLaunchpadTarget will be
354
        raised.  If the source is not a Launchpad branch, a REMOTE branch will
355
        be created implicitly, with submitter as its owner/registrant.
356
357
        :param md: The `MergeDirective` to get branch URLs from.
358
        :param submitter: The `Person` who requested that the merge be
359
            performed.
360
        :return: source_branch, target_branch
361
        """
7940.2.5 by Jonathan Lange
Move branch lookup methods to IBranchLookup
362
        mp_target = getUtility(IBranchLookup).getByUrl(md.target_branch)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
363
        if mp_target is None:
364
            raise NonLaunchpadTarget()
7675.100.2 by Tim Penhey
XXX comment.
365
        # XXX TimPenhey 2009-04-01 bug 352800
366
        # Disabled pull processing until we can create stacked branches.
367
        if True: # md.bundle is None:
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
368
            mp_source = self._getSourceNoBundle(
7659.3.26 by Aaron Bentley
Cleanup, refactoring
369
                md, mp_target, submitter)
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
370
        else:
371
            mp_source = self._getSourceWithBundle(
372
                md, mp_target, submitter)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
373
        return mp_source, mp_target
374
7659.3.25 by Aaron Bentley
Improve branch name selection.
375
    @staticmethod
7779.1.5 by Jonathan Lange
Change the property from 'container' to 'target'
376
    def _getNewBranchInfo(url, target_branch, submitter):
7659.3.26 by Aaron Bentley
Cleanup, refactoring
377
        """Return the namespace and basename for a branch.
378
379
        If an LP URL is provided, the namespace and basename will match the
380
        LP URL.
381
382
        Otherwise, the target is used to determine the namespace, and the base
383
        depends on what was supplied.
384
385
        If a URL is supplied, its base is used.
386
387
        If no URL is supplied, 'merge' is used as the base.
388
389
        :param url: The public URL of the source branch, if any.
7779.1.5 by Jonathan Lange
Change the property from 'container' to 'target'
390
        :param target_branch: The target branch.
7659.3.27 by Aaron Bentley
Get namespace from target container.
391
        :param submitter: The person submitting the merge proposal.
7659.3.26 by Aaron Bentley
Cleanup, refactoring
392
        """
7659.3.25 by Aaron Bentley
Improve branch name selection.
393
        if url is not None:
7940.2.5 by Jonathan Lange
Move branch lookup methods to IBranchLookup
394
            branches = getUtility(IBranchLookup)
7940.2.2 by Jonathan Lange
Rename URIToUniqueName to uriToUniqueName, to make it standards compliant.
395
            unique_name = branches.uriToUniqueName(URI(url))
7659.3.26 by Aaron Bentley
Cleanup, refactoring
396
            if unique_name is not None:
397
                namespace_name, base = split_unique_name(unique_name)
7659.3.25 by Aaron Bentley
Improve branch name selection.
398
                return lookup_branch_namespace(namespace_name), base
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
399
        if url is None:
400
            basename = 'merge'
401
        else:
402
            basename = urlparse(url)[2].split('/')[-1]
7779.1.5 by Jonathan Lange
Change the property from 'container' to 'target'
403
        namespace = target_branch.target.getNamespace(submitter)
7659.3.26 by Aaron Bentley
Cleanup, refactoring
404
        return namespace, basename
405
406
    def _getNewBranch(self, branch_type, url, target, submitter):
407
        """Return a new database branch.
408
409
        :param branch_type: The type of branch to create.
410
        :param url: The public location of the branch to create.
411
        :param product: The product associated with the branch to create.
412
        :param submitter: The person who requested the merge.
413
        """
7659.3.27 by Aaron Bentley
Get namespace from target container.
414
        namespace, basename = self._getNewBranchInfo(url, target, submitter)
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
415
        if branch_type == BranchType.REMOTE:
416
            db_url = url
417
        else:
418
            db_url = None
419
        return namespace.createBranchWithPrefix(
420
            branch_type, basename, submitter, url=db_url)
421
7659.3.26 by Aaron Bentley
Cleanup, refactoring
422
    def _getSourceNoBundle(self, md, target, submitter):
423
        """Get a source branch for a merge directive with no bundle."""
7675.100.3 by Tim Penhey
Disable tests that rely on real branches being created.
424
        mp_source = None
425
        if md.source_branch is not None:
426
            mp_source = getUtility(IBranchLookup).getByUrl(md.source_branch)
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
427
        if mp_source is None:
428
            mp_source = self._getNewBranch(
7659.3.26 by Aaron Bentley
Cleanup, refactoring
429
                BranchType.REMOTE, md.source_branch, target, submitter)
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
430
        return mp_source
431
432
    def _getSourceWithBundle(self, md, target, submitter):
7659.3.26 by Aaron Bentley
Cleanup, refactoring
433
        """Get a source branch for a merge directive with a bundle."""
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
434
        mp_source = None
435
        if md.source_branch is not None:
7940.2.5 by Jonathan Lange
Move branch lookup methods to IBranchLookup
436
            mp_source = getUtility(IBranchLookup).getByUrl(md.source_branch)
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
437
        if mp_source is None:
438
            mp_source = self._getNewBranch(
7659.3.26 by Aaron Bentley
Cleanup, refactoring
439
                BranchType.HOSTED, md.source_branch, target,
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
440
                submitter)
7659.3.37 by Aaron Bentley
Get create_merge_proposals under test with bundles.
441
        transaction.commit()
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
442
        assert mp_source.branch_type == BranchType.HOSTED
443
        try:
7659.3.24 by Aaron Bentley
Apply bundles to hosted location, not target.
444
            bzr_branch = Branch.open(mp_source.getPullURL())
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
445
        except NotBranchError:
446
            bzr_target = removeSecurityProxy(target).getBzrBranch()
447
            transport = get_transport(
7659.3.24 by Aaron Bentley
Apply bundles to hosted location, not target.
448
                mp_source.getPullURL(),
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
449
                possible_transports=[bzr_target.bzrdir.root_transport])
450
            bzrdir = bzr_target.bzrdir.clone_on_transport(transport)
451
            bzr_branch = bzrdir.open_branch()
7659.3.37 by Aaron Bentley
Get create_merge_proposals under test with bundles.
452
        # Don't attempt to use public-facing urls.
453
        md.target_branch = target.warehouse_url
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
454
        md.install_revisions(bzr_branch.repository)
455
        bzr_branch.pull(bzr_branch, stop_revision=md.revision_id)
7659.3.23 by Aaron Bentley
Ensure a mirror is requested.
456
        mp_source.requestMirror()
7659.3.20 by Aaron Bentley
Initial work on supporting merge directive bundles.
457
        return mp_source
458
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
459
    def findMergeDirectiveAndComment(self, message):
460
        """Extract the comment and Merge Directive from a SignedMessage."""
461
        body = None
462
        md = None
463
        for part in message.walk():
464
            if part.is_multipart():
465
                continue
466
            payload = part.get_payload(decode=True)
467
            if part['Content-type'].startswith('text/plain'):
468
                body = payload
469
            try:
470
                md = MergeDirective.from_lines(payload.splitlines(True))
471
            except NotAMergeDirective:
472
                pass
473
            if None not in (body, md):
474
                return body, md
475
        else:
476
            raise MissingMergeDirective()
477
478
    def processMergeProposal(self, message):
479
        """Generate a merge proposal (and comment) from an email message.
480
481
        The message is expected to contain a merge directive in one of its
482
        parts.  Its values are used to generate a BranchMergeProposal.
483
        If the message has a non-empty body, it is turned into a
484
        CodeReviewComment.
485
        """
486
        submitter = getUtility(ILaunchBag).user
7667.7.2 by Paul Hummer
Fixed the oops with the missing merge directive
487
        try:
488
            comment_text, md = self.findMergeDirectiveAndComment(message)
489
        except MissingMergeDirective:
490
            body = get_error_message('missingmergedirective.txt')
491
            simple_sendmail('merge@code.launchpad.net',
492
                [message.get('from')],
493
                'Error Creating Merge Proposal', body)
494
            return
7676.2.3 by Paul Hummer
Fixed the test for NonLaunchpadTarget
495
496
        try:
497
            source, target = self._acquireBranchesForProposal(md, submitter)
498
        except NonLaunchpadTarget:
499
            body = get_error_message('nonlaunchpadtarget.txt',
500
                target_branch=md.target_branch)
501
            simple_sendmail('merge@code.launchpad.net',
502
                [message.get('from')],
503
                'Error Creating Merge Proposal', body)
504
            return
505
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
506
        if md.patch is not None:
507
            diff_source = getUtility(IStaticDiffSource)
7735.4.11 by Tim Penhey
Update the filenames for the static diffs.
508
            # XXX: Tim Penhey, 2009-02-12, bug 328271
509
            # If the branch is private we should probably use the restricted
510
            # librarian.
511
            # Using the .txt suffix to allow users to view the file in
512
            # firefox without firefox trying to get them to download it.
513
            filename = '%s.diff.txt' % source.name
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
514
            review_diff = diff_source.acquireFromText(
7735.4.11 by Tim Penhey
Update the filenames for the static diffs.
515
                md.base_revision_id, md.revision_id, md.patch,
516
                filename=filename)
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
517
            transaction.commit()
518
        else:
519
            review_diff = None
7658.3.16 by Stuart Bishop
Reapply backed out db changes
520
7573.2.2 by Paul Hummer
Added try/except block, catching the oops
521
        try:
522
            bmp = source.addLandingTarget(submitter, target,
523
                                          needs_review=True,
524
                                          review_diff=review_diff)
7573.2.4 by Paul Hummer
Fixed little bugs to get the test failing properly
525
7658.3.16 by Stuart Bishop
Reapply backed out db changes
526
            context = CodeReviewEmailCommandExecutionContext(
527
                bmp, submitter, notify_event_listeners=False)
528
            processed_count = self.processCommands(context, comment_text)
529
7675.6.3 by Tim Penhey
Add in the default reviewer for reviews generated using merge directives.
530
            # If there are no reviews requested yet, request the default
531
            # reviewer of the target branch.
532
            if bmp.votes.count() == 0:
533
                bmp.nominateReviewer(
534
                    target.code_reviewer, submitter, None,
535
                    _notify_listeners=False)
536
7573.2.4 by Paul Hummer
Fixed little bugs to get the test failing properly
537
            if comment_text.strip() == '':
538
                comment = None
539
            else:
540
                comment = bmp.createComment(
7658.3.16 by Stuart Bishop
Reapply backed out db changes
541
                    submitter, message['Subject'], comment_text,
542
                    _notify_listeners=False)
7573.2.4 by Paul Hummer
Fixed little bugs to get the test failing properly
543
            return bmp, comment
544
7573.2.2 by Paul Hummer
Added try/except block, catching the oops
545
        except BranchMergeProposalExists:
7573.2.3 by Paul Hummer
Added code to handle the BranchMergeProposalExists exception
546
            body = get_error_message(
547
                'branchmergeproposal-exists.txt',
7573.2.6 by Paul Hummer
Fixed the broken test
548
                source_branch=source.bzr_identity,
549
                target_branch=target.bzr_identity)
7573.2.4 by Paul Hummer
Fixed little bugs to get the test failing properly
550
            simple_sendmail('merge@code.launchpad.net',
7660.5.2 by Paul Hummer
Fixed the bug
551
                [message.get('from')],
7573.2.3 by Paul Hummer
Added code to handle the BranchMergeProposalExists exception
552
                'Error Creating Merge Proposal', body)
7658.3.16 by Stuart Bishop
Reapply backed out db changes
553
            transaction.abort()
554
        except IncomingEmailError, error:
555
            send_process_error_notification(
556
                str(submitter.preferredemail.email),
557
                'Submit Request Failure',
558
                error.message, comment_text, error.failing_command)
559
            transaction.abort()
7573.2.2 by Paul Hummer
Added try/except block, catching the oops
560