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