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