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