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