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