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