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