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