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 |