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