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