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