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