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