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