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