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