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