##// END OF EJS Templates
patchbomb: use mail.Generator alias for py2/py3 compat
Denis Laxalde -
r43427:3460eee5 default
parent child Browse files
Show More
@@ -1,1001 +1,995 b''
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 By default, :hg:`email` will prompt for a ``To`` or ``CC`` header if
48 48 you do not supply one via configuration or the command line. You can
49 49 override this to never prompt by configuring an empty value::
50 50
51 51 [email]
52 52 cc =
53 53
54 54 You can control the default inclusion of an introduction message with the
55 55 ``patchbomb.intro`` configuration option. The configuration is always
56 56 overwritten by command line flags like --intro and --desc::
57 57
58 58 [patchbomb]
59 59 intro=auto # include introduction message if more than 1 patch (default)
60 60 intro=never # never include an introduction message
61 61 intro=always # always include an introduction message
62 62
63 63 You can specify a template for flags to be added in subject prefixes. Flags
64 64 specified by --flag option are exported as ``{flags}`` keyword::
65 65
66 66 [patchbomb]
67 67 flagtemplate = "{separate(' ',
68 68 ifeq(branch, 'default', '', branch|upper),
69 69 flags)}"
70 70
71 71 You can set patchbomb to always ask for confirmation by setting
72 72 ``patchbomb.confirm`` to true.
73 73 '''
74 74 from __future__ import absolute_import
75 75
76 76 import email.encoders as emailencoders
77 import email.generator as emailgen
78 77 import email.mime.base as emimebase
79 78 import email.mime.multipart as emimemultipart
80 79 import email.utils as eutil
81 80 import errno
82 81 import os
83 82 import socket
84 83
85 84 from mercurial.i18n import _
86 85 from mercurial.pycompat import open
87 86 from mercurial import (
88 87 cmdutil,
89 88 commands,
90 89 encoding,
91 90 error,
92 91 formatter,
93 92 hg,
94 93 mail,
95 94 node as nodemod,
96 95 patch,
97 96 pycompat,
98 97 registrar,
99 98 scmutil,
100 99 templater,
101 100 util,
102 101 )
103 102 from mercurial.utils import dateutil
104 103
105 104 stringio = util.stringio
106 105
107 106 cmdtable = {}
108 107 command = registrar.command(cmdtable)
109 108
110 109 configtable = {}
111 110 configitem = registrar.configitem(configtable)
112 111
113 112 configitem(
114 113 b'patchbomb', b'bundletype', default=None,
115 114 )
116 115 configitem(
117 116 b'patchbomb', b'bcc', default=None,
118 117 )
119 118 configitem(
120 119 b'patchbomb', b'cc', default=None,
121 120 )
122 121 configitem(
123 122 b'patchbomb', b'confirm', default=False,
124 123 )
125 124 configitem(
126 125 b'patchbomb', b'flagtemplate', default=None,
127 126 )
128 127 configitem(
129 128 b'patchbomb', b'from', default=None,
130 129 )
131 130 configitem(
132 131 b'patchbomb', b'intro', default=b'auto',
133 132 )
134 133 configitem(
135 134 b'patchbomb', b'publicurl', default=None,
136 135 )
137 136 configitem(
138 137 b'patchbomb', b'reply-to', default=None,
139 138 )
140 139 configitem(
141 140 b'patchbomb', b'to', default=None,
142 141 )
143 142
144 if pycompat.ispy3:
145 _bytesgenerator = emailgen.BytesGenerator
146 else:
147 _bytesgenerator = emailgen.Generator
148
149 143 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
150 144 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
151 145 # be specifying the version(s) of Mercurial they are tested with, or
152 146 # leave the attribute unspecified.
153 147 testedwith = b'ships-with-hg-core'
154 148
155 149
156 150 def _addpullheader(seq, ctx):
157 151 """Add a header pointing to a public URL where the changeset is available
158 152 """
159 153 repo = ctx.repo()
160 154 # experimental config: patchbomb.publicurl
161 155 # waiting for some logic that check that the changeset are available on the
162 156 # destination before patchbombing anything.
163 157 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
164 158 if publicurl:
165 159 return b'Available At %s\n# hg pull %s -r %s' % (
166 160 publicurl,
167 161 publicurl,
168 162 ctx,
169 163 )
170 164 return None
171 165
172 166
173 167 def uisetup(ui):
174 168 cmdutil.extraexport.append(b'pullurl')
175 169 cmdutil.extraexportmap[b'pullurl'] = _addpullheader
176 170
177 171
178 172 def reposetup(ui, repo):
179 173 if not repo.local():
180 174 return
181 175 repo._wlockfreeprefix.add(b'last-email.txt')
182 176
183 177
184 178 def prompt(ui, prompt, default=None, rest=b':'):
185 179 if default:
186 180 prompt += b' [%s]' % default
187 181 return ui.prompt(prompt + rest, default)
188 182
189 183
190 184 def introwanted(ui, opts, number):
191 185 '''is an introductory message apparently wanted?'''
192 186 introconfig = ui.config(b'patchbomb', b'intro')
193 187 if opts.get(b'intro') or opts.get(b'desc'):
194 188 intro = True
195 189 elif introconfig == b'always':
196 190 intro = True
197 191 elif introconfig == b'never':
198 192 intro = False
199 193 elif introconfig == b'auto':
200 194 intro = number > 1
201 195 else:
202 196 ui.write_err(
203 197 _(b'warning: invalid patchbomb.intro value "%s"\n') % introconfig
204 198 )
205 199 ui.write_err(_(b'(should be one of always, never, auto)\n'))
206 200 intro = number > 1
207 201 return intro
208 202
209 203
210 204 def _formatflags(ui, repo, rev, flags):
211 205 """build flag string optionally by template"""
212 206 tmpl = ui.config(b'patchbomb', b'flagtemplate')
213 207 if not tmpl:
214 208 return b' '.join(flags)
215 209 out = util.stringio()
216 210 spec = formatter.templatespec(b'', templater.unquotestring(tmpl), None)
217 211 with formatter.templateformatter(ui, out, b'patchbombflag', {}, spec) as fm:
218 212 fm.startitem()
219 213 fm.context(ctx=repo[rev])
220 214 fm.write(b'flags', b'%s', fm.formatlist(flags, name=b'flag'))
221 215 return out.getvalue()
222 216
223 217
224 218 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
225 219 """build prefix to patch subject"""
226 220 flag = _formatflags(ui, repo, rev, flags)
227 221 if flag:
228 222 flag = b' ' + flag
229 223
230 224 if not numbered:
231 225 return b'[PATCH%s]' % flag
232 226 else:
233 227 tlen = len(b"%d" % total)
234 228 return b'[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
235 229
236 230
237 231 def makepatch(
238 232 ui,
239 233 repo,
240 234 rev,
241 235 patchlines,
242 236 opts,
243 237 _charsets,
244 238 idx,
245 239 total,
246 240 numbered,
247 241 patchname=None,
248 242 ):
249 243
250 244 desc = []
251 245 node = None
252 246 body = b''
253 247
254 248 for line in patchlines:
255 249 if line.startswith(b'#'):
256 250 if line.startswith(b'# Node ID'):
257 251 node = line.split()[-1]
258 252 continue
259 253 if line.startswith(b'diff -r') or line.startswith(b'diff --git'):
260 254 break
261 255 desc.append(line)
262 256
263 257 if not patchname and not node:
264 258 raise ValueError
265 259
266 260 if opts.get(b'attach') and not opts.get(b'body'):
267 261 body = (
268 262 b'\n'.join(desc[1:]).strip()
269 263 or b'Patch subject is complete summary.'
270 264 )
271 265 body += b'\n\n\n'
272 266
273 267 if opts.get(b'plain'):
274 268 while patchlines and patchlines[0].startswith(b'# '):
275 269 patchlines.pop(0)
276 270 if patchlines:
277 271 patchlines.pop(0)
278 272 while patchlines and not patchlines[0].strip():
279 273 patchlines.pop(0)
280 274
281 275 ds = patch.diffstat(patchlines)
282 276 if opts.get(b'diffstat'):
283 277 body += ds + b'\n\n'
284 278
285 279 addattachment = opts.get(b'attach') or opts.get(b'inline')
286 280 if not addattachment or opts.get(b'body'):
287 281 body += b'\n'.join(patchlines)
288 282
289 283 if addattachment:
290 284 msg = emimemultipart.MIMEMultipart()
291 285 if body:
292 286 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(b'test')))
293 287 p = mail.mimetextpatch(
294 288 b'\n'.join(patchlines), b'x-patch', opts.get(b'test')
295 289 )
296 290 binnode = nodemod.bin(node)
297 291 # if node is mq patch, it will have the patch file's name as a tag
298 292 if not patchname:
299 293 patchtags = [
300 294 t
301 295 for t in repo.nodetags(binnode)
302 296 if t.endswith(b'.patch') or t.endswith(b'.diff')
303 297 ]
304 298 if patchtags:
305 299 patchname = patchtags[0]
306 300 elif total > 1:
307 301 patchname = cmdutil.makefilename(
308 302 repo[node], b'%b-%n.patch', seqno=idx, total=total
309 303 )
310 304 else:
311 305 patchname = cmdutil.makefilename(repo[node], b'%b.patch')
312 306 disposition = r'inline'
313 307 if opts.get(b'attach'):
314 308 disposition = r'attachment'
315 309 p[r'Content-Disposition'] = (
316 310 disposition + r'; filename=' + encoding.strfromlocal(patchname)
317 311 )
318 312 msg.attach(p)
319 313 else:
320 314 msg = mail.mimetextpatch(body, display=opts.get(b'test'))
321 315
322 316 prefix = _formatprefix(
323 317 ui, repo, rev, opts.get(b'flag'), idx, total, numbered
324 318 )
325 319 subj = desc[0].strip().rstrip(b'. ')
326 320 if not numbered:
327 321 subj = b' '.join([prefix, opts.get(b'subject') or subj])
328 322 else:
329 323 subj = b' '.join([prefix, subj])
330 324 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(b'test'))
331 325 msg[b'X-Mercurial-Node'] = node
332 326 msg[b'X-Mercurial-Series-Index'] = b'%i' % idx
333 327 msg[b'X-Mercurial-Series-Total'] = b'%i' % total
334 328 return msg, subj, ds
335 329
336 330
337 331 def _getpatches(repo, revs, **opts):
338 332 """return a list of patches for a list of revisions
339 333
340 334 Each patch in the list is itself a list of lines.
341 335 """
342 336 ui = repo.ui
343 337 prev = repo[b'.'].rev()
344 338 for r in revs:
345 339 if r == prev and (repo[None].files() or repo[None].deleted()):
346 340 ui.warn(_(b'warning: working directory has uncommitted changes\n'))
347 341 output = stringio()
348 342 cmdutil.exportfile(
349 343 repo, [r], output, opts=patch.difffeatureopts(ui, opts, git=True)
350 344 )
351 345 yield output.getvalue().split(b'\n')
352 346
353 347
354 348 def _getbundle(repo, dest, **opts):
355 349 """return a bundle containing changesets missing in "dest"
356 350
357 351 The `opts` keyword-arguments are the same as the one accepted by the
358 352 `bundle` command.
359 353
360 354 The bundle is a returned as a single in-memory binary blob.
361 355 """
362 356 ui = repo.ui
363 357 tmpdir = pycompat.mkdtemp(prefix=b'hg-email-bundle-')
364 358 tmpfn = os.path.join(tmpdir, b'bundle')
365 359 btype = ui.config(b'patchbomb', b'bundletype')
366 360 if btype:
367 361 opts[r'type'] = btype
368 362 try:
369 363 commands.bundle(ui, repo, tmpfn, dest, **opts)
370 364 return util.readfile(tmpfn)
371 365 finally:
372 366 try:
373 367 os.unlink(tmpfn)
374 368 except OSError:
375 369 pass
376 370 os.rmdir(tmpdir)
377 371
378 372
379 373 def _getdescription(repo, defaultbody, sender, **opts):
380 374 """obtain the body of the introduction message and return it
381 375
382 376 This is also used for the body of email with an attached bundle.
383 377
384 378 The body can be obtained either from the command line option or entered by
385 379 the user through the editor.
386 380 """
387 381 ui = repo.ui
388 382 if opts.get(r'desc'):
389 383 body = open(opts.get(r'desc')).read()
390 384 else:
391 385 ui.write(
392 386 _(b'\nWrite the introductory message for the patch series.\n\n')
393 387 )
394 388 body = ui.edit(
395 389 defaultbody, sender, repopath=repo.path, action=b'patchbombbody'
396 390 )
397 391 # Save series description in case sendmail fails
398 392 msgfile = repo.vfs(b'last-email.txt', b'wb')
399 393 msgfile.write(body)
400 394 msgfile.close()
401 395 return body
402 396
403 397
404 398 def _getbundlemsgs(repo, sender, bundle, **opts):
405 399 """Get the full email for sending a given bundle
406 400
407 401 This function returns a list of "email" tuples (subject, content, None).
408 402 The list is always one message long in that case.
409 403 """
410 404 ui = repo.ui
411 405 _charsets = mail._charsets(ui)
412 406 subj = opts.get(r'subject') or prompt(
413 407 ui, b'Subject:', b'A bundle for your repository'
414 408 )
415 409
416 410 body = _getdescription(repo, b'', sender, **opts)
417 411 msg = emimemultipart.MIMEMultipart()
418 412 if body:
419 413 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
420 414 datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
421 415 datapart.set_payload(bundle)
422 416 bundlename = b'%s.hg' % opts.get(r'bundlename', b'bundle')
423 417 datapart.add_header(
424 418 r'Content-Disposition',
425 419 r'attachment',
426 420 filename=encoding.strfromlocal(bundlename),
427 421 )
428 422 emailencoders.encode_base64(datapart)
429 423 msg.attach(datapart)
430 424 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
431 425 return [(msg, subj, None)]
432 426
433 427
434 428 def _makeintro(repo, sender, revs, patches, **opts):
435 429 """make an introduction email, asking the user for content if needed
436 430
437 431 email is returned as (subject, body, cumulative-diffstat)"""
438 432 ui = repo.ui
439 433 _charsets = mail._charsets(ui)
440 434
441 435 # use the last revision which is likely to be a bookmarked head
442 436 prefix = _formatprefix(
443 437 ui, repo, revs.last(), opts.get(r'flag'), 0, len(patches), numbered=True
444 438 )
445 439 subj = opts.get(r'subject') or prompt(
446 440 ui, b'(optional) Subject: ', rest=prefix, default=b''
447 441 )
448 442 if not subj:
449 443 return None # skip intro if the user doesn't bother
450 444
451 445 subj = prefix + b' ' + subj
452 446
453 447 body = b''
454 448 if opts.get(r'diffstat'):
455 449 # generate a cumulative diffstat of the whole patch series
456 450 diffstat = patch.diffstat(sum(patches, []))
457 451 body = b'\n' + diffstat
458 452 else:
459 453 diffstat = None
460 454
461 455 body = _getdescription(repo, body, sender, **opts)
462 456 msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
463 457 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
464 458 return (msg, subj, diffstat)
465 459
466 460
467 461 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
468 462 """return a list of emails from a list of patches
469 463
470 464 This involves introduction message creation if necessary.
471 465
472 466 This function returns a list of "email" tuples (subject, content, None).
473 467 """
474 468 bytesopts = pycompat.byteskwargs(opts)
475 469 ui = repo.ui
476 470 _charsets = mail._charsets(ui)
477 471 patches = list(_getpatches(repo, revs, **opts))
478 472 msgs = []
479 473
480 474 ui.write(_(b'this patch series consists of %d patches.\n\n') % len(patches))
481 475
482 476 # build the intro message, or skip it if the user declines
483 477 if introwanted(ui, bytesopts, len(patches)):
484 478 msg = _makeintro(repo, sender, revs, patches, **opts)
485 479 if msg:
486 480 msgs.append(msg)
487 481
488 482 # are we going to send more than one message?
489 483 numbered = len(msgs) + len(patches) > 1
490 484
491 485 # now generate the actual patch messages
492 486 name = None
493 487 assert len(revs) == len(patches)
494 488 for i, (r, p) in enumerate(zip(revs, patches)):
495 489 if patchnames:
496 490 name = patchnames[i]
497 491 msg = makepatch(
498 492 ui,
499 493 repo,
500 494 r,
501 495 p,
502 496 bytesopts,
503 497 _charsets,
504 498 i + 1,
505 499 len(patches),
506 500 numbered,
507 501 name,
508 502 )
509 503 msgs.append(msg)
510 504
511 505 return msgs
512 506
513 507
514 508 def _getoutgoing(repo, dest, revs):
515 509 '''Return the revisions present locally but not in dest'''
516 510 ui = repo.ui
517 511 url = ui.expandpath(dest or b'default-push', dest or b'default')
518 512 url = hg.parseurl(url)[0]
519 513 ui.status(_(b'comparing with %s\n') % util.hidepassword(url))
520 514
521 515 revs = [r for r in revs if r >= 0]
522 516 if not revs:
523 517 revs = [repo.changelog.tiprev()]
524 518 revs = repo.revs(b'outgoing(%s) and ::%ld', dest or b'', revs)
525 519 if not revs:
526 520 ui.status(_(b"no changes found\n"))
527 521 return revs
528 522
529 523
530 524 def _msgid(node, timestamp):
531 525 hostname = encoding.strtolocal(socket.getfqdn())
532 526 hostname = encoding.environ.get(b'HGHOSTNAME', hostname)
533 527 return b'<%s.%d@%s>' % (node, timestamp, hostname)
534 528
535 529
536 530 emailopts = [
537 531 (b'', b'body', None, _(b'send patches as inline message text (default)')),
538 532 (b'a', b'attach', None, _(b'send patches as attachments')),
539 533 (b'i', b'inline', None, _(b'send patches as inline attachments')),
540 534 (
541 535 b'',
542 536 b'bcc',
543 537 [],
544 538 _(b'email addresses of blind carbon copy recipients'),
545 539 _(b'EMAIL'),
546 540 ),
547 541 (b'c', b'cc', [], _(b'email addresses of copy recipients'), _(b'EMAIL')),
548 542 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
549 543 (b'd', b'diffstat', None, _(b'add diffstat output to messages')),
550 544 (
551 545 b'',
552 546 b'date',
553 547 b'',
554 548 _(b'use the given date as the sending date'),
555 549 _(b'DATE'),
556 550 ),
557 551 (
558 552 b'',
559 553 b'desc',
560 554 b'',
561 555 _(b'use the given file as the series description'),
562 556 _(b'FILE'),
563 557 ),
564 558 (b'f', b'from', b'', _(b'email address of sender'), _(b'EMAIL')),
565 559 (b'n', b'test', None, _(b'print messages that would be sent')),
566 560 (
567 561 b'm',
568 562 b'mbox',
569 563 b'',
570 564 _(b'write messages to mbox file instead of sending them'),
571 565 _(b'FILE'),
572 566 ),
573 567 (
574 568 b'',
575 569 b'reply-to',
576 570 [],
577 571 _(b'email addresses replies should be sent to'),
578 572 _(b'EMAIL'),
579 573 ),
580 574 (
581 575 b's',
582 576 b'subject',
583 577 b'',
584 578 _(b'subject of first message (intro or single patch)'),
585 579 _(b'TEXT'),
586 580 ),
587 581 (
588 582 b'',
589 583 b'in-reply-to',
590 584 b'',
591 585 _(b'message identifier to reply to'),
592 586 _(b'MSGID'),
593 587 ),
594 588 (b'', b'flag', [], _(b'flags to add in subject prefixes'), _(b'FLAG')),
595 589 (b't', b'to', [], _(b'email addresses of recipients'), _(b'EMAIL')),
596 590 ]
597 591
598 592
599 593 @command(
600 594 b'email',
601 595 [
602 596 (b'g', b'git', None, _(b'use git extended diff format')),
603 597 (b'', b'plain', None, _(b'omit hg patch header')),
604 598 (
605 599 b'o',
606 600 b'outgoing',
607 601 None,
608 602 _(b'send changes not found in the target repository'),
609 603 ),
610 604 (
611 605 b'b',
612 606 b'bundle',
613 607 None,
614 608 _(b'send changes not in target as a binary bundle'),
615 609 ),
616 610 (
617 611 b'B',
618 612 b'bookmark',
619 613 b'',
620 614 _(b'send changes only reachable by given bookmark'),
621 615 _(b'BOOKMARK'),
622 616 ),
623 617 (
624 618 b'',
625 619 b'bundlename',
626 620 b'bundle',
627 621 _(b'name of the bundle attachment file'),
628 622 _(b'NAME'),
629 623 ),
630 624 (b'r', b'rev', [], _(b'a revision to send'), _(b'REV')),
631 625 (
632 626 b'',
633 627 b'force',
634 628 None,
635 629 _(
636 630 b'run even when remote repository is unrelated '
637 631 b'(with -b/--bundle)'
638 632 ),
639 633 ),
640 634 (
641 635 b'',
642 636 b'base',
643 637 [],
644 638 _(
645 639 b'a base changeset to specify instead of a destination '
646 640 b'(with -b/--bundle)'
647 641 ),
648 642 _(b'REV'),
649 643 ),
650 644 (
651 645 b'',
652 646 b'intro',
653 647 None,
654 648 _(b'send an introduction email for a single patch'),
655 649 ),
656 650 ]
657 651 + emailopts
658 652 + cmdutil.remoteopts,
659 653 _(b'hg email [OPTION]... [DEST]...'),
660 654 helpcategory=command.CATEGORY_IMPORT_EXPORT,
661 655 )
662 656 def email(ui, repo, *revs, **opts):
663 657 '''send changesets by email
664 658
665 659 By default, diffs are sent in the format generated by
666 660 :hg:`export`, one per message. The series starts with a "[PATCH 0
667 661 of N]" introduction, which describes the series as a whole.
668 662
669 663 Each patch email has a Subject line of "[PATCH M of N] ...", using
670 664 the first line of the changeset description as the subject text.
671 665 The message contains two or three parts. First, the changeset
672 666 description.
673 667
674 668 With the -d/--diffstat option, if the diffstat program is
675 669 installed, the result of running diffstat on the patch is inserted.
676 670
677 671 Finally, the patch itself, as generated by :hg:`export`.
678 672
679 673 With the -d/--diffstat or --confirm options, you will be presented
680 674 with a final summary of all messages and asked for confirmation before
681 675 the messages are sent.
682 676
683 677 By default the patch is included as text in the email body for
684 678 easy reviewing. Using the -a/--attach option will instead create
685 679 an attachment for the patch. With -i/--inline an inline attachment
686 680 will be created. You can include a patch both as text in the email
687 681 body and as a regular or an inline attachment by combining the
688 682 -a/--attach or -i/--inline with the --body option.
689 683
690 684 With -B/--bookmark changesets reachable by the given bookmark are
691 685 selected.
692 686
693 687 With -o/--outgoing, emails will be generated for patches not found
694 688 in the destination repository (or only those which are ancestors
695 689 of the specified revisions if any are provided)
696 690
697 691 With -b/--bundle, changesets are selected as for --outgoing, but a
698 692 single email containing a binary Mercurial bundle as an attachment
699 693 will be sent. Use the ``patchbomb.bundletype`` config option to
700 694 control the bundle type as with :hg:`bundle --type`.
701 695
702 696 With -m/--mbox, instead of previewing each patchbomb message in a
703 697 pager or sending the messages directly, it will create a UNIX
704 698 mailbox file with the patch emails. This mailbox file can be
705 699 previewed with any mail user agent which supports UNIX mbox
706 700 files.
707 701
708 702 With -n/--test, all steps will run, but mail will not be sent.
709 703 You will be prompted for an email recipient address, a subject and
710 704 an introductory message describing the patches of your patchbomb.
711 705 Then when all is done, patchbomb messages are displayed.
712 706
713 707 In case email sending fails, you will find a backup of your series
714 708 introductory message in ``.hg/last-email.txt``.
715 709
716 710 The default behavior of this command can be customized through
717 711 configuration. (See :hg:`help patchbomb` for details)
718 712
719 713 Examples::
720 714
721 715 hg email -r 3000 # send patch 3000 only
722 716 hg email -r 3000 -r 3001 # send patches 3000 and 3001
723 717 hg email -r 3000:3005 # send patches 3000 through 3005
724 718 hg email 3000 # send patch 3000 (deprecated)
725 719
726 720 hg email -o # send all patches not in default
727 721 hg email -o DEST # send all patches not in DEST
728 722 hg email -o -r 3000 # send all ancestors of 3000 not in default
729 723 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
730 724
731 725 hg email -B feature # send all ancestors of feature bookmark
732 726
733 727 hg email -b # send bundle of all patches not in default
734 728 hg email -b DEST # send bundle of all patches not in DEST
735 729 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
736 730 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
737 731
738 732 hg email -o -m mbox && # generate an mbox file...
739 733 mutt -R -f mbox # ... and view it with mutt
740 734 hg email -o -m mbox && # generate an mbox file ...
741 735 formail -s sendmail \\ # ... and use formail to send from the mbox
742 736 -bm -t < mbox # ... using sendmail
743 737
744 738 Before using this command, you will need to enable email in your
745 739 hgrc. See the [email] section in hgrc(5) for details.
746 740 '''
747 741 opts = pycompat.byteskwargs(opts)
748 742
749 743 _charsets = mail._charsets(ui)
750 744
751 745 bundle = opts.get(b'bundle')
752 746 date = opts.get(b'date')
753 747 mbox = opts.get(b'mbox')
754 748 outgoing = opts.get(b'outgoing')
755 749 rev = opts.get(b'rev')
756 750 bookmark = opts.get(b'bookmark')
757 751
758 752 if not (opts.get(b'test') or mbox):
759 753 # really sending
760 754 mail.validateconfig(ui)
761 755
762 756 if not (revs or rev or outgoing or bundle or bookmark):
763 757 raise error.Abort(
764 758 _(b'specify at least one changeset with -B, -r or -o')
765 759 )
766 760
767 761 if outgoing and bundle:
768 762 raise error.Abort(
769 763 _(
770 764 b"--outgoing mode always on with --bundle;"
771 765 b" do not re-specify --outgoing"
772 766 )
773 767 )
774 768 if rev and bookmark:
775 769 raise error.Abort(_(b"-r and -B are mutually exclusive"))
776 770
777 771 if outgoing or bundle:
778 772 if len(revs) > 1:
779 773 raise error.Abort(_(b"too many destinations"))
780 774 if revs:
781 775 dest = revs[0]
782 776 else:
783 777 dest = None
784 778 revs = []
785 779
786 780 if rev:
787 781 if revs:
788 782 raise error.Abort(_(b'use only one form to specify the revision'))
789 783 revs = rev
790 784 elif bookmark:
791 785 if bookmark not in repo._bookmarks:
792 786 raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
793 787 revs = scmutil.bookmarkrevs(repo, bookmark)
794 788
795 789 revs = scmutil.revrange(repo, revs)
796 790 if outgoing:
797 791 revs = _getoutgoing(repo, dest, revs)
798 792 if bundle:
799 793 opts[b'revs'] = [b"%d" % r for r in revs]
800 794
801 795 # check if revision exist on the public destination
802 796 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
803 797 if publicurl:
804 798 repo.ui.debug(b'checking that revision exist in the public repo\n')
805 799 try:
806 800 publicpeer = hg.peer(repo, {}, publicurl)
807 801 except error.RepoError:
808 802 repo.ui.write_err(
809 803 _(b'unable to access public repo: %s\n') % publicurl
810 804 )
811 805 raise
812 806 if not publicpeer.capable(b'known'):
813 807 repo.ui.debug(b'skipping existence checks: public repo too old\n')
814 808 else:
815 809 out = [repo[r] for r in revs]
816 810 known = publicpeer.known(h.node() for h in out)
817 811 missing = []
818 812 for idx, h in enumerate(out):
819 813 if not known[idx]:
820 814 missing.append(h)
821 815 if missing:
822 816 if len(missing) > 1:
823 817 msg = _(b'public "%s" is missing %s and %i others')
824 818 msg %= (publicurl, missing[0], len(missing) - 1)
825 819 else:
826 820 msg = _(b'public url %s is missing %s')
827 821 msg %= (publicurl, missing[0])
828 822 missingrevs = [ctx.rev() for ctx in missing]
829 823 revhint = b' '.join(
830 824 b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs)
831 825 )
832 826 hint = _(b"use 'hg push %s %s'") % (publicurl, revhint)
833 827 raise error.Abort(msg, hint=hint)
834 828
835 829 # start
836 830 if date:
837 831 start_time = dateutil.parsedate(date)
838 832 else:
839 833 start_time = dateutil.makedate()
840 834
841 835 def genmsgid(id):
842 836 return _msgid(id[:20], int(start_time[0]))
843 837
844 838 # deprecated config: patchbomb.from
845 839 sender = (
846 840 opts.get(b'from')
847 841 or ui.config(b'email', b'from')
848 842 or ui.config(b'patchbomb', b'from')
849 843 or prompt(ui, b'From', ui.username())
850 844 )
851 845
852 846 if bundle:
853 847 stropts = pycompat.strkwargs(opts)
854 848 bundledata = _getbundle(repo, dest, **stropts)
855 849 bundleopts = stropts.copy()
856 850 bundleopts.pop(r'bundle', None) # already processed
857 851 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
858 852 else:
859 853 msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))
860 854
861 855 showaddrs = []
862 856
863 857 def getaddrs(header, ask=False, default=None):
864 858 configkey = header.lower()
865 859 opt = header.replace(b'-', b'_').lower()
866 860 addrs = opts.get(opt)
867 861 if addrs:
868 862 showaddrs.append(b'%s: %s' % (header, b', '.join(addrs)))
869 863 return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test'))
870 864
871 865 # not on the command line: fallback to config and then maybe ask
872 866 addr = ui.config(b'email', configkey) or ui.config(
873 867 b'patchbomb', configkey
874 868 )
875 869 if not addr:
876 870 specified = ui.hasconfig(b'email', configkey) or ui.hasconfig(
877 871 b'patchbomb', configkey
878 872 )
879 873 if not specified and ask:
880 874 addr = prompt(ui, header, default=default)
881 875 if addr:
882 876 showaddrs.append(b'%s: %s' % (header, addr))
883 877 return mail.addrlistencode(ui, [addr], _charsets, opts.get(b'test'))
884 878 elif default:
885 879 return mail.addrlistencode(
886 880 ui, [default], _charsets, opts.get(b'test')
887 881 )
888 882 return []
889 883
890 884 to = getaddrs(b'To', ask=True)
891 885 if not to:
892 886 # we can get here in non-interactive mode
893 887 raise error.Abort(_(b'no recipient addresses provided'))
894 888 cc = getaddrs(b'Cc', ask=True, default=b'')
895 889 bcc = getaddrs(b'Bcc')
896 890 replyto = getaddrs(b'Reply-To')
897 891
898 892 confirm = ui.configbool(b'patchbomb', b'confirm')
899 893 confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm'))
900 894
901 895 if confirm:
902 896 ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary')
903 897 ui.write((b'From: %s\n' % sender), label=b'patchbomb.from')
904 898 for addr in showaddrs:
905 899 ui.write(b'%s\n' % addr, label=b'patchbomb.to')
906 900 for m, subj, ds in msgs:
907 901 ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject')
908 902 if ds:
909 903 ui.write(ds, label=b'patchbomb.diffstats')
910 904 ui.write(b'\n')
911 905 if ui.promptchoice(
912 906 _(b'are you sure you want to send (yn)?$$ &Yes $$ &No')
913 907 ):
914 908 raise error.Abort(_(b'patchbomb canceled'))
915 909
916 910 ui.write(b'\n')
917 911
918 912 parent = opts.get(b'in_reply_to') or None
919 913 # angle brackets may be omitted, they're not semantically part of the msg-id
920 914 if parent is not None:
921 915 if not parent.startswith(b'<'):
922 916 parent = b'<' + parent
923 917 if not parent.endswith(b'>'):
924 918 parent += b'>'
925 919
926 920 sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
927 921 sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test'))
928 922 sendmail = None
929 923 firstpatch = None
930 924 progress = ui.makeprogress(
931 925 _(b'sending'), unit=_(b'emails'), total=len(msgs)
932 926 )
933 927 for i, (m, subj, ds) in enumerate(msgs):
934 928 try:
935 929 m[b'Message-Id'] = genmsgid(m[b'X-Mercurial-Node'])
936 930 if not firstpatch:
937 931 firstpatch = m[b'Message-Id']
938 932 m[b'X-Mercurial-Series-Id'] = firstpatch
939 933 except TypeError:
940 934 m[b'Message-Id'] = genmsgid(b'patchbomb')
941 935 if parent:
942 936 m[b'In-Reply-To'] = parent
943 937 m[b'References'] = parent
944 938 if not parent or b'X-Mercurial-Node' not in m:
945 939 parent = m[b'Message-Id']
946 940
947 941 m[b'User-Agent'] = b'Mercurial-patchbomb/%s' % util.version()
948 942 m[b'Date'] = eutil.formatdate(start_time[0], localtime=True)
949 943
950 944 start_time = (start_time[0] + 1, start_time[1])
951 945 m[b'From'] = sender
952 946 m[b'To'] = b', '.join(to)
953 947 if cc:
954 948 m[b'Cc'] = b', '.join(cc)
955 949 if bcc:
956 950 m[b'Bcc'] = b', '.join(bcc)
957 951 if replyto:
958 952 m[b'Reply-To'] = b', '.join(replyto)
959 953 # Fix up all headers to be native strings.
960 954 # TODO(durin42): this should probably be cleaned up above in the future.
961 955 if pycompat.ispy3:
962 956 for hdr, val in list(m.items()):
963 957 change = False
964 958 if isinstance(hdr, bytes):
965 959 del m[hdr]
966 960 hdr = pycompat.strurl(hdr)
967 961 change = True
968 962 if isinstance(val, bytes):
969 963 val = pycompat.strurl(val)
970 964 if not change:
971 965 # prevent duplicate headers
972 966 del m[hdr]
973 967 change = True
974 968 if change:
975 969 m[hdr] = val
976 970 if opts.get(b'test'):
977 971 ui.status(_(b'displaying '), subj, b' ...\n')
978 972 ui.pager(b'email')
979 generator = _bytesgenerator(ui, mangle_from_=False)
973 generator = mail.Generator(ui, mangle_from_=False)
980 974 try:
981 975 generator.flatten(m, 0)
982 976 ui.write(b'\n')
983 977 except IOError as inst:
984 978 if inst.errno != errno.EPIPE:
985 979 raise
986 980 else:
987 981 if not sendmail:
988 982 sendmail = mail.connect(ui, mbox=mbox)
989 983 ui.status(_(b'sending '), subj, b' ...\n')
990 984 progress.update(i, item=subj)
991 985 if not mbox:
992 986 # Exim does not remove the Bcc field
993 987 del m[b'Bcc']
994 988 fp = stringio()
995 generator = _bytesgenerator(fp, mangle_from_=False)
989 generator = mail.Generator(fp, mangle_from_=False)
996 990 generator.flatten(m, 0)
997 991 alldests = to + bcc + cc
998 992 alldests = [encoding.strfromlocal(d) for d in alldests]
999 993 sendmail(sender_addr, alldests, fp.getvalue())
1000 994
1001 995 progress.complete()
General Comments 0
You need to be logged in to leave comments. Login now