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