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