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