##// END OF EJS Templates
patchbomb: continue if we can't import readline.
Bryan O'Sullivan -
r1204:b0f6053d default
parent child Browse files
Show More
@@ -1,255 +1,260
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 #
38 #
39 # To configure other defaults, add a section like this to your hgrc
39 # To configure other defaults, add a section like this to your hgrc
40 # file:
40 # file:
41 #
41 #
42 # [patchbomb]
42 # [patchbomb]
43 # from = My Name <my@email>
43 # from = My Name <my@email>
44 # to = recipient1, recipient2, ...
44 # to = recipient1, recipient2, ...
45 # cc = cc1, cc2, ...
45 # cc = cc1, cc2, ...
46
46
47 from email.MIMEMultipart import MIMEMultipart
47 from email.MIMEMultipart import MIMEMultipart
48 from email.MIMEText import MIMEText
48 from email.MIMEText import MIMEText
49 from mercurial import commands
49 from mercurial import commands
50 from mercurial import fancyopts
50 from mercurial import fancyopts
51 from mercurial import hg
51 from mercurial import hg
52 from mercurial import ui
52 from mercurial import ui
53 import os
53 import os
54 import popen2
54 import popen2
55 import readline
56 import smtplib
55 import smtplib
57 import socket
56 import socket
58 import sys
57 import sys
59 import tempfile
58 import tempfile
60 import time
59 import time
61
60
61 try:
62 # readline gives raw_input editing capabilities, but is not
63 # present on windows
64 import readline
65 except ImportError: pass
66
62 def diffstat(patch):
67 def diffstat(patch):
63 fd, name = tempfile.mkstemp()
68 fd, name = tempfile.mkstemp()
64 try:
69 try:
65 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
70 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
66 try:
71 try:
67 for line in patch: print >> p.tochild, line
72 for line in patch: print >> p.tochild, line
68 p.tochild.close()
73 p.tochild.close()
69 if p.wait(): return
74 if p.wait(): return
70 fp = os.fdopen(fd, 'r')
75 fp = os.fdopen(fd, 'r')
71 stat = []
76 stat = []
72 for line in fp: stat.append(line.lstrip())
77 for line in fp: stat.append(line.lstrip())
73 last = stat.pop()
78 last = stat.pop()
74 stat.insert(0, last)
79 stat.insert(0, last)
75 stat = ''.join(stat)
80 stat = ''.join(stat)
76 if stat.startswith('0 files'): raise ValueError
81 if stat.startswith('0 files'): raise ValueError
77 return stat
82 return stat
78 except: raise
83 except: raise
79 finally:
84 finally:
80 try: os.unlink(name)
85 try: os.unlink(name)
81 except: pass
86 except: pass
82
87
83 def patchbomb(ui, repo, *revs, **opts):
88 def patchbomb(ui, repo, *revs, **opts):
84 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
89 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
85 if default: prompt += ' [%s]' % default
90 if default: prompt += ' [%s]' % default
86 prompt += rest
91 prompt += rest
87 while True:
92 while True:
88 r = raw_input(prompt)
93 r = raw_input(prompt)
89 if r: return r
94 if r: return r
90 if default is not None: return default
95 if default is not None: return default
91 if empty_ok: return r
96 if empty_ok: return r
92 ui.warn('Please enter a valid value.\n')
97 ui.warn('Please enter a valid value.\n')
93
98
94 def confirm(s):
99 def confirm(s):
95 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
100 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
96 raise ValueError
101 raise ValueError
97
102
98 def cdiffstat(summary, patch):
103 def cdiffstat(summary, patch):
99 s = diffstat(patch)
104 s = diffstat(patch)
100 if s:
105 if s:
101 if summary:
106 if summary:
102 ui.write(summary, '\n')
107 ui.write(summary, '\n')
103 ui.write(s, '\n')
108 ui.write(s, '\n')
104 confirm('Does the diffstat above look okay')
109 confirm('Does the diffstat above look okay')
105 return s
110 return s
106
111
107 def makepatch(patch, idx, total):
112 def makepatch(patch, idx, total):
108 desc = []
113 desc = []
109 node = None
114 node = None
110 body = ''
115 body = ''
111 for line in patch:
116 for line in patch:
112 if line.startswith('#'):
117 if line.startswith('#'):
113 if line.startswith('# Node ID'): node = line.split()[-1]
118 if line.startswith('# Node ID'): node = line.split()[-1]
114 continue
119 continue
115 if line.startswith('diff -r'): break
120 if line.startswith('diff -r'): break
116 desc.append(line)
121 desc.append(line)
117 if not node: raise ValueError
122 if not node: raise ValueError
118
123
119 #body = ('\n'.join(desc[1:]).strip() or
124 #body = ('\n'.join(desc[1:]).strip() or
120 # 'Patch subject is complete summary.')
125 # 'Patch subject is complete summary.')
121 #body += '\n\n\n'
126 #body += '\n\n\n'
122
127
123 if opts['diffstat']:
128 if opts['diffstat']:
124 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
129 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
125 body += '\n'.join(patch)
130 body += '\n'.join(patch)
126 msg = MIMEText(body)
131 msg = MIMEText(body)
127 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
132 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
128 if subj.endswith('.'): subj = subj[:-1]
133 if subj.endswith('.'): subj = subj[:-1]
129 msg['Subject'] = subj
134 msg['Subject'] = subj
130 msg['X-Mercurial-Node'] = node
135 msg['X-Mercurial-Node'] = node
131 return msg
136 return msg
132
137
133 start_time = int(time.time())
138 start_time = int(time.time())
134
139
135 def genmsgid(id):
140 def genmsgid(id):
136 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
141 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
137
142
138 patches = []
143 patches = []
139
144
140 class exportee:
145 class exportee:
141 def __init__(self, container):
146 def __init__(self, container):
142 self.lines = []
147 self.lines = []
143 self.container = container
148 self.container = container
144 self.name = 'email'
149 self.name = 'email'
145
150
146 def write(self, data):
151 def write(self, data):
147 self.lines.append(data)
152 self.lines.append(data)
148
153
149 def close(self):
154 def close(self):
150 self.container.append(''.join(self.lines).split('\n'))
155 self.container.append(''.join(self.lines).split('\n'))
151 self.lines = []
156 self.lines = []
152
157
153 commands.export(ui, repo, *args, **{'output': exportee(patches),
158 commands.export(ui, repo, *args, **{'output': exportee(patches),
154 'text': None})
159 'text': None})
155
160
156 jumbo = []
161 jumbo = []
157 msgs = []
162 msgs = []
158
163
159 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
164 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
160
165
161 for p, i in zip(patches, range(len(patches))):
166 for p, i in zip(patches, range(len(patches))):
162 jumbo.extend(p)
167 jumbo.extend(p)
163 msgs.append(makepatch(p, i + 1, len(patches)))
168 msgs.append(makepatch(p, i + 1, len(patches)))
164
169
165 ui.write('\nWrite the introductory message for the patch series.\n\n')
170 ui.write('\nWrite the introductory message for the patch series.\n\n')
166
171
167 sender = (opts['from'] or ui.config('patchbomb', 'from') or
172 sender = (opts['from'] or ui.config('patchbomb', 'from') or
168 prompt('From', ui.username()))
173 prompt('From', ui.username()))
169
174
170 msg = MIMEMultipart()
175 msg = MIMEMultipart()
171 msg['Subject'] = '[PATCH 0 of %d] %s' % (
176 msg['Subject'] = '[PATCH 0 of %d] %s' % (
172 len(patches),
177 len(patches),
173 opts['subject'] or
178 opts['subject'] or
174 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
179 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
175
180
176 def getaddrs(opt, prpt, default = None):
181 def getaddrs(opt, prpt, default = None):
177 addrs = opts[opt] or (ui.config('patchbomb', opt) or
182 addrs = opts[opt] or (ui.config('patchbomb', opt) or
178 prompt(prpt, default = default)).split(',')
183 prompt(prpt, default = default)).split(',')
179 return [a.strip() for a in addrs if a.strip()]
184 return [a.strip() for a in addrs if a.strip()]
180 to = getaddrs('to', 'To')
185 to = getaddrs('to', 'To')
181 cc = getaddrs('cc', 'Cc', '')
186 cc = getaddrs('cc', 'Cc', '')
182
187
183 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
188 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
184
189
185 body = []
190 body = []
186
191
187 while True:
192 while True:
188 try: l = raw_input()
193 try: l = raw_input()
189 except EOFError: break
194 except EOFError: break
190 if l == '.': break
195 if l == '.': break
191 body.append(l)
196 body.append(l)
192
197
193 msg.attach(MIMEText('\n'.join(body) + '\n'))
198 msg.attach(MIMEText('\n'.join(body) + '\n'))
194
199
195 ui.write('\n')
200 ui.write('\n')
196
201
197 if opts['diffstat']:
202 if opts['diffstat']:
198 d = cdiffstat('Final summary:\n', jumbo)
203 d = cdiffstat('Final summary:\n', jumbo)
199 if d: msg.attach(MIMEText(d))
204 if d: msg.attach(MIMEText(d))
200
205
201 msgs.insert(0, msg)
206 msgs.insert(0, msg)
202
207
203 if not opts['test']:
208 if not opts['test']:
204 s = smtplib.SMTP()
209 s = smtplib.SMTP()
205 s.connect(host = ui.config('smtp', 'host', 'mail'),
210 s.connect(host = ui.config('smtp', 'host', 'mail'),
206 port = int(ui.config('smtp', 'port', 25)))
211 port = int(ui.config('smtp', 'port', 25)))
207
212
208 parent = None
213 parent = None
209 tz = time.strftime('%z')
214 tz = time.strftime('%z')
210 for m in msgs:
215 for m in msgs:
211 try:
216 try:
212 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
217 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
213 except TypeError:
218 except TypeError:
214 m['Message-Id'] = genmsgid('patchbomb')
219 m['Message-Id'] = genmsgid('patchbomb')
215 if parent:
220 if parent:
216 m['In-Reply-To'] = parent
221 m['In-Reply-To'] = parent
217 else:
222 else:
218 parent = m['Message-Id']
223 parent = m['Message-Id']
219 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
224 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
220 start_time += 1
225 start_time += 1
221 m['From'] = sender
226 m['From'] = sender
222 m['To'] = ', '.join(to)
227 m['To'] = ', '.join(to)
223 if cc: m['Cc'] = ', '.join(cc)
228 if cc: m['Cc'] = ', '.join(cc)
224 ui.status('Sending ', m['Subject'], ' ...\n')
229 ui.status('Sending ', m['Subject'], ' ...\n')
225 if opts['test']:
230 if opts['test']:
226 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
231 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
227 fp.write(m.as_string(0))
232 fp.write(m.as_string(0))
228 fp.write('\n')
233 fp.write('\n')
229 fp.close()
234 fp.close()
230 else:
235 else:
231 s.sendmail(sender, to + cc, m.as_string(0))
236 s.sendmail(sender, to + cc, m.as_string(0))
232 if not opts['test']:
237 if not opts['test']:
233 s.close()
238 s.close()
234
239
235 if __name__ == '__main__':
240 if __name__ == '__main__':
236 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
241 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
237 ('d', 'diffstat', None, 'add diffstat output to messages'),
242 ('d', 'diffstat', None, 'add diffstat output to messages'),
238 ('f', 'from', '', 'email address of sender'),
243 ('f', 'from', '', 'email address of sender'),
239 ('n', 'test', None, 'print messages that would be sent'),
244 ('n', 'test', None, 'print messages that would be sent'),
240 ('s', 'subject', '', 'subject of introductory message'),
245 ('s', 'subject', '', 'subject of introductory message'),
241 ('t', 'to', [], 'email addresses of recipients')]
246 ('t', 'to', [], 'email addresses of recipients')]
242 options = {}
247 options = {}
243 try:
248 try:
244 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
249 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
245 options)
250 options)
246 except fancyopts.getopt.GetoptError, inst:
251 except fancyopts.getopt.GetoptError, inst:
247 u = ui.ui()
252 u = ui.ui()
248 u.warn('error: %s' % inst)
253 u.warn('error: %s' % inst)
249 sys.exit(1)
254 sys.exit(1)
250
255
251 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
256 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
252 not options["noninteractive"])
257 not options["noninteractive"])
253 repo = hg.repository(ui = u)
258 repo = hg.repository(ui = u)
254
259
255 patchbomb(u, repo, *args, **options)
260 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now