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