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