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