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