##// END OF EJS Templates
patchbomb: add TLS and SMTP AUTH support....
Bryan O'Sullivan -
r1226:f3837564 default
parent child Browse files
Show More
@@ -1,260 +1,270
1 1 #!/usr/bin/python
2 2 #
3 3 # Interactive script for sending a collection of Mercurial changesets
4 4 # as a series of patch emails.
5 5 #
6 6 # The series is started off with a "[PATCH 0 of N]" introduction,
7 7 # which describes the series as a whole.
8 8 #
9 9 # Each patch email has a Subject line of "[PATCH M of N] ...", using
10 10 # the first line of the changeset description as the subject text.
11 11 # The message contains two or three body parts:
12 12 #
13 13 # The remainder of the changeset description.
14 14 #
15 15 # [Optional] If the diffstat program is installed, the result of
16 16 # running diffstat on the patch.
17 17 #
18 18 # The patch itself, as generated by "hg export".
19 19 #
20 20 # Each message refers to all of its predecessors using the In-Reply-To
21 21 # and References headers, so they will show up as a sequence in
22 22 # threaded mail and news readers, and in mail archives.
23 23 #
24 24 # For each changeset, you will be prompted with a diffstat summary and
25 25 # the changeset summary, so you can be sure you are sending the right
26 26 # changes.
27 27 #
28 28 # It is best to run this script with the "-n" (test only) flag before
29 29 # firing it up "for real", in which case it will use your pager to
30 30 # display each of the messages that it would send.
31 31 #
32 32 # To configure a default mail host, add a section like this to your
33 33 # hgrc file:
34 34 #
35 35 # [smtp]
36 36 # host = my_mail_host
37 37 # port = 1025
38 # tls = yes # or omit if not needed
39 # username = user # if SMTP authentication required
40 # password = password # if SMTP authentication required - PLAINTEXT
38 41 #
39 42 # To configure other defaults, add a section like this to your hgrc
40 43 # file:
41 44 #
42 45 # [patchbomb]
43 46 # from = My Name <my@email>
44 47 # to = recipient1, recipient2, ...
45 48 # cc = cc1, cc2, ...
46 49
47 50 from email.MIMEMultipart import MIMEMultipart
48 51 from email.MIMEText import MIMEText
49 52 from mercurial import commands
50 53 from mercurial import fancyopts
51 54 from mercurial import hg
52 55 from mercurial import ui
53 56 import os
54 57 import popen2
55 58 import smtplib
56 59 import socket
57 60 import sys
58 61 import tempfile
59 62 import time
60 63
61 64 try:
62 65 # readline gives raw_input editing capabilities, but is not
63 66 # present on windows
64 67 import readline
65 68 except ImportError: pass
66 69
67 70 def diffstat(patch):
68 71 fd, name = tempfile.mkstemp()
69 72 try:
70 73 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
71 74 try:
72 75 for line in patch: print >> p.tochild, line
73 76 p.tochild.close()
74 77 if p.wait(): return
75 78 fp = os.fdopen(fd, 'r')
76 79 stat = []
77 80 for line in fp: stat.append(line.lstrip())
78 81 last = stat.pop()
79 82 stat.insert(0, last)
80 83 stat = ''.join(stat)
81 84 if stat.startswith('0 files'): raise ValueError
82 85 return stat
83 86 except: raise
84 87 finally:
85 88 try: os.unlink(name)
86 89 except: pass
87 90
88 91 def patchbomb(ui, repo, *revs, **opts):
89 92 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
90 93 if default: prompt += ' [%s]' % default
91 94 prompt += rest
92 95 while True:
93 96 r = raw_input(prompt)
94 97 if r: return r
95 98 if default is not None: return default
96 99 if empty_ok: return r
97 100 ui.warn('Please enter a valid value.\n')
98 101
99 102 def confirm(s):
100 103 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
101 104 raise ValueError
102 105
103 106 def cdiffstat(summary, patch):
104 107 s = diffstat(patch)
105 108 if s:
106 109 if summary:
107 110 ui.write(summary, '\n')
108 111 ui.write(s, '\n')
109 112 confirm('Does the diffstat above look okay')
110 113 return s
111 114
112 115 def makepatch(patch, idx, total):
113 116 desc = []
114 117 node = None
115 118 body = ''
116 119 for line in patch:
117 120 if line.startswith('#'):
118 121 if line.startswith('# Node ID'): node = line.split()[-1]
119 122 continue
120 123 if line.startswith('diff -r'): break
121 124 desc.append(line)
122 125 if not node: raise ValueError
123 126
124 127 #body = ('\n'.join(desc[1:]).strip() or
125 128 # 'Patch subject is complete summary.')
126 129 #body += '\n\n\n'
127 130
128 131 if opts['diffstat']:
129 132 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
130 133 body += '\n'.join(patch)
131 134 msg = MIMEText(body)
132 135 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
133 136 if subj.endswith('.'): subj = subj[:-1]
134 137 msg['Subject'] = subj
135 138 msg['X-Mercurial-Node'] = node
136 139 return msg
137 140
138 141 start_time = int(time.time())
139 142
140 143 def genmsgid(id):
141 144 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
142 145
143 146 patches = []
144 147
145 148 class exportee:
146 149 def __init__(self, container):
147 150 self.lines = []
148 151 self.container = container
149 152 self.name = 'email'
150 153
151 154 def write(self, data):
152 155 self.lines.append(data)
153 156
154 157 def close(self):
155 158 self.container.append(''.join(self.lines).split('\n'))
156 159 self.lines = []
157 160
158 161 commands.export(ui, repo, *args, **{'output': exportee(patches),
159 162 'text': None})
160 163
161 164 jumbo = []
162 165 msgs = []
163 166
164 167 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
165 168
166 169 for p, i in zip(patches, range(len(patches))):
167 170 jumbo.extend(p)
168 171 msgs.append(makepatch(p, i + 1, len(patches)))
169 172
170 173 ui.write('\nWrite the introductory message for the patch series.\n\n')
171 174
172 175 sender = (opts['from'] or ui.config('patchbomb', 'from') or
173 176 prompt('From', ui.username()))
174 177
175 178 msg = MIMEMultipart()
176 179 msg['Subject'] = '[PATCH 0 of %d] %s' % (
177 180 len(patches),
178 181 opts['subject'] or
179 182 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
180 183
181 184 def getaddrs(opt, prpt, default = None):
182 185 addrs = opts[opt] or (ui.config('patchbomb', opt) or
183 186 prompt(prpt, default = default)).split(',')
184 187 return [a.strip() for a in addrs if a.strip()]
185 188 to = getaddrs('to', 'To')
186 189 cc = getaddrs('cc', 'Cc', '')
187 190
188 191 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
189 192
190 193 body = []
191 194
192 195 while True:
193 196 try: l = raw_input()
194 197 except EOFError: break
195 198 if l == '.': break
196 199 body.append(l)
197 200
198 201 msg.attach(MIMEText('\n'.join(body) + '\n'))
199 202
200 203 ui.write('\n')
201 204
202 205 if opts['diffstat']:
203 206 d = cdiffstat('Final summary:\n', jumbo)
204 207 if d: msg.attach(MIMEText(d))
205 208
206 209 msgs.insert(0, msg)
207 210
208 211 if not opts['test']:
209 212 s = smtplib.SMTP()
210 213 s.connect(host = ui.config('smtp', 'host', 'mail'),
211 214 port = int(ui.config('smtp', 'port', 25)))
212
215 if ui.configbool('smtp', 'tls'):
216 s.ehlo()
217 s.starttls()
218 s.ehlo()
219 username = ui.config('smtp', 'username')
220 password = ui.config('smtp', 'password')
221 if username and password:
222 s.login(username, password)
213 223 parent = None
214 224 tz = time.strftime('%z')
215 225 for m in msgs:
216 226 try:
217 227 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
218 228 except TypeError:
219 229 m['Message-Id'] = genmsgid('patchbomb')
220 230 if parent:
221 231 m['In-Reply-To'] = parent
222 232 else:
223 233 parent = m['Message-Id']
224 234 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
225 235 start_time += 1
226 236 m['From'] = sender
227 237 m['To'] = ', '.join(to)
228 238 if cc: m['Cc'] = ', '.join(cc)
229 239 ui.status('Sending ', m['Subject'], ' ...\n')
230 240 if opts['test']:
231 241 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
232 242 fp.write(m.as_string(0))
233 243 fp.write('\n')
234 244 fp.close()
235 245 else:
236 246 s.sendmail(sender, to + cc, m.as_string(0))
237 247 if not opts['test']:
238 248 s.close()
239 249
240 250 if __name__ == '__main__':
241 251 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
242 252 ('d', 'diffstat', None, 'add diffstat output to messages'),
243 253 ('f', 'from', '', 'email address of sender'),
244 254 ('n', 'test', None, 'print messages that would be sent'),
245 255 ('s', 'subject', '', 'subject of introductory message'),
246 256 ('t', 'to', [], 'email addresses of recipients')]
247 257 options = {}
248 258 try:
249 259 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
250 260 options)
251 261 except fancyopts.getopt.GetoptError, inst:
252 262 u = ui.ui()
253 263 u.warn('error: %s' % inst)
254 264 sys.exit(1)
255 265
256 266 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
257 267 not options["noninteractive"])
258 268 repo = hg.repository(ui = u)
259 269
260 270 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now