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