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