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