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