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