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