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