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