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