1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
#!/usr/bin/env python
"""Send a review to the launchpad-reviews mailing list.
The review should live in a text file which you name on the command line. The
text file must have the full path of the review branch in the first 10 lines.
This script will sniff the branch and CC the person whose branch this is.
"""
from __future__ import with_statement
import os
import pwd
import sys
import time
import smtplib
import optparse
import urlparse
from email.mime.text import MIMEText
from email.utils import formataddr
from bzrlib.branch import Branch
from bzrlib.smtp_connection import SMTPConnection
from bzrlib.workingtree import WorkingTree
LAUNCHPAD_REVIEWS = 'Launchpad Reviews <launchpad-reviews@lists.canonical.com>'
SLASH = '/'
COMMASPACE = ', '
COLON = ':'
# Hack the urlparse module global because it stupidly hardcodes the scheme
# identifiers, doesn't know about bzr+ssh, and doesn't provide an API for
# amending the schemes.
urlparse.uses_netloc.append('bzr+ssh')
def parseargs():
parser = optparse.OptionParser(usage="""\
%prog [options] filename
'filename' is the name of a text file containing your review. You must
include the full branch url on a line by itself within the first 10 lines of
the file. Leading whitespace on the branch url line is ignored.
This script parses the branch author's recipient email address from the branch
url, requiring that the user portion of the path on devpad correspond directly
to a deliverable email address @canonical.com. E.g.:
bzr+ssh://devpad.canonical.com/code/barry/launchpad/review-script
For example, say you were reviewing the above branch, and you had a text file
containing your marked up review. Put that branch url within the first 10
lines, and let's say the file was 'script.txt'. You'd send this review
with the following command line:
% utilities/review script.txt""")
parser.add_option('-f', '--from',
type='string', default=None, dest='sender',
help="""\
The From: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
the envelope sender (i.e. the MAIL FROM). If not given, this is obtained from
the branch using Bazaar.""")
parser.add_option('-t', '--to',
type='string', default=None, dest='to_recipient',
help="""\
The To: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
an envelope recipient (i.e. the RCPT TO). If not given, the recipient's
address is found by looking at the first pending merge in the branch.""")
parser.add_option('-c', '--cc',
type='string', default=[], action='append',
dest='cc_recipients',
help="""\
The Cc: header value used. The value may be any string parseable by
email.utils.parseaddr(). The address portion of the string will be used as
an envelope recipient (i.e. the RCPT TO). Note that
launchpad-reviews@canonical.com is always CC'd.""")
parser.add_option('-d', '--debug-mode',
default=0, action='count',
help="""\
Debug mode. With one -d the message is printed to stdout and no email is
sent. With two -d flags, an email is sent only to you (via the /etc/password
calculated sender or the -f flag) and not to the mailing list.""")
opts, args = parser.parse_args()
if len(args) < 1:
parser.error('Missing review text file name')
if len(args) > 1:
parser.error('Too many arguments')
return parser, opts, args
# I always put the branch name in one of the first few lines
def get_branch_and_user(filename):
"""Parse the branch and user names from a branch url in the file.
Return a 2-tuple of (branch, username). If no matching branch url can be
found within the first 10 lines of the file, (None, None) is returned.
"""
DEVPAD = 'devpad.canonical.com'
LP = 'bazaar.launchpad.net'
with open(filename) as fp:
for i, line in enumerate(fp):
if i >= 10:
# The branch was not in the first 10 lines, so we fail.
break
branch = line.strip()
parts = urlparse.urlsplit(branch)
if not parts.scheme:
continue
path = parts.path.split(SLASH)
if parts.netloc == LP:
username = path[1][1:]
return SLASH.join(path[1:]), username
elif parts.netloc == DEVPAD:
if path[1] <> 'code' or path[2] == 'launchpad':
continue
return SLASH.join(path[2:]), path[2]
else:
continue
return None, None
def get_this_tree():
"""Get the working tree.
First try the branch we're sitting in and if that fails then use the one
this review script lives in.
"""
try:
return WorkingTree.open(os.getcwdu())
except NotBranchError:
tree_root = os.path.dirname(os.path.dirname(__file__))
return WorkingTree.open(tree_root)
def get_most_recent_author(tree):
"""Return the author of the most recent pending merge."""
# The first entry in get_parent_ids() is the most recent revision id. The
# rest are the pending merge ids.
pending_merges = tree.get_parent_ids()[1:]
if len(pending_merges) >= 1:
revision = tree.branch.repository.get_revision(pending_merges[0])
return revision.get_apparent_author()
else:
return None
def main():
parser, opts, args = parseargs()
branch, user = get_branch_and_user(args[0])
if branch is None:
parser.error('No branch url was found in first 10 lines')
# Load the branch we're within.
this_tree = get_this_tree()
this_branch = this_tree.branch
if opts.to_recipient is None:
to_recipient = get_most_recent_author(this_tree)
if to_recipient is None:
to_recipient = user + '@canonical.com'
else:
to_recipient = opts.to_recipient
cc_recipients = [LAUNCHPAD_REVIEWS]
cc_recipients.extend(opts.cc_recipients)
envto = [to_recipient]
envto.extend(cc_recipients)
if opts.sender is None:
sender = this_branch.get_config().username()
if sender is None:
parser.error(
'A sender address could not be determined. Have you set '
'your email address in locations.conf?')
else:
sender = opts.sender
envfrom = sender
# Craft the email message.
with open(args[0]) as fp:
msg = MIMEText(fp.read())
msg['From'] = sender
msg['Subject'] = 'REVIEW: ' + branch
msg['To'] = to_recipient
msg['Cc'] = COMMASPACE.join(cc_recipients)
# In debug mode 1, we print everything to stdout.
if opts.debug_mode == 1:
print 'MAIL FROM:', envfrom
for recipient in envto:
print 'RCPT TO:', recipient
print msg.as_string()
return 0
# In debug mode 2, we mail the message to ourselves.
elif opts.debug_mode > 1:
envto = [sender]
else:
# We already have the SMTP recipients set up as expected, so there's
# nothing more to do.
pass
# Send the message.
SMTPConnection(this_branch.get_config()).send_email(msg)
return 0
if __name__ == '__main__':
sys.exit(main())
|