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