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