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