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