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