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