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