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