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