##// END OF EJS Templates
patchbomb: fix cStringIO import (spotted by pychecker)
Christian Ebert -
r6478:a135374e default
parent child Browse files
Show More
@@ -1,470 +1,469 b''
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 # To enable this extension:
27 27 #
28 28 # [extensions]
29 29 # hgext.patchbomb =
30 30 #
31 31 # To configure other defaults, add a section like this to your hgrc
32 32 # file:
33 33 #
34 34 # [email]
35 35 # from = My Name <my@email>
36 36 # to = recipient1, recipient2, ...
37 37 # cc = cc1, cc2, ...
38 38 # bcc = bcc1, bcc2, ...
39 39 #
40 40 # Then you can use the "hg email" command to mail a series of changesets
41 41 # as a patchbomb.
42 42 #
43 43 # To avoid sending patches prematurely, it is a good idea to first run
44 44 # the "email" command with the "-n" option (test only). You will be
45 45 # prompted for an email recipient address, a subject an an introductory
46 46 # message describing the patches of your patchbomb. Then when all is
47 47 # done, patchbomb messages are displayed. If PAGER environment variable
48 48 # is set, your pager will be fired up once for each patchbomb message, so
49 49 # you can verify everything is alright.
50 50 #
51 51 # The "-m" (mbox) option is also very useful. Instead of previewing
52 52 # each patchbomb message in a pager or sending the messages directly,
53 53 # it will create a UNIX mailbox file with the patch emails. This
54 54 # mailbox file can be previewed with any mail user agent which supports
55 55 # UNIX mbox files, i.e. with mutt:
56 56 #
57 57 # % mutt -R -f mbox
58 58 #
59 59 # When you are previewing the patchbomb messages, you can use `formail'
60 60 # (a utility that is commonly installed as part of the procmail package),
61 61 # to send each message out:
62 62 #
63 63 # % formail -s sendmail -bm -t < mbox
64 64 #
65 65 # That should be all. Now your patchbomb is on its way out.
66 66
67 import os, errno, socket, tempfile
67 import os, errno, socket, tempfile, cStringIO
68 68 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
69 69 import email.Utils, email.Encoders, email.Generator
70 import cStringIO.StringIO
71 70 from mercurial import cmdutil, commands, hg, mail, patch, util
72 71 from mercurial.i18n import _
73 72 from mercurial.node import bin
74 73
75 74 def patchbomb(ui, repo, *revs, **opts):
76 75 '''send changesets by email
77 76
78 77 By default, diffs are sent in the format generated by hg export,
79 78 one per message. The series starts with a "[PATCH 0 of N]"
80 79 introduction, which describes the series as a whole.
81 80
82 81 Each patch email has a Subject line of "[PATCH M of N] ...", using
83 82 the first line of the changeset description as the subject text.
84 83 The message contains two or three body parts. First, the rest of
85 84 the changeset description. Next, (optionally) if the diffstat
86 85 program is installed, the result of running diffstat on the patch.
87 86 Finally, the patch itself, as generated by "hg export".
88 87
89 88 With --outgoing, emails will be generated for patches not
90 89 found in the destination repository (or only those which are
91 90 ancestors of the specified revisions if any are provided)
92 91
93 92 With --bundle, changesets are selected as for --outgoing,
94 93 but a single email containing a binary Mercurial bundle as an
95 94 attachment will be sent.
96 95
97 96 Examples:
98 97
99 98 hg email -r 3000 # send patch 3000 only
100 99 hg email -r 3000 -r 3001 # send patches 3000 and 3001
101 100 hg email -r 3000:3005 # send patches 3000 through 3005
102 101 hg email 3000 # send patch 3000 (deprecated)
103 102
104 103 hg email -o # send all patches not in default
105 104 hg email -o DEST # send all patches not in DEST
106 105 hg email -o -r 3000 # send all ancestors of 3000 not in default
107 106 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
108 107
109 108 hg email -b # send bundle of all patches not in default
110 109 hg email -b DEST # send bundle of all patches not in DEST
111 110 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
112 111 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
113 112
114 113 Before using this command, you will need to enable email in your hgrc.
115 114 See the [email] section in hgrc(5) for details.
116 115 '''
117 116
118 117 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
119 118 if not ui.interactive:
120 119 return default
121 120 if default:
122 121 prompt += ' [%s]' % default
123 122 prompt += rest
124 123 while True:
125 124 r = ui.prompt(prompt, default=default)
126 125 if r:
127 126 return r
128 127 if default is not None:
129 128 return default
130 129 if empty_ok:
131 130 return r
132 131 ui.warn(_('Please enter a valid value.\n'))
133 132
134 133 def confirm(s, denial):
135 134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
136 135 raise util.Abort(denial)
137 136
138 137 def cdiffstat(summary, patchlines):
139 138 s = patch.diffstat(patchlines)
140 139 if s:
141 140 if summary:
142 141 ui.write(summary, '\n')
143 142 ui.write(s, '\n')
144 143 confirm(_('Does the diffstat above look okay'),
145 144 _('diffstat rejected'))
146 145 elif s is None:
147 146 ui.warn(_('No diffstat information available.\n'))
148 147 s = ''
149 148 return s
150 149
151 150 def makepatch(patch, idx, total):
152 151 desc = []
153 152 node = None
154 153 body = ''
155 154 for line in patch:
156 155 if line.startswith('#'):
157 156 if line.startswith('# Node ID'):
158 157 node = line.split()[-1]
159 158 continue
160 159 if line.startswith('diff -r') or line.startswith('diff --git'):
161 160 break
162 161 desc.append(line)
163 162 if not node:
164 163 raise ValueError
165 164
166 165 if opts['attach']:
167 166 body = ('\n'.join(desc[1:]).strip() or
168 167 'Patch subject is complete summary.')
169 168 body += '\n\n\n'
170 169
171 170 if opts.get('plain'):
172 171 while patch and patch[0].startswith('# '):
173 172 patch.pop(0)
174 173 if patch:
175 174 patch.pop(0)
176 175 while patch and not patch[0].strip():
177 176 patch.pop(0)
178 177 if opts.get('diffstat'):
179 178 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
180 179 if opts.get('attach') or opts.get('inline'):
181 180 msg = email.MIMEMultipart.MIMEMultipart()
182 181 if body:
183 182 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
184 183 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
185 184 binnode = bin(node)
186 185 # if node is mq patch, it will have patch file name as tag
187 186 patchname = [t for t in repo.nodetags(binnode)
188 187 if t.endswith('.patch') or t.endswith('.diff')]
189 188 if patchname:
190 189 patchname = patchname[0]
191 190 elif total > 1:
192 191 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
193 192 binnode, idx, total)
194 193 else:
195 194 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
196 195 disposition = 'inline'
197 196 if opts['attach']:
198 197 disposition = 'attachment'
199 198 p['Content-Disposition'] = disposition + '; filename=' + patchname
200 199 msg.attach(p)
201 200 else:
202 201 body += '\n'.join(patch)
203 202 msg = email.MIMEText.MIMEText(body)
204 203
205 204 subj = desc[0].strip().rstrip('. ')
206 205 if total == 1:
207 206 subj = '[PATCH] ' + (opts.get('subject') or subj)
208 207 else:
209 208 tlen = len(str(total))
210 209 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
211 210 msg['Subject'] = subj
212 211 msg['X-Mercurial-Node'] = node
213 212 return msg
214 213
215 214 def outgoing(dest, revs):
216 215 '''Return the revisions present locally but not in dest'''
217 216 dest = ui.expandpath(dest or 'default-push', dest or 'default')
218 217 revs = [repo.lookup(rev) for rev in revs]
219 218 other = hg.repository(ui, dest)
220 219 ui.status(_('comparing with %s\n') % dest)
221 220 o = repo.findoutgoing(other)
222 221 if not o:
223 222 ui.status(_("no changes found\n"))
224 223 return []
225 224 o = repo.changelog.nodesbetween(o, revs or None)[0]
226 225 return [str(repo.changelog.rev(r)) for r in o]
227 226
228 227 def getbundle(dest):
229 228 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
230 229 tmpfn = os.path.join(tmpdir, 'bundle')
231 230 try:
232 231 commands.bundle(ui, repo, tmpfn, dest, **opts)
233 232 return open(tmpfn, 'rb').read()
234 233 finally:
235 234 try:
236 235 os.unlink(tmpfn)
237 236 except:
238 237 pass
239 238 os.rmdir(tmpdir)
240 239
241 240 if not (opts.get('test') or opts.get('mbox')):
242 241 # really sending
243 242 mail.validateconfig(ui)
244 243
245 244 if not (revs or opts.get('rev')
246 245 or opts.get('outgoing') or opts.get('bundle')):
247 246 raise util.Abort(_('specify at least one changeset with -r or -o'))
248 247
249 248 cmdutil.setremoteconfig(ui, opts)
250 249 if opts.get('outgoing') and opts.get('bundle'):
251 250 raise util.Abort(_("--outgoing mode always on with --bundle;"
252 251 " do not re-specify --outgoing"))
253 252
254 253 if opts.get('outgoing') or opts.get('bundle'):
255 254 if len(revs) > 1:
256 255 raise util.Abort(_("too many destinations"))
257 256 dest = revs and revs[0] or None
258 257 revs = []
259 258
260 259 if opts.get('rev'):
261 260 if revs:
262 261 raise util.Abort(_('use only one form to specify the revision'))
263 262 revs = opts.get('rev')
264 263
265 264 if opts.get('outgoing'):
266 265 revs = outgoing(dest, opts.get('rev'))
267 266 if opts.get('bundle'):
268 267 opts['revs'] = revs
269 268
270 269 # start
271 270 if opts.get('date'):
272 271 start_time = util.parsedate(opts.get('date'))
273 272 else:
274 273 start_time = util.makedate()
275 274
276 275 def genmsgid(id):
277 276 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
278 277
279 278 def getdescription(body, sender):
280 279 if opts.get('desc'):
281 280 body = open(opts.get('desc')).read()
282 281 else:
283 282 ui.write(_('\nWrite the introductory message for the '
284 283 'patch series.\n\n'))
285 284 body = ui.edit(body, sender)
286 285 return body
287 286
288 287 def getexportmsgs():
289 288 patches = []
290 289
291 290 class exportee:
292 291 def __init__(self, container):
293 292 self.lines = []
294 293 self.container = container
295 294 self.name = 'email'
296 295
297 296 def write(self, data):
298 297 self.lines.append(data)
299 298
300 299 def close(self):
301 300 self.container.append(''.join(self.lines).split('\n'))
302 301 self.lines = []
303 302
304 303 commands.export(ui, repo, *revs, **{'output': exportee(patches),
305 304 'switch_parent': False,
306 305 'text': None,
307 306 'git': opts.get('git')})
308 307
309 308 jumbo = []
310 309 msgs = []
311 310
312 311 ui.write(_('This patch series consists of %d patches.\n\n')
313 312 % len(patches))
314 313
315 314 for p, i in zip(patches, xrange(len(patches))):
316 315 jumbo.extend(p)
317 316 msgs.append(makepatch(p, i + 1, len(patches)))
318 317
319 318 if len(patches) > 1:
320 319 tlen = len(str(len(patches)))
321 320
322 321 subj = '[PATCH %0*d of %d] %s' % (
323 322 tlen, 0, len(patches),
324 323 opts.get('subject') or
325 324 prompt('Subject:',
326 325 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
327 326
328 327 body = ''
329 328 if opts.get('diffstat'):
330 329 d = cdiffstat(_('Final summary:\n'), jumbo)
331 330 if d:
332 331 body = '\n' + d
333 332
334 333 body = getdescription(body, sender)
335 334 msg = email.MIMEText.MIMEText(body)
336 335 msg['Subject'] = subj
337 336
338 337 msgs.insert(0, msg)
339 338 return msgs
340 339
341 340 def getbundlemsgs(bundle):
342 341 subj = (opts.get('subject')
343 342 or prompt('Subject:', default='A bundle for your repository'))
344 343
345 344 body = getdescription('', sender)
346 345 msg = email.MIMEMultipart.MIMEMultipart()
347 346 if body:
348 347 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
349 348 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
350 349 datapart.set_payload(bundle)
351 350 datapart.add_header('Content-Disposition', 'attachment',
352 351 filename='bundle.hg')
353 352 email.Encoders.encode_base64(datapart)
354 353 msg.attach(datapart)
355 354 msg['Subject'] = subj
356 355 return [msg]
357 356
358 357 sender = (opts.get('from') or ui.config('email', 'from') or
359 358 ui.config('patchbomb', 'from') or
360 359 prompt('From', ui.username()))
361 360
362 361 if opts.get('bundle'):
363 362 msgs = getbundlemsgs(getbundle(dest))
364 363 else:
365 364 msgs = getexportmsgs()
366 365
367 366 def getaddrs(opt, prpt, default = None):
368 367 addrs = opts.get(opt) or (ui.config('email', opt) or
369 368 ui.config('patchbomb', opt) or
370 369 prompt(prpt, default = default)).split(',')
371 370 return [a.strip() for a in addrs if a.strip()]
372 371
373 372 to = getaddrs('to', 'To')
374 373 cc = getaddrs('cc', 'Cc', '')
375 374
376 375 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
377 376 ui.config('patchbomb', 'bcc') or '').split(',')
378 377 bcc = [a.strip() for a in bcc if a.strip()]
379 378
380 379 ui.write('\n')
381 380
382 381 parent = None
383 382
384 383 sender_addr = email.Utils.parseaddr(sender)[1]
385 384 sendmail = None
386 385 for m in msgs:
387 386 try:
388 387 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
389 388 except TypeError:
390 389 m['Message-Id'] = genmsgid('patchbomb')
391 390 if parent:
392 391 m['In-Reply-To'] = parent
393 392 else:
394 393 parent = m['Message-Id']
395 394 m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2")
396 395
397 396 start_time = (start_time[0] + 1, start_time[1])
398 397 m['From'] = sender
399 398 m['To'] = ', '.join(to)
400 399 if cc:
401 400 m['Cc'] = ', '.join(cc)
402 401 if bcc:
403 402 m['Bcc'] = ', '.join(bcc)
404 403 if opts.get('test'):
405 404 ui.status('Displaying ', m['Subject'], ' ...\n')
406 405 ui.flush()
407 406 if 'PAGER' in os.environ:
408 407 fp = os.popen(os.environ['PAGER'], 'w')
409 408 else:
410 409 fp = ui
411 410 generator = email.Generator.Generator(fp, mangle_from_=False)
412 411 try:
413 412 generator.flatten(m, 0)
414 413 fp.write('\n')
415 414 except IOError, inst:
416 415 if inst.errno != errno.EPIPE:
417 416 raise
418 417 if fp is not ui:
419 418 fp.close()
420 419 elif opts.get('mbox'):
421 420 ui.status('Writing ', m['Subject'], ' ...\n')
422 421 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
423 422 generator = email.Generator.Generator(fp, mangle_from_=True)
424 423 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
425 424 fp.write('From %s %s\n' % (sender_addr, date))
426 425 generator.flatten(m, 0)
427 426 fp.write('\n\n')
428 427 fp.close()
429 428 else:
430 429 if not sendmail:
431 430 sendmail = mail.connect(ui)
432 431 ui.status('Sending ', m['Subject'], ' ...\n')
433 432 # Exim does not remove the Bcc field
434 433 del m['Bcc']
435 434 fp = cStringIO.StringIO()
436 435 generator = email.Generator.Generator(fp, mangle_from_=False)
437 436 generator.flatten(m, 0)
438 437 sendmail(sender, to + bcc + cc, fp.getvalue())
439 438
440 439 cmdtable = {
441 440 "email":
442 441 (patchbomb,
443 442 [('a', 'attach', None, _('send patches as attachments')),
444 443 ('i', 'inline', None, _('send patches as inline attachments')),
445 444 ('', 'bcc', [], _('email addresses of blind copy recipients')),
446 445 ('c', 'cc', [], _('email addresses of copy recipients')),
447 446 ('d', 'diffstat', None, _('add diffstat output to messages')),
448 447 ('', 'date', '', _('use the given date as the sending date')),
449 448 ('', 'desc', '', _('use the given file as the series description')),
450 449 ('g', 'git', None, _('use git extended diff format')),
451 450 ('f', 'from', '', _('email address of sender')),
452 451 ('', 'plain', None, _('omit hg patch header')),
453 452 ('n', 'test', None, _('print messages that would be sent')),
454 453 ('m', 'mbox', '',
455 454 _('write messages to mbox file instead of sending them')),
456 455 ('o', 'outgoing', None,
457 456 _('send changes not found in the target repository')),
458 457 ('b', 'bundle', None,
459 458 _('send changes not in target as a binary bundle')),
460 459 ('r', 'rev', [], _('a revision to send')),
461 460 ('s', 'subject', '',
462 461 _('subject of first message (intro or single patch)')),
463 462 ('t', 'to', [], _('email addresses of recipients')),
464 463 ('', 'force', None,
465 464 _('run even when remote repository is unrelated (with -b)')),
466 465 ('', 'base', [],
467 466 _('a base changeset to specify instead of a destination (with -b)')),
468 467 ] + commands.remoteopts,
469 468 _('hg email [OPTION]... [DEST]...'))
470 469 }
General Comments 0
You need to be logged in to leave comments. Login now