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