##// END OF EJS Templates
patchbomb: pass --git argument to diffstat
Idan Kamara -
r14397:0368ad79 default
parent child Browse files
Show More
@@ -1,551 +1,551
1 1 # patchbomb.py - sending Mercurial changesets as patch emails
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to send changesets as (a series of) patch emails
9 9
10 10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 11 describes the series as a whole.
12 12
13 13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 14 first line of the changeset description as the subject text. The
15 15 message contains two or three body parts:
16 16
17 17 - The changeset description.
18 18 - [Optional] The result of running diffstat on the patch.
19 19 - The patch itself, as generated by :hg:`export`.
20 20
21 21 Each message refers to the first in the series using the In-Reply-To
22 22 and References headers, so they will show up as a sequence in threaded
23 23 mail and news readers, and in mail archives.
24 24
25 25 To configure other defaults, add a section like this to your
26 26 configuration file::
27 27
28 28 [email]
29 29 from = My Name <my@email>
30 30 to = recipient1, recipient2, ...
31 31 cc = cc1, cc2, ...
32 32 bcc = bcc1, bcc2, ...
33 33 reply-to = address1, address2, ...
34 34
35 35 Use ``[patchbomb]`` as configuration section name if you need to
36 36 override global ``[email]`` address settings.
37 37
38 38 Then you can use the :hg:`email` command to mail a series of
39 39 changesets as a patchbomb.
40 40
41 41 You can also either configure the method option in the email section
42 42 to be a sendmail compatible mailer or fill out the [smtp] section so
43 43 that the patchbomb extension can automatically send patchbombs
44 44 directly from the commandline. See the [email] and [smtp] sections in
45 45 hgrc(5) for details.
46 46 '''
47 47
48 48 import os, errno, socket, tempfile, cStringIO, time
49 49 import email.MIMEMultipart, email.MIMEBase
50 50 import email.Utils, email.Encoders, email.Generator
51 51 from mercurial import cmdutil, commands, hg, mail, patch, util, discovery
52 52 from mercurial import scmutil
53 53 from mercurial.i18n import _
54 54 from mercurial.node import bin
55 55
56 56 cmdtable = {}
57 57 command = cmdutil.command(cmdtable)
58 58
59 59 def prompt(ui, prompt, default=None, rest=':'):
60 60 if not ui.interactive() and default is None:
61 61 raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
62 62 if default:
63 63 prompt += ' [%s]' % default
64 64 prompt += rest
65 65 while True:
66 66 r = ui.prompt(prompt, default=default)
67 67 if r:
68 68 return r
69 69 if default is not None:
70 70 return default
71 71 ui.warn(_('Please enter a valid value.\n'))
72 72
73 73 def introneeded(opts, number):
74 74 '''is an introductory message required?'''
75 75 return number > 1 or opts.get('intro') or opts.get('desc')
76 76
77 77 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
78 78 patchname=None):
79 79
80 80 desc = []
81 81 node = None
82 82 body = ''
83 83
84 84 for line in patchlines:
85 85 if line.startswith('#'):
86 86 if line.startswith('# Node ID'):
87 87 node = line.split()[-1]
88 88 continue
89 89 if line.startswith('diff -r') or line.startswith('diff --git'):
90 90 break
91 91 desc.append(line)
92 92
93 93 if not patchname and not node:
94 94 raise ValueError
95 95
96 96 if opts.get('attach'):
97 97 body = ('\n'.join(desc[1:]).strip() or
98 98 'Patch subject is complete summary.')
99 99 body += '\n\n\n'
100 100
101 101 if opts.get('plain'):
102 102 while patchlines and patchlines[0].startswith('# '):
103 103 patchlines.pop(0)
104 104 if patchlines:
105 105 patchlines.pop(0)
106 106 while patchlines and not patchlines[0].strip():
107 107 patchlines.pop(0)
108 108
109 ds = patch.diffstat(patchlines)
109 ds = patch.diffstat(patchlines, git=opts.get('git'))
110 110 if opts.get('diffstat'):
111 111 body += ds + '\n\n'
112 112
113 113 if opts.get('attach') or opts.get('inline'):
114 114 msg = email.MIMEMultipart.MIMEMultipart()
115 115 if body:
116 116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
117 117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
118 118 binnode = bin(node)
119 119 # if node is mq patch, it will have the patch file's name as a tag
120 120 if not patchname:
121 121 patchtags = [t for t in repo.nodetags(binnode)
122 122 if t.endswith('.patch') or t.endswith('.diff')]
123 123 if patchtags:
124 124 patchname = patchtags[0]
125 125 elif total > 1:
126 126 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
127 127 binnode, seqno=idx, total=total)
128 128 else:
129 129 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
130 130 disposition = 'inline'
131 131 if opts.get('attach'):
132 132 disposition = 'attachment'
133 133 p['Content-Disposition'] = disposition + '; filename=' + patchname
134 134 msg.attach(p)
135 135 else:
136 136 body += '\n'.join(patchlines)
137 137 msg = mail.mimetextpatch(body, display=opts.get('test'))
138 138
139 139 flag = ' '.join(opts.get('flag'))
140 140 if flag:
141 141 flag = ' ' + flag
142 142
143 143 subj = desc[0].strip().rstrip('. ')
144 144 if not introneeded(opts, total):
145 145 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
146 146 else:
147 147 tlen = len(str(total))
148 148 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
149 149 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
150 150 msg['X-Mercurial-Node'] = node
151 151 return msg, subj, ds
152 152
153 153 emailopts = [
154 154 ('a', 'attach', None, _('send patches as attachments')),
155 155 ('i', 'inline', None, _('send patches as inline attachments')),
156 156 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
157 157 ('c', 'cc', [], _('email addresses of copy recipients')),
158 158 ('', 'confirm', None, _('ask for confirmation before sending')),
159 159 ('d', 'diffstat', None, _('add diffstat output to messages')),
160 160 ('', 'date', '', _('use the given date as the sending date')),
161 161 ('', 'desc', '', _('use the given file as the series description')),
162 162 ('f', 'from', '', _('email address of sender')),
163 163 ('n', 'test', None, _('print messages that would be sent')),
164 164 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
165 165 ('', 'reply-to', [], _('email addresses replies should be sent to')),
166 166 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
167 167 ('', 'in-reply-to', '', _('message identifier to reply to')),
168 168 ('', 'flag', [], _('flags to add in subject prefixes')),
169 169 ('t', 'to', [], _('email addresses of recipients'))]
170 170
171 171 @command('email',
172 172 [('g', 'git', None, _('use git extended diff format')),
173 173 ('', 'plain', None, _('omit hg patch header')),
174 174 ('o', 'outgoing', None,
175 175 _('send changes not found in the target repository')),
176 176 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
177 177 ('', 'bundlename', 'bundle',
178 178 _('name of the bundle attachment file'), _('NAME')),
179 179 ('r', 'rev', [], _('a revision to send'), _('REV')),
180 180 ('', 'force', None, _('run even when remote repository is unrelated '
181 181 '(with -b/--bundle)')),
182 182 ('', 'base', [], _('a base changeset to specify instead of a destination '
183 183 '(with -b/--bundle)'), _('REV')),
184 184 ('', 'intro', None, _('send an introduction email for a single patch')),
185 185 ] + emailopts + commands.remoteopts,
186 186 _('hg email [OPTION]... [DEST]...'))
187 187 def patchbomb(ui, repo, *revs, **opts):
188 188 '''send changesets by email
189 189
190 190 By default, diffs are sent in the format generated by
191 191 :hg:`export`, one per message. The series starts with a "[PATCH 0
192 192 of N]" introduction, which describes the series as a whole.
193 193
194 194 Each patch email has a Subject line of "[PATCH M of N] ...", using
195 195 the first line of the changeset description as the subject text.
196 196 The message contains two or three parts. First, the changeset
197 197 description.
198 198
199 199 With the -d/--diffstat option, if the diffstat program is
200 200 installed, the result of running diffstat on the patch is inserted.
201 201
202 202 Finally, the patch itself, as generated by :hg:`export`.
203 203
204 204 With the -d/--diffstat or -c/--confirm options, you will be presented
205 205 with a final summary of all messages and asked for confirmation before
206 206 the messages are sent.
207 207
208 208 By default the patch is included as text in the email body for
209 209 easy reviewing. Using the -a/--attach option will instead create
210 210 an attachment for the patch. With -i/--inline an inline attachment
211 211 will be created.
212 212
213 213 With -o/--outgoing, emails will be generated for patches not found
214 214 in the destination repository (or only those which are ancestors
215 215 of the specified revisions if any are provided)
216 216
217 217 With -b/--bundle, changesets are selected as for --outgoing, but a
218 218 single email containing a binary Mercurial bundle as an attachment
219 219 will be sent.
220 220
221 221 With -m/--mbox, instead of previewing each patchbomb message in a
222 222 pager or sending the messages directly, it will create a UNIX
223 223 mailbox file with the patch emails. This mailbox file can be
224 224 previewed with any mail user agent which supports UNIX mbox
225 225 files.
226 226
227 227 With -n/--test, all steps will run, but mail will not be sent.
228 228 You will be prompted for an email recipient address, a subject and
229 229 an introductory message describing the patches of your patchbomb.
230 230 Then when all is done, patchbomb messages are displayed. If the
231 231 PAGER environment variable is set, your pager will be fired up once
232 232 for each patchbomb message, so you can verify everything is alright.
233 233
234 234 In case email sending fails, you will find a backup of your series
235 235 introductory message in ``.hg/last-email.txt``.
236 236
237 237 Examples::
238 238
239 239 hg email -r 3000 # send patch 3000 only
240 240 hg email -r 3000 -r 3001 # send patches 3000 and 3001
241 241 hg email -r 3000:3005 # send patches 3000 through 3005
242 242 hg email 3000 # send patch 3000 (deprecated)
243 243
244 244 hg email -o # send all patches not in default
245 245 hg email -o DEST # send all patches not in DEST
246 246 hg email -o -r 3000 # send all ancestors of 3000 not in default
247 247 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
248 248
249 249 hg email -b # send bundle of all patches not in default
250 250 hg email -b DEST # send bundle of all patches not in DEST
251 251 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
252 252 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
253 253
254 254 hg email -o -m mbox && # generate an mbox file...
255 255 mutt -R -f mbox # ... and view it with mutt
256 256 hg email -o -m mbox && # generate an mbox file ...
257 257 formail -s sendmail \\ # ... and use formail to send from the mbox
258 258 -bm -t < mbox # ... using sendmail
259 259
260 260 Before using this command, you will need to enable email in your
261 261 hgrc. See the [email] section in hgrc(5) for details.
262 262 '''
263 263
264 264 _charsets = mail._charsets(ui)
265 265
266 266 bundle = opts.get('bundle')
267 267 date = opts.get('date')
268 268 mbox = opts.get('mbox')
269 269 outgoing = opts.get('outgoing')
270 270 rev = opts.get('rev')
271 271 # internal option used by pbranches
272 272 patches = opts.get('patches')
273 273
274 274 def getoutgoing(dest, revs):
275 275 '''Return the revisions present locally but not in dest'''
276 276 dest = ui.expandpath(dest or 'default-push', dest or 'default')
277 277 dest, branches = hg.parseurl(dest)
278 278 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
279 279 other = hg.repository(hg.remoteui(repo, opts), dest)
280 280 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
281 281 common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
282 282 nodes = revs and map(repo.lookup, revs) or revs
283 283 o = repo.changelog.findmissing(common, heads=nodes)
284 284 if not o:
285 285 ui.status(_("no changes found\n"))
286 286 return []
287 287 return [str(repo.changelog.rev(r)) for r in o]
288 288
289 289 def getpatches(revs):
290 290 for r in scmutil.revrange(repo, revs):
291 291 output = cStringIO.StringIO()
292 292 cmdutil.export(repo, [r], fp=output,
293 293 opts=patch.diffopts(ui, opts))
294 294 yield output.getvalue().split('\n')
295 295
296 296 def getbundle(dest):
297 297 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
298 298 tmpfn = os.path.join(tmpdir, 'bundle')
299 299 try:
300 300 commands.bundle(ui, repo, tmpfn, dest, **opts)
301 301 fp = open(tmpfn, 'rb')
302 302 data = fp.read()
303 303 fp.close()
304 304 return data
305 305 finally:
306 306 try:
307 307 os.unlink(tmpfn)
308 308 except:
309 309 pass
310 310 os.rmdir(tmpdir)
311 311
312 312 if not (opts.get('test') or mbox):
313 313 # really sending
314 314 mail.validateconfig(ui)
315 315
316 316 if not (revs or rev or outgoing or bundle or patches):
317 317 raise util.Abort(_('specify at least one changeset with -r or -o'))
318 318
319 319 if outgoing and bundle:
320 320 raise util.Abort(_("--outgoing mode always on with --bundle;"
321 321 " do not re-specify --outgoing"))
322 322
323 323 if outgoing or bundle:
324 324 if len(revs) > 1:
325 325 raise util.Abort(_("too many destinations"))
326 326 dest = revs and revs[0] or None
327 327 revs = []
328 328
329 329 if rev:
330 330 if revs:
331 331 raise util.Abort(_('use only one form to specify the revision'))
332 332 revs = rev
333 333
334 334 if outgoing:
335 335 revs = getoutgoing(dest, rev)
336 336 if bundle:
337 337 opts['revs'] = revs
338 338
339 339 # start
340 340 if date:
341 341 start_time = util.parsedate(date)
342 342 else:
343 343 start_time = util.makedate()
344 344
345 345 def genmsgid(id):
346 346 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
347 347
348 348 def getdescription(body, sender):
349 349 if opts.get('desc'):
350 350 body = open(opts.get('desc')).read()
351 351 else:
352 352 ui.write(_('\nWrite the introductory message for the '
353 353 'patch series.\n\n'))
354 354 body = ui.edit(body, sender)
355 355 # Save serie description in case sendmail fails
356 356 msgfile = repo.opener('last-email.txt', 'wb')
357 357 msgfile.write(body)
358 358 msgfile.close()
359 359 return body
360 360
361 361 def getpatchmsgs(patches, patchnames=None):
362 362 jumbo = []
363 363 msgs = []
364 364
365 365 ui.write(_('This patch series consists of %d patches.\n\n')
366 366 % len(patches))
367 367
368 368 name = None
369 369 for i, p in enumerate(patches):
370 370 jumbo.extend(p)
371 371 if patchnames:
372 372 name = patchnames[i]
373 373 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
374 374 len(patches), name)
375 375 msgs.append(msg)
376 376
377 377 if introneeded(opts, len(patches)):
378 378 tlen = len(str(len(patches)))
379 379
380 380 flag = ' '.join(opts.get('flag'))
381 381 if flag:
382 382 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
383 383 else:
384 384 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
385 385 subj += ' ' + (opts.get('subject') or
386 386 prompt(ui, 'Subject: ', rest=subj))
387 387
388 388 body = ''
389 389 ds = patch.diffstat(jumbo)
390 390 if ds and opts.get('diffstat'):
391 391 body = '\n' + ds
392 392
393 393 body = getdescription(body, sender)
394 394 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
395 395 msg['Subject'] = mail.headencode(ui, subj, _charsets,
396 396 opts.get('test'))
397 397
398 398 msgs.insert(0, (msg, subj, ds))
399 399 return msgs
400 400
401 401 def getbundlemsgs(bundle):
402 402 subj = (opts.get('subject')
403 403 or prompt(ui, 'Subject:', 'A bundle for your repository'))
404 404
405 405 body = getdescription('', sender)
406 406 msg = email.MIMEMultipart.MIMEMultipart()
407 407 if body:
408 408 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
409 409 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
410 410 datapart.set_payload(bundle)
411 411 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
412 412 datapart.add_header('Content-Disposition', 'attachment',
413 413 filename=bundlename)
414 414 email.Encoders.encode_base64(datapart)
415 415 msg.attach(datapart)
416 416 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
417 417 return [(msg, subj, None)]
418 418
419 419 sender = (opts.get('from') or ui.config('email', 'from') or
420 420 ui.config('patchbomb', 'from') or
421 421 prompt(ui, 'From', ui.username()))
422 422
423 423 if patches:
424 424 msgs = getpatchmsgs(patches, opts.get('patchnames'))
425 425 elif bundle:
426 426 msgs = getbundlemsgs(getbundle(dest))
427 427 else:
428 428 msgs = getpatchmsgs(list(getpatches(revs)))
429 429
430 430 showaddrs = []
431 431
432 432 def getaddrs(opt, prpt=None, default=None):
433 433 addrs = opts.get(opt.replace('-', '_'))
434 434 if opt != 'reply-to':
435 435 showaddr = '%s:' % opt.capitalize()
436 436 else:
437 437 showaddr = 'Reply-To:'
438 438
439 439 if addrs:
440 440 showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
441 441 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
442 442
443 443 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
444 444 if not addrs and prpt:
445 445 addrs = prompt(ui, prpt, default)
446 446
447 447 if addrs:
448 448 showaddrs.append('%s %s' % (showaddr, addrs))
449 449 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
450 450
451 451 to = getaddrs('to', 'To')
452 452 cc = getaddrs('cc', 'Cc', '')
453 453 bcc = getaddrs('bcc')
454 454 replyto = getaddrs('reply-to')
455 455
456 456 if opts.get('diffstat') or opts.get('confirm'):
457 457 ui.write(_('\nFinal summary:\n\n'))
458 458 ui.write('From: %s\n' % sender)
459 459 for addr in showaddrs:
460 460 ui.write('%s\n' % addr)
461 461 for m, subj, ds in msgs:
462 462 ui.write('Subject: %s\n' % subj)
463 463 if ds:
464 464 ui.write(ds)
465 465 ui.write('\n')
466 466 if ui.promptchoice(_('are you sure you want to send (yn)?'),
467 467 (_('&Yes'), _('&No'))):
468 468 raise util.Abort(_('patchbomb canceled'))
469 469
470 470 ui.write('\n')
471 471
472 472 parent = opts.get('in_reply_to') or None
473 473 # angle brackets may be omitted, they're not semantically part of the msg-id
474 474 if parent is not None:
475 475 if not parent.startswith('<'):
476 476 parent = '<' + parent
477 477 if not parent.endswith('>'):
478 478 parent += '>'
479 479
480 480 first = True
481 481
482 482 sender_addr = email.Utils.parseaddr(sender)[1]
483 483 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
484 484 sendmail = None
485 485 for i, (m, subj, ds) in enumerate(msgs):
486 486 try:
487 487 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
488 488 except TypeError:
489 489 m['Message-Id'] = genmsgid('patchbomb')
490 490 if parent:
491 491 m['In-Reply-To'] = parent
492 492 m['References'] = parent
493 493 if first:
494 494 parent = m['Message-Id']
495 495 first = False
496 496
497 497 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
498 498 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
499 499
500 500 start_time = (start_time[0] + 1, start_time[1])
501 501 m['From'] = sender
502 502 m['To'] = ', '.join(to)
503 503 if cc:
504 504 m['Cc'] = ', '.join(cc)
505 505 if bcc:
506 506 m['Bcc'] = ', '.join(bcc)
507 507 if replyto:
508 508 m['Reply-To'] = ', '.join(replyto)
509 509 if opts.get('test'):
510 510 ui.status(_('Displaying '), subj, ' ...\n')
511 511 ui.flush()
512 512 if 'PAGER' in os.environ and not ui.plain():
513 513 fp = util.popen(os.environ['PAGER'], 'w')
514 514 else:
515 515 fp = ui
516 516 generator = email.Generator.Generator(fp, mangle_from_=False)
517 517 try:
518 518 generator.flatten(m, 0)
519 519 fp.write('\n')
520 520 except IOError, inst:
521 521 if inst.errno != errno.EPIPE:
522 522 raise
523 523 if fp is not ui:
524 524 fp.close()
525 525 elif mbox:
526 526 ui.status(_('Writing '), subj, ' ...\n')
527 527 ui.progress(_('writing'), i, item=subj, total=len(msgs))
528 528 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
529 529 generator = email.Generator.Generator(fp, mangle_from_=True)
530 530 # Should be time.asctime(), but Windows prints 2-characters day
531 531 # of month instead of one. Make them print the same thing.
532 532 date = time.strftime('%a %b %d %H:%M:%S %Y',
533 533 time.localtime(start_time[0]))
534 534 fp.write('From %s %s\n' % (sender_addr, date))
535 535 generator.flatten(m, 0)
536 536 fp.write('\n\n')
537 537 fp.close()
538 538 else:
539 539 if not sendmail:
540 540 sendmail = mail.connect(ui)
541 541 ui.status(_('Sending '), subj, ' ...\n')
542 542 ui.progress(_('sending'), i, item=subj, total=len(msgs))
543 543 # Exim does not remove the Bcc field
544 544 del m['Bcc']
545 545 fp = cStringIO.StringIO()
546 546 generator = email.Generator.Generator(fp, mangle_from_=False)
547 547 generator.flatten(m, 0)
548 548 sendmail(sender, to + bcc + cc, fp.getvalue())
549 549
550 550 ui.progress(_('writing'), None)
551 551 ui.progress(_('sending'), None)
General Comments 0
You need to be logged in to leave comments. Login now