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