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