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