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