##// END OF EJS Templates
Get patchbomb working with tip again.
Bryan O'Sullivan -
r1032:706c590c default
parent child Browse files
Show More
@@ -1,248 +1,249
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 for line in patch:
110 for line in patch:
111 if line.startswith('#'):
111 if line.startswith('#'):
112 if line.startswith('# Node ID'): node = line.split()[-1]
112 if line.startswith('# Node ID'): node = line.split()[-1]
113 continue
113 continue
114 if line.startswith('diff -r'): break
114 if line.startswith('diff -r'): break
115 desc.append(line)
115 desc.append(line)
116 if not node: raise ValueError
116 if not node: raise ValueError
117 body = ('\n'.join(desc[1:]).strip() or
117 body = ('\n'.join(desc[1:]).strip() or
118 'Patch subject is complete summary.')
118 'Patch subject is complete summary.')
119 body += '\n\n\n'
119 body += '\n\n\n'
120 if opts['diffstat']:
120 if opts['diffstat']:
121 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
121 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
122 body += '\n'.join(patch)
122 body += '\n'.join(patch)
123 msg = MIMEText(body)
123 msg = MIMEText(body)
124 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
124 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
125 if subj.endswith('.'): subj = subj[:-1]
125 if subj.endswith('.'): subj = subj[:-1]
126 msg['Subject'] = subj
126 msg['Subject'] = subj
127 msg['X-Mercurial-Node'] = node
127 msg['X-Mercurial-Node'] = node
128 return msg
128 return msg
129
129
130 start_time = int(time.time())
130 start_time = int(time.time())
131
131
132 def genmsgid(id):
132 def genmsgid(id):
133 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
133 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
134
134
135 patches = []
135 patches = []
136
136
137 class exportee:
137 class exportee:
138 def __init__(self, container):
138 def __init__(self, container):
139 self.lines = []
139 self.lines = []
140 self.container = container
140 self.container = container
141 self.name = 'email'
141 self.name = 'email'
142
142
143 def write(self, data):
143 def write(self, data):
144 self.lines.append(data)
144 self.lines.append(data)
145
145
146 def close(self):
146 def close(self):
147 self.container.append(''.join(self.lines).split('\n'))
147 self.container.append(''.join(self.lines).split('\n'))
148 self.lines = []
148 self.lines = []
149
149
150 commands.export(ui, repo, *args, **{'output': exportee(patches)})
150 commands.export(ui, repo, *args, **{'output': exportee(patches),
151 'text': None})
151
152
152 jumbo = []
153 jumbo = []
153 msgs = []
154 msgs = []
154
155
155 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
156 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
156
157
157 for p, i in zip(patches, range(len(patches))):
158 for p, i in zip(patches, range(len(patches))):
158 jumbo.extend(p)
159 jumbo.extend(p)
159 msgs.append(makepatch(p, i + 1, len(patches)))
160 msgs.append(makepatch(p, i + 1, len(patches)))
160
161
161 ui.write('\nWrite the introductory message for the patch series.\n\n')
162 ui.write('\nWrite the introductory message for the patch series.\n\n')
162
163
163 sender = (opts['from'] or ui.config('patchbomb', 'from') or
164 sender = (opts['from'] or ui.config('patchbomb', 'from') or
164 prompt('From', ui.username()))
165 prompt('From', ui.username()))
165
166
166 msg = MIMEMultipart()
167 msg = MIMEMultipart()
167 msg['Subject'] = '[PATCH 0 of %d] %s' % (
168 msg['Subject'] = '[PATCH 0 of %d] %s' % (
168 len(patches),
169 len(patches),
169 opts['subject'] or
170 opts['subject'] or
170 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
171 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
171 to = opts['to'] or ui.config('patchbomb', 'to') or prompt('To')
172 to = opts['to'] or ui.config('patchbomb', 'to') or prompt('To')
172 to = [t.strip() for t in to.split(',')]
173 to = [t.strip() for t in to.split(',')]
173 cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
174 cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
174 prompt('Cc', default = ''))
175 prompt('Cc', default = ''))
175 cc = (cc and [c.strip() for c in cc.split(',')]) or []
176 cc = (cc and [c.strip() for c in cc.split(',')]) or []
176
177
177 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
178 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
178
179
179 body = []
180 body = []
180
181
181 while True:
182 while True:
182 try: l = raw_input()
183 try: l = raw_input()
183 except EOFError: break
184 except EOFError: break
184 if l == '.': break
185 if l == '.': break
185 body.append(l)
186 body.append(l)
186
187
187 msg.attach(MIMEText('\n'.join(body) + '\n'))
188 msg.attach(MIMEText('\n'.join(body) + '\n'))
188
189
189 ui.write('\n')
190 ui.write('\n')
190
191
191 d = cdiffstat('Final summary:\n', jumbo)
192 d = cdiffstat('Final summary:\n', jumbo)
192 if d: msg.attach(MIMEText(d))
193 if d: msg.attach(MIMEText(d))
193
194
194 msgs.insert(0, msg)
195 msgs.insert(0, msg)
195
196
196 if not opts['test']:
197 if not opts['test']:
197 s = smtplib.SMTP()
198 s = smtplib.SMTP()
198 s.connect(host = ui.config('smtp', 'host', 'mail'),
199 s.connect(host = ui.config('smtp', 'host', 'mail'),
199 port = int(ui.config('smtp', 'port', 25)))
200 port = int(ui.config('smtp', 'port', 25)))
200
201
201 parent = None
202 parent = None
202 tz = time.strftime('%z')
203 tz = time.strftime('%z')
203 for m in msgs:
204 for m in msgs:
204 try:
205 try:
205 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
206 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
206 except TypeError:
207 except TypeError:
207 m['Message-Id'] = genmsgid('patchbomb')
208 m['Message-Id'] = genmsgid('patchbomb')
208 if parent:
209 if parent:
209 m['In-Reply-To'] = parent
210 m['In-Reply-To'] = parent
210 else:
211 else:
211 parent = m['Message-Id']
212 parent = m['Message-Id']
212 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
213 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
213 start_time += 1
214 start_time += 1
214 m['From'] = sender
215 m['From'] = sender
215 m['To'] = ', '.join(to)
216 m['To'] = ', '.join(to)
216 if cc: m['Cc'] = ', '.join(cc)
217 if cc: m['Cc'] = ', '.join(cc)
217 ui.status('Sending ', m['Subject'], ' ...\n')
218 ui.status('Sending ', m['Subject'], ' ...\n')
218 if opts['test']:
219 if opts['test']:
219 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
220 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
220 fp.write(m.as_string(0))
221 fp.write(m.as_string(0))
221 fp.write('\n')
222 fp.write('\n')
222 fp.close()
223 fp.close()
223 else:
224 else:
224 s.sendmail(sender, to + cc, m.as_string(0))
225 s.sendmail(sender, to + cc, m.as_string(0))
225 if not opts['test']:
226 if not opts['test']:
226 s.close()
227 s.close()
227
228
228 if __name__ == '__main__':
229 if __name__ == '__main__':
229 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
230 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
230 ('d', 'diffstat', None, 'add diffstat output to messages'),
231 ('d', 'diffstat', None, 'add diffstat output to messages'),
231 ('f', 'from', '', 'email address of sender'),
232 ('f', 'from', '', 'email address of sender'),
232 ('n', 'test', None, 'print messages that would be sent'),
233 ('n', 'test', None, 'print messages that would be sent'),
233 ('s', 'subject', '', 'subject of introductory message'),
234 ('s', 'subject', '', 'subject of introductory message'),
234 ('t', 'to', [], 'email addresses of recipients')]
235 ('t', 'to', [], 'email addresses of recipients')]
235 options = {}
236 options = {}
236 try:
237 try:
237 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
238 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
238 options)
239 options)
239 except fancyopts.getopt.GetoptError, inst:
240 except fancyopts.getopt.GetoptError, inst:
240 u = ui.ui()
241 u = ui.ui()
241 u.warn('error: %s' % inst)
242 u.warn('error: %s' % inst)
242 sys.exit(1)
243 sys.exit(1)
243
244
244 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
245 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
245 not options["noninteractive"])
246 not options["noninteractive"])
246 repo = hg.repository(ui = u)
247 repo = hg.repository(ui = u)
247
248
248 patchbomb(u, repo, *args, **options)
249 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now