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