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