##// END OF EJS Templates
formatter: pass in template spec to templateformatter as argument...
Yuya Nishihara -
r43369:f1c5358f default
parent child Browse files
Show More
@@ -1,1003 +1,1003 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.generator as emailgen
77 import email.generator as emailgen
78 import email.mime.base as emimebase
78 import email.mime.base as emimebase
79 import email.mime.multipart as emimemultipart
79 import email.mime.multipart as emimemultipart
80 import email.utils as eutil
80 import email.utils as eutil
81 import errno
81 import errno
82 import os
82 import os
83 import socket
83 import socket
84
84
85 from mercurial.i18n import _
85 from mercurial.i18n import _
86 from mercurial.pycompat import open
86 from mercurial.pycompat import open
87 from mercurial import (
87 from mercurial import (
88 cmdutil,
88 cmdutil,
89 commands,
89 commands,
90 encoding,
90 encoding,
91 error,
91 error,
92 formatter,
92 formatter,
93 hg,
93 hg,
94 mail,
94 mail,
95 node as nodemod,
95 node as nodemod,
96 patch,
96 patch,
97 pycompat,
97 pycompat,
98 registrar,
98 registrar,
99 scmutil,
99 scmutil,
100 templater,
100 templater,
101 util,
101 util,
102 )
102 )
103 from mercurial.utils import dateutil
103 from mercurial.utils import dateutil
104
104
105 stringio = util.stringio
105 stringio = util.stringio
106
106
107 cmdtable = {}
107 cmdtable = {}
108 command = registrar.command(cmdtable)
108 command = registrar.command(cmdtable)
109
109
110 configtable = {}
110 configtable = {}
111 configitem = registrar.configitem(configtable)
111 configitem = registrar.configitem(configtable)
112
112
113 configitem(
113 configitem(
114 b'patchbomb', b'bundletype', default=None,
114 b'patchbomb', b'bundletype', default=None,
115 )
115 )
116 configitem(
116 configitem(
117 b'patchbomb', b'bcc', default=None,
117 b'patchbomb', b'bcc', default=None,
118 )
118 )
119 configitem(
119 configitem(
120 b'patchbomb', b'cc', default=None,
120 b'patchbomb', b'cc', default=None,
121 )
121 )
122 configitem(
122 configitem(
123 b'patchbomb', b'confirm', default=False,
123 b'patchbomb', b'confirm', default=False,
124 )
124 )
125 configitem(
125 configitem(
126 b'patchbomb', b'flagtemplate', default=None,
126 b'patchbomb', b'flagtemplate', default=None,
127 )
127 )
128 configitem(
128 configitem(
129 b'patchbomb', b'from', default=None,
129 b'patchbomb', b'from', default=None,
130 )
130 )
131 configitem(
131 configitem(
132 b'patchbomb', b'intro', default=b'auto',
132 b'patchbomb', b'intro', default=b'auto',
133 )
133 )
134 configitem(
134 configitem(
135 b'patchbomb', b'publicurl', default=None,
135 b'patchbomb', b'publicurl', default=None,
136 )
136 )
137 configitem(
137 configitem(
138 b'patchbomb', b'reply-to', default=None,
138 b'patchbomb', b'reply-to', default=None,
139 )
139 )
140 configitem(
140 configitem(
141 b'patchbomb', b'to', default=None,
141 b'patchbomb', b'to', default=None,
142 )
142 )
143
143
144 if pycompat.ispy3:
144 if pycompat.ispy3:
145 _bytesgenerator = emailgen.BytesGenerator
145 _bytesgenerator = emailgen.BytesGenerator
146 else:
146 else:
147 _bytesgenerator = emailgen.Generator
147 _bytesgenerator = emailgen.Generator
148
148
149 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
149 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
150 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
150 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
151 # be specifying the version(s) of Mercurial they are tested with, or
151 # be specifying the version(s) of Mercurial they are tested with, or
152 # leave the attribute unspecified.
152 # leave the attribute unspecified.
153 testedwith = b'ships-with-hg-core'
153 testedwith = b'ships-with-hg-core'
154
154
155
155
156 def _addpullheader(seq, ctx):
156 def _addpullheader(seq, ctx):
157 """Add a header pointing to a public URL where the changeset is available
157 """Add a header pointing to a public URL where the changeset is available
158 """
158 """
159 repo = ctx.repo()
159 repo = ctx.repo()
160 # experimental config: patchbomb.publicurl
160 # experimental config: patchbomb.publicurl
161 # waiting for some logic that check that the changeset are available on the
161 # waiting for some logic that check that the changeset are available on the
162 # destination before patchbombing anything.
162 # destination before patchbombing anything.
163 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
163 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
164 if publicurl:
164 if publicurl:
165 return b'Available At %s\n' b'# hg pull %s -r %s' % (
165 return b'Available At %s\n' b'# hg pull %s -r %s' % (
166 publicurl,
166 publicurl,
167 publicurl,
167 publicurl,
168 ctx,
168 ctx,
169 )
169 )
170 return None
170 return None
171
171
172
172
173 def uisetup(ui):
173 def uisetup(ui):
174 cmdutil.extraexport.append(b'pullurl')
174 cmdutil.extraexport.append(b'pullurl')
175 cmdutil.extraexportmap[b'pullurl'] = _addpullheader
175 cmdutil.extraexportmap[b'pullurl'] = _addpullheader
176
176
177
177
178 def reposetup(ui, repo):
178 def reposetup(ui, repo):
179 if not repo.local():
179 if not repo.local():
180 return
180 return
181 repo._wlockfreeprefix.add(b'last-email.txt')
181 repo._wlockfreeprefix.add(b'last-email.txt')
182
182
183
183
184 def prompt(ui, prompt, default=None, rest=b':'):
184 def prompt(ui, prompt, default=None, rest=b':'):
185 if default:
185 if default:
186 prompt += b' [%s]' % default
186 prompt += b' [%s]' % default
187 return ui.prompt(prompt + rest, default)
187 return ui.prompt(prompt + rest, default)
188
188
189
189
190 def introwanted(ui, opts, number):
190 def introwanted(ui, opts, number):
191 '''is an introductory message apparently wanted?'''
191 '''is an introductory message apparently wanted?'''
192 introconfig = ui.config(b'patchbomb', b'intro')
192 introconfig = ui.config(b'patchbomb', b'intro')
193 if opts.get(b'intro') or opts.get(b'desc'):
193 if opts.get(b'intro') or opts.get(b'desc'):
194 intro = True
194 intro = True
195 elif introconfig == b'always':
195 elif introconfig == b'always':
196 intro = True
196 intro = True
197 elif introconfig == b'never':
197 elif introconfig == b'never':
198 intro = False
198 intro = False
199 elif introconfig == b'auto':
199 elif introconfig == b'auto':
200 intro = number > 1
200 intro = number > 1
201 else:
201 else:
202 ui.write_err(
202 ui.write_err(
203 _(b'warning: invalid patchbomb.intro value "%s"\n') % introconfig
203 _(b'warning: invalid patchbomb.intro value "%s"\n') % introconfig
204 )
204 )
205 ui.write_err(_(b'(should be one of always, never, auto)\n'))
205 ui.write_err(_(b'(should be one of always, never, auto)\n'))
206 intro = number > 1
206 intro = number > 1
207 return intro
207 return intro
208
208
209
209
210 def _formatflags(ui, repo, rev, flags):
210 def _formatflags(ui, repo, rev, flags):
211 """build flag string optionally by template"""
211 """build flag string optionally by template"""
212 tmpl = ui.config(b'patchbomb', b'flagtemplate')
212 tmpl = ui.config(b'patchbomb', b'flagtemplate')
213 if not tmpl:
213 if not tmpl:
214 return b' '.join(flags)
214 return b' '.join(flags)
215 out = util.stringio()
215 out = util.stringio()
216 opts = {b'template': templater.unquotestring(tmpl)}
216 spec = formatter.templatespec(b'', templater.unquotestring(tmpl), None)
217 with formatter.templateformatter(ui, out, b'patchbombflag', opts) as fm:
217 with formatter.templateformatter(ui, out, b'patchbombflag', {}, spec) as fm:
218 fm.startitem()
218 fm.startitem()
219 fm.context(ctx=repo[rev])
219 fm.context(ctx=repo[rev])
220 fm.write(b'flags', b'%s', fm.formatlist(flags, name=b'flag'))
220 fm.write(b'flags', b'%s', fm.formatlist(flags, name=b'flag'))
221 return out.getvalue()
221 return out.getvalue()
222
222
223
223
224 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
224 def _formatprefix(ui, repo, rev, flags, idx, total, numbered):
225 """build prefix to patch subject"""
225 """build prefix to patch subject"""
226 flag = _formatflags(ui, repo, rev, flags)
226 flag = _formatflags(ui, repo, rev, flags)
227 if flag:
227 if flag:
228 flag = b' ' + flag
228 flag = b' ' + flag
229
229
230 if not numbered:
230 if not numbered:
231 return b'[PATCH%s]' % flag
231 return b'[PATCH%s]' % flag
232 else:
232 else:
233 tlen = len(b"%d" % total)
233 tlen = len(b"%d" % total)
234 return b'[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
234 return b'[PATCH %0*d of %d%s]' % (tlen, idx, total, flag)
235
235
236
236
237 def makepatch(
237 def makepatch(
238 ui,
238 ui,
239 repo,
239 repo,
240 rev,
240 rev,
241 patchlines,
241 patchlines,
242 opts,
242 opts,
243 _charsets,
243 _charsets,
244 idx,
244 idx,
245 total,
245 total,
246 numbered,
246 numbered,
247 patchname=None,
247 patchname=None,
248 ):
248 ):
249
249
250 desc = []
250 desc = []
251 node = None
251 node = None
252 body = b''
252 body = b''
253
253
254 for line in patchlines:
254 for line in patchlines:
255 if line.startswith(b'#'):
255 if line.startswith(b'#'):
256 if line.startswith(b'# Node ID'):
256 if line.startswith(b'# Node ID'):
257 node = line.split()[-1]
257 node = line.split()[-1]
258 continue
258 continue
259 if line.startswith(b'diff -r') or line.startswith(b'diff --git'):
259 if line.startswith(b'diff -r') or line.startswith(b'diff --git'):
260 break
260 break
261 desc.append(line)
261 desc.append(line)
262
262
263 if not patchname and not node:
263 if not patchname and not node:
264 raise ValueError
264 raise ValueError
265
265
266 if opts.get(b'attach') and not opts.get(b'body'):
266 if opts.get(b'attach') and not opts.get(b'body'):
267 body = (
267 body = (
268 b'\n'.join(desc[1:]).strip()
268 b'\n'.join(desc[1:]).strip()
269 or b'Patch subject is complete summary.'
269 or b'Patch subject is complete summary.'
270 )
270 )
271 body += b'\n\n\n'
271 body += b'\n\n\n'
272
272
273 if opts.get(b'plain'):
273 if opts.get(b'plain'):
274 while patchlines and patchlines[0].startswith(b'# '):
274 while patchlines and patchlines[0].startswith(b'# '):
275 patchlines.pop(0)
275 patchlines.pop(0)
276 if patchlines:
276 if patchlines:
277 patchlines.pop(0)
277 patchlines.pop(0)
278 while patchlines and not patchlines[0].strip():
278 while patchlines and not patchlines[0].strip():
279 patchlines.pop(0)
279 patchlines.pop(0)
280
280
281 ds = patch.diffstat(patchlines)
281 ds = patch.diffstat(patchlines)
282 if opts.get(b'diffstat'):
282 if opts.get(b'diffstat'):
283 body += ds + b'\n\n'
283 body += ds + b'\n\n'
284
284
285 addattachment = opts.get(b'attach') or opts.get(b'inline')
285 addattachment = opts.get(b'attach') or opts.get(b'inline')
286 if not addattachment or opts.get(b'body'):
286 if not addattachment or opts.get(b'body'):
287 body += b'\n'.join(patchlines)
287 body += b'\n'.join(patchlines)
288
288
289 if addattachment:
289 if addattachment:
290 msg = emimemultipart.MIMEMultipart()
290 msg = emimemultipart.MIMEMultipart()
291 if body:
291 if body:
292 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(b'test')))
292 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(b'test')))
293 p = mail.mimetextpatch(
293 p = mail.mimetextpatch(
294 b'\n'.join(patchlines), b'x-patch', opts.get(b'test')
294 b'\n'.join(patchlines), b'x-patch', opts.get(b'test')
295 )
295 )
296 binnode = nodemod.bin(node)
296 binnode = nodemod.bin(node)
297 # if node is mq patch, it will have the patch file's name as a tag
297 # if node is mq patch, it will have the patch file's name as a tag
298 if not patchname:
298 if not patchname:
299 patchtags = [
299 patchtags = [
300 t
300 t
301 for t in repo.nodetags(binnode)
301 for t in repo.nodetags(binnode)
302 if t.endswith(b'.patch') or t.endswith(b'.diff')
302 if t.endswith(b'.patch') or t.endswith(b'.diff')
303 ]
303 ]
304 if patchtags:
304 if patchtags:
305 patchname = patchtags[0]
305 patchname = patchtags[0]
306 elif total > 1:
306 elif total > 1:
307 patchname = cmdutil.makefilename(
307 patchname = cmdutil.makefilename(
308 repo[node], b'%b-%n.patch', seqno=idx, total=total
308 repo[node], b'%b-%n.patch', seqno=idx, total=total
309 )
309 )
310 else:
310 else:
311 patchname = cmdutil.makefilename(repo[node], b'%b.patch')
311 patchname = cmdutil.makefilename(repo[node], b'%b.patch')
312 disposition = r'inline'
312 disposition = r'inline'
313 if opts.get(b'attach'):
313 if opts.get(b'attach'):
314 disposition = r'attachment'
314 disposition = r'attachment'
315 p[r'Content-Disposition'] = (
315 p[r'Content-Disposition'] = (
316 disposition + r'; filename=' + encoding.strfromlocal(patchname)
316 disposition + r'; filename=' + encoding.strfromlocal(patchname)
317 )
317 )
318 msg.attach(p)
318 msg.attach(p)
319 else:
319 else:
320 msg = mail.mimetextpatch(body, display=opts.get(b'test'))
320 msg = mail.mimetextpatch(body, display=opts.get(b'test'))
321
321
322 prefix = _formatprefix(
322 prefix = _formatprefix(
323 ui, repo, rev, opts.get(b'flag'), idx, total, numbered
323 ui, repo, rev, opts.get(b'flag'), idx, total, numbered
324 )
324 )
325 subj = desc[0].strip().rstrip(b'. ')
325 subj = desc[0].strip().rstrip(b'. ')
326 if not numbered:
326 if not numbered:
327 subj = b' '.join([prefix, opts.get(b'subject') or subj])
327 subj = b' '.join([prefix, opts.get(b'subject') or subj])
328 else:
328 else:
329 subj = b' '.join([prefix, subj])
329 subj = b' '.join([prefix, subj])
330 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(b'test'))
330 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(b'test'))
331 msg[b'X-Mercurial-Node'] = node
331 msg[b'X-Mercurial-Node'] = node
332 msg[b'X-Mercurial-Series-Index'] = b'%i' % idx
332 msg[b'X-Mercurial-Series-Index'] = b'%i' % idx
333 msg[b'X-Mercurial-Series-Total'] = b'%i' % total
333 msg[b'X-Mercurial-Series-Total'] = b'%i' % total
334 return msg, subj, ds
334 return msg, subj, ds
335
335
336
336
337 def _getpatches(repo, revs, **opts):
337 def _getpatches(repo, revs, **opts):
338 """return a list of patches for a list of revisions
338 """return a list of patches for a list of revisions
339
339
340 Each patch in the list is itself a list of lines.
340 Each patch in the list is itself a list of lines.
341 """
341 """
342 ui = repo.ui
342 ui = repo.ui
343 prev = repo[b'.'].rev()
343 prev = repo[b'.'].rev()
344 for r in revs:
344 for r in revs:
345 if r == prev and (repo[None].files() or repo[None].deleted()):
345 if r == prev and (repo[None].files() or repo[None].deleted()):
346 ui.warn(
346 ui.warn(
347 _(b'warning: working directory has ' b'uncommitted changes\n')
347 _(b'warning: working directory has ' b'uncommitted changes\n')
348 )
348 )
349 output = stringio()
349 output = stringio()
350 cmdutil.exportfile(
350 cmdutil.exportfile(
351 repo, [r], output, opts=patch.difffeatureopts(ui, opts, git=True)
351 repo, [r], output, opts=patch.difffeatureopts(ui, opts, git=True)
352 )
352 )
353 yield output.getvalue().split(b'\n')
353 yield output.getvalue().split(b'\n')
354
354
355
355
356 def _getbundle(repo, dest, **opts):
356 def _getbundle(repo, dest, **opts):
357 """return a bundle containing changesets missing in "dest"
357 """return a bundle containing changesets missing in "dest"
358
358
359 The `opts` keyword-arguments are the same as the one accepted by the
359 The `opts` keyword-arguments are the same as the one accepted by the
360 `bundle` command.
360 `bundle` command.
361
361
362 The bundle is a returned as a single in-memory binary blob.
362 The bundle is a returned as a single in-memory binary blob.
363 """
363 """
364 ui = repo.ui
364 ui = repo.ui
365 tmpdir = pycompat.mkdtemp(prefix=b'hg-email-bundle-')
365 tmpdir = pycompat.mkdtemp(prefix=b'hg-email-bundle-')
366 tmpfn = os.path.join(tmpdir, b'bundle')
366 tmpfn = os.path.join(tmpdir, b'bundle')
367 btype = ui.config(b'patchbomb', b'bundletype')
367 btype = ui.config(b'patchbomb', b'bundletype')
368 if btype:
368 if btype:
369 opts[r'type'] = btype
369 opts[r'type'] = btype
370 try:
370 try:
371 commands.bundle(ui, repo, tmpfn, dest, **opts)
371 commands.bundle(ui, repo, tmpfn, dest, **opts)
372 return util.readfile(tmpfn)
372 return util.readfile(tmpfn)
373 finally:
373 finally:
374 try:
374 try:
375 os.unlink(tmpfn)
375 os.unlink(tmpfn)
376 except OSError:
376 except OSError:
377 pass
377 pass
378 os.rmdir(tmpdir)
378 os.rmdir(tmpdir)
379
379
380
380
381 def _getdescription(repo, defaultbody, sender, **opts):
381 def _getdescription(repo, defaultbody, sender, **opts):
382 """obtain the body of the introduction message and return it
382 """obtain the body of the introduction message and return it
383
383
384 This is also used for the body of email with an attached bundle.
384 This is also used for the body of email with an attached bundle.
385
385
386 The body can be obtained either from the command line option or entered by
386 The body can be obtained either from the command line option or entered by
387 the user through the editor.
387 the user through the editor.
388 """
388 """
389 ui = repo.ui
389 ui = repo.ui
390 if opts.get(r'desc'):
390 if opts.get(r'desc'):
391 body = open(opts.get(r'desc')).read()
391 body = open(opts.get(r'desc')).read()
392 else:
392 else:
393 ui.write(
393 ui.write(
394 _(b'\nWrite the introductory message for the ' b'patch series.\n\n')
394 _(b'\nWrite the introductory message for the ' b'patch series.\n\n')
395 )
395 )
396 body = ui.edit(
396 body = ui.edit(
397 defaultbody, sender, repopath=repo.path, action=b'patchbombbody'
397 defaultbody, sender, repopath=repo.path, action=b'patchbombbody'
398 )
398 )
399 # Save series description in case sendmail fails
399 # Save series description in case sendmail fails
400 msgfile = repo.vfs(b'last-email.txt', b'wb')
400 msgfile = repo.vfs(b'last-email.txt', b'wb')
401 msgfile.write(body)
401 msgfile.write(body)
402 msgfile.close()
402 msgfile.close()
403 return body
403 return body
404
404
405
405
406 def _getbundlemsgs(repo, sender, bundle, **opts):
406 def _getbundlemsgs(repo, sender, bundle, **opts):
407 """Get the full email for sending a given bundle
407 """Get the full email for sending a given bundle
408
408
409 This function returns a list of "email" tuples (subject, content, None).
409 This function returns a list of "email" tuples (subject, content, None).
410 The list is always one message long in that case.
410 The list is always one message long in that case.
411 """
411 """
412 ui = repo.ui
412 ui = repo.ui
413 _charsets = mail._charsets(ui)
413 _charsets = mail._charsets(ui)
414 subj = opts.get(r'subject') or prompt(
414 subj = opts.get(r'subject') or prompt(
415 ui, b'Subject:', b'A bundle for your repository'
415 ui, b'Subject:', b'A bundle for your repository'
416 )
416 )
417
417
418 body = _getdescription(repo, b'', sender, **opts)
418 body = _getdescription(repo, b'', sender, **opts)
419 msg = emimemultipart.MIMEMultipart()
419 msg = emimemultipart.MIMEMultipart()
420 if body:
420 if body:
421 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
421 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(r'test')))
422 datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
422 datapart = emimebase.MIMEBase(r'application', r'x-mercurial-bundle')
423 datapart.set_payload(bundle)
423 datapart.set_payload(bundle)
424 bundlename = b'%s.hg' % opts.get(r'bundlename', b'bundle')
424 bundlename = b'%s.hg' % opts.get(r'bundlename', b'bundle')
425 datapart.add_header(
425 datapart.add_header(
426 r'Content-Disposition',
426 r'Content-Disposition',
427 r'attachment',
427 r'attachment',
428 filename=encoding.strfromlocal(bundlename),
428 filename=encoding.strfromlocal(bundlename),
429 )
429 )
430 emailencoders.encode_base64(datapart)
430 emailencoders.encode_base64(datapart)
431 msg.attach(datapart)
431 msg.attach(datapart)
432 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
432 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
433 return [(msg, subj, None)]
433 return [(msg, subj, None)]
434
434
435
435
436 def _makeintro(repo, sender, revs, patches, **opts):
436 def _makeintro(repo, sender, revs, patches, **opts):
437 """make an introduction email, asking the user for content if needed
437 """make an introduction email, asking the user for content if needed
438
438
439 email is returned as (subject, body, cumulative-diffstat)"""
439 email is returned as (subject, body, cumulative-diffstat)"""
440 ui = repo.ui
440 ui = repo.ui
441 _charsets = mail._charsets(ui)
441 _charsets = mail._charsets(ui)
442
442
443 # use the last revision which is likely to be a bookmarked head
443 # use the last revision which is likely to be a bookmarked head
444 prefix = _formatprefix(
444 prefix = _formatprefix(
445 ui, repo, revs.last(), opts.get(r'flag'), 0, len(patches), numbered=True
445 ui, repo, revs.last(), opts.get(r'flag'), 0, len(patches), numbered=True
446 )
446 )
447 subj = opts.get(r'subject') or prompt(
447 subj = opts.get(r'subject') or prompt(
448 ui, b'(optional) Subject: ', rest=prefix, default=b''
448 ui, b'(optional) Subject: ', rest=prefix, default=b''
449 )
449 )
450 if not subj:
450 if not subj:
451 return None # skip intro if the user doesn't bother
451 return None # skip intro if the user doesn't bother
452
452
453 subj = prefix + b' ' + subj
453 subj = prefix + b' ' + subj
454
454
455 body = b''
455 body = b''
456 if opts.get(r'diffstat'):
456 if opts.get(r'diffstat'):
457 # generate a cumulative diffstat of the whole patch series
457 # generate a cumulative diffstat of the whole patch series
458 diffstat = patch.diffstat(sum(patches, []))
458 diffstat = patch.diffstat(sum(patches, []))
459 body = b'\n' + diffstat
459 body = b'\n' + diffstat
460 else:
460 else:
461 diffstat = None
461 diffstat = None
462
462
463 body = _getdescription(repo, body, sender, **opts)
463 body = _getdescription(repo, body, sender, **opts)
464 msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
464 msg = mail.mimeencode(ui, body, _charsets, opts.get(r'test'))
465 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
465 msg[b'Subject'] = mail.headencode(ui, subj, _charsets, opts.get(r'test'))
466 return (msg, subj, diffstat)
466 return (msg, subj, diffstat)
467
467
468
468
469 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
469 def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts):
470 """return a list of emails from a list of patches
470 """return a list of emails from a list of patches
471
471
472 This involves introduction message creation if necessary.
472 This involves introduction message creation if necessary.
473
473
474 This function returns a list of "email" tuples (subject, content, None).
474 This function returns a list of "email" tuples (subject, content, None).
475 """
475 """
476 bytesopts = pycompat.byteskwargs(opts)
476 bytesopts = pycompat.byteskwargs(opts)
477 ui = repo.ui
477 ui = repo.ui
478 _charsets = mail._charsets(ui)
478 _charsets = mail._charsets(ui)
479 patches = list(_getpatches(repo, revs, **opts))
479 patches = list(_getpatches(repo, revs, **opts))
480 msgs = []
480 msgs = []
481
481
482 ui.write(_(b'this patch series consists of %d patches.\n\n') % len(patches))
482 ui.write(_(b'this patch series consists of %d patches.\n\n') % len(patches))
483
483
484 # build the intro message, or skip it if the user declines
484 # build the intro message, or skip it if the user declines
485 if introwanted(ui, bytesopts, len(patches)):
485 if introwanted(ui, bytesopts, len(patches)):
486 msg = _makeintro(repo, sender, revs, patches, **opts)
486 msg = _makeintro(repo, sender, revs, patches, **opts)
487 if msg:
487 if msg:
488 msgs.append(msg)
488 msgs.append(msg)
489
489
490 # are we going to send more than one message?
490 # are we going to send more than one message?
491 numbered = len(msgs) + len(patches) > 1
491 numbered = len(msgs) + len(patches) > 1
492
492
493 # now generate the actual patch messages
493 # now generate the actual patch messages
494 name = None
494 name = None
495 assert len(revs) == len(patches)
495 assert len(revs) == len(patches)
496 for i, (r, p) in enumerate(zip(revs, patches)):
496 for i, (r, p) in enumerate(zip(revs, patches)):
497 if patchnames:
497 if patchnames:
498 name = patchnames[i]
498 name = patchnames[i]
499 msg = makepatch(
499 msg = makepatch(
500 ui,
500 ui,
501 repo,
501 repo,
502 r,
502 r,
503 p,
503 p,
504 bytesopts,
504 bytesopts,
505 _charsets,
505 _charsets,
506 i + 1,
506 i + 1,
507 len(patches),
507 len(patches),
508 numbered,
508 numbered,
509 name,
509 name,
510 )
510 )
511 msgs.append(msg)
511 msgs.append(msg)
512
512
513 return msgs
513 return msgs
514
514
515
515
516 def _getoutgoing(repo, dest, revs):
516 def _getoutgoing(repo, dest, revs):
517 '''Return the revisions present locally but not in dest'''
517 '''Return the revisions present locally but not in dest'''
518 ui = repo.ui
518 ui = repo.ui
519 url = ui.expandpath(dest or b'default-push', dest or b'default')
519 url = ui.expandpath(dest or b'default-push', dest or b'default')
520 url = hg.parseurl(url)[0]
520 url = hg.parseurl(url)[0]
521 ui.status(_(b'comparing with %s\n') % util.hidepassword(url))
521 ui.status(_(b'comparing with %s\n') % util.hidepassword(url))
522
522
523 revs = [r for r in revs if r >= 0]
523 revs = [r for r in revs if r >= 0]
524 if not revs:
524 if not revs:
525 revs = [repo.changelog.tiprev()]
525 revs = [repo.changelog.tiprev()]
526 revs = repo.revs(b'outgoing(%s) and ::%ld', dest or b'', revs)
526 revs = repo.revs(b'outgoing(%s) and ::%ld', dest or b'', revs)
527 if not revs:
527 if not revs:
528 ui.status(_(b"no changes found\n"))
528 ui.status(_(b"no changes found\n"))
529 return revs
529 return revs
530
530
531
531
532 def _msgid(node, timestamp):
532 def _msgid(node, timestamp):
533 hostname = encoding.strtolocal(socket.getfqdn())
533 hostname = encoding.strtolocal(socket.getfqdn())
534 hostname = encoding.environ.get(b'HGHOSTNAME', hostname)
534 hostname = encoding.environ.get(b'HGHOSTNAME', hostname)
535 return b'<%s.%d@%s>' % (node, timestamp, hostname)
535 return b'<%s.%d@%s>' % (node, timestamp, hostname)
536
536
537
537
538 emailopts = [
538 emailopts = [
539 (b'', b'body', None, _(b'send patches as inline message text (default)')),
539 (b'', b'body', None, _(b'send patches as inline message text (default)')),
540 (b'a', b'attach', None, _(b'send patches as attachments')),
540 (b'a', b'attach', None, _(b'send patches as attachments')),
541 (b'i', b'inline', None, _(b'send patches as inline attachments')),
541 (b'i', b'inline', None, _(b'send patches as inline attachments')),
542 (
542 (
543 b'',
543 b'',
544 b'bcc',
544 b'bcc',
545 [],
545 [],
546 _(b'email addresses of blind carbon copy recipients'),
546 _(b'email addresses of blind carbon copy recipients'),
547 _(b'EMAIL'),
547 _(b'EMAIL'),
548 ),
548 ),
549 (b'c', b'cc', [], _(b'email addresses of copy recipients'), _(b'EMAIL')),
549 (b'c', b'cc', [], _(b'email addresses of copy recipients'), _(b'EMAIL')),
550 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
550 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
551 (b'd', b'diffstat', None, _(b'add diffstat output to messages')),
551 (b'd', b'diffstat', None, _(b'add diffstat output to messages')),
552 (
552 (
553 b'',
553 b'',
554 b'date',
554 b'date',
555 b'',
555 b'',
556 _(b'use the given date as the sending date'),
556 _(b'use the given date as the sending date'),
557 _(b'DATE'),
557 _(b'DATE'),
558 ),
558 ),
559 (
559 (
560 b'',
560 b'',
561 b'desc',
561 b'desc',
562 b'',
562 b'',
563 _(b'use the given file as the series description'),
563 _(b'use the given file as the series description'),
564 _(b'FILE'),
564 _(b'FILE'),
565 ),
565 ),
566 (b'f', b'from', b'', _(b'email address of sender'), _(b'EMAIL')),
566 (b'f', b'from', b'', _(b'email address of sender'), _(b'EMAIL')),
567 (b'n', b'test', None, _(b'print messages that would be sent')),
567 (b'n', b'test', None, _(b'print messages that would be sent')),
568 (
568 (
569 b'm',
569 b'm',
570 b'mbox',
570 b'mbox',
571 b'',
571 b'',
572 _(b'write messages to mbox file instead of sending them'),
572 _(b'write messages to mbox file instead of sending them'),
573 _(b'FILE'),
573 _(b'FILE'),
574 ),
574 ),
575 (
575 (
576 b'',
576 b'',
577 b'reply-to',
577 b'reply-to',
578 [],
578 [],
579 _(b'email addresses replies should be sent to'),
579 _(b'email addresses replies should be sent to'),
580 _(b'EMAIL'),
580 _(b'EMAIL'),
581 ),
581 ),
582 (
582 (
583 b's',
583 b's',
584 b'subject',
584 b'subject',
585 b'',
585 b'',
586 _(b'subject of first message (intro or single patch)'),
586 _(b'subject of first message (intro or single patch)'),
587 _(b'TEXT'),
587 _(b'TEXT'),
588 ),
588 ),
589 (
589 (
590 b'',
590 b'',
591 b'in-reply-to',
591 b'in-reply-to',
592 b'',
592 b'',
593 _(b'message identifier to reply to'),
593 _(b'message identifier to reply to'),
594 _(b'MSGID'),
594 _(b'MSGID'),
595 ),
595 ),
596 (b'', b'flag', [], _(b'flags to add in subject prefixes'), _(b'FLAG')),
596 (b'', b'flag', [], _(b'flags to add in subject prefixes'), _(b'FLAG')),
597 (b't', b'to', [], _(b'email addresses of recipients'), _(b'EMAIL')),
597 (b't', b'to', [], _(b'email addresses of recipients'), _(b'EMAIL')),
598 ]
598 ]
599
599
600
600
601 @command(
601 @command(
602 b'email',
602 b'email',
603 [
603 [
604 (b'g', b'git', None, _(b'use git extended diff format')),
604 (b'g', b'git', None, _(b'use git extended diff format')),
605 (b'', b'plain', None, _(b'omit hg patch header')),
605 (b'', b'plain', None, _(b'omit hg patch header')),
606 (
606 (
607 b'o',
607 b'o',
608 b'outgoing',
608 b'outgoing',
609 None,
609 None,
610 _(b'send changes not found in the target repository'),
610 _(b'send changes not found in the target repository'),
611 ),
611 ),
612 (
612 (
613 b'b',
613 b'b',
614 b'bundle',
614 b'bundle',
615 None,
615 None,
616 _(b'send changes not in target as a binary bundle'),
616 _(b'send changes not in target as a binary bundle'),
617 ),
617 ),
618 (
618 (
619 b'B',
619 b'B',
620 b'bookmark',
620 b'bookmark',
621 b'',
621 b'',
622 _(b'send changes only reachable by given bookmark'),
622 _(b'send changes only reachable by given bookmark'),
623 _(b'BOOKMARK'),
623 _(b'BOOKMARK'),
624 ),
624 ),
625 (
625 (
626 b'',
626 b'',
627 b'bundlename',
627 b'bundlename',
628 b'bundle',
628 b'bundle',
629 _(b'name of the bundle attachment file'),
629 _(b'name of the bundle attachment file'),
630 _(b'NAME'),
630 _(b'NAME'),
631 ),
631 ),
632 (b'r', b'rev', [], _(b'a revision to send'), _(b'REV')),
632 (b'r', b'rev', [], _(b'a revision to send'), _(b'REV')),
633 (
633 (
634 b'',
634 b'',
635 b'force',
635 b'force',
636 None,
636 None,
637 _(
637 _(
638 b'run even when remote repository is unrelated '
638 b'run even when remote repository is unrelated '
639 b'(with -b/--bundle)'
639 b'(with -b/--bundle)'
640 ),
640 ),
641 ),
641 ),
642 (
642 (
643 b'',
643 b'',
644 b'base',
644 b'base',
645 [],
645 [],
646 _(
646 _(
647 b'a base changeset to specify instead of a destination '
647 b'a base changeset to specify instead of a destination '
648 b'(with -b/--bundle)'
648 b'(with -b/--bundle)'
649 ),
649 ),
650 _(b'REV'),
650 _(b'REV'),
651 ),
651 ),
652 (
652 (
653 b'',
653 b'',
654 b'intro',
654 b'intro',
655 None,
655 None,
656 _(b'send an introduction email for a single patch'),
656 _(b'send an introduction email for a single patch'),
657 ),
657 ),
658 ]
658 ]
659 + emailopts
659 + emailopts
660 + cmdutil.remoteopts,
660 + cmdutil.remoteopts,
661 _(b'hg email [OPTION]... [DEST]...'),
661 _(b'hg email [OPTION]... [DEST]...'),
662 helpcategory=command.CATEGORY_IMPORT_EXPORT,
662 helpcategory=command.CATEGORY_IMPORT_EXPORT,
663 )
663 )
664 def email(ui, repo, *revs, **opts):
664 def email(ui, repo, *revs, **opts):
665 '''send changesets by email
665 '''send changesets by email
666
666
667 By default, diffs are sent in the format generated by
667 By default, diffs are sent in the format generated by
668 :hg:`export`, one per message. The series starts with a "[PATCH 0
668 :hg:`export`, one per message. The series starts with a "[PATCH 0
669 of N]" introduction, which describes the series as a whole.
669 of N]" introduction, which describes the series as a whole.
670
670
671 Each patch email has a Subject line of "[PATCH M of N] ...", using
671 Each patch email has a Subject line of "[PATCH M of N] ...", using
672 the first line of the changeset description as the subject text.
672 the first line of the changeset description as the subject text.
673 The message contains two or three parts. First, the changeset
673 The message contains two or three parts. First, the changeset
674 description.
674 description.
675
675
676 With the -d/--diffstat option, if the diffstat program is
676 With the -d/--diffstat option, if the diffstat program is
677 installed, the result of running diffstat on the patch is inserted.
677 installed, the result of running diffstat on the patch is inserted.
678
678
679 Finally, the patch itself, as generated by :hg:`export`.
679 Finally, the patch itself, as generated by :hg:`export`.
680
680
681 With the -d/--diffstat or --confirm options, you will be presented
681 With the -d/--diffstat or --confirm options, you will be presented
682 with a final summary of all messages and asked for confirmation before
682 with a final summary of all messages and asked for confirmation before
683 the messages are sent.
683 the messages are sent.
684
684
685 By default the patch is included as text in the email body for
685 By default the patch is included as text in the email body for
686 easy reviewing. Using the -a/--attach option will instead create
686 easy reviewing. Using the -a/--attach option will instead create
687 an attachment for the patch. With -i/--inline an inline attachment
687 an attachment for the patch. With -i/--inline an inline attachment
688 will be created. You can include a patch both as text in the email
688 will be created. You can include a patch both as text in the email
689 body and as a regular or an inline attachment by combining the
689 body and as a regular or an inline attachment by combining the
690 -a/--attach or -i/--inline with the --body option.
690 -a/--attach or -i/--inline with the --body option.
691
691
692 With -B/--bookmark changesets reachable by the given bookmark are
692 With -B/--bookmark changesets reachable by the given bookmark are
693 selected.
693 selected.
694
694
695 With -o/--outgoing, emails will be generated for patches not found
695 With -o/--outgoing, emails will be generated for patches not found
696 in the destination repository (or only those which are ancestors
696 in the destination repository (or only those which are ancestors
697 of the specified revisions if any are provided)
697 of the specified revisions if any are provided)
698
698
699 With -b/--bundle, changesets are selected as for --outgoing, but a
699 With -b/--bundle, changesets are selected as for --outgoing, but a
700 single email containing a binary Mercurial bundle as an attachment
700 single email containing a binary Mercurial bundle as an attachment
701 will be sent. Use the ``patchbomb.bundletype`` config option to
701 will be sent. Use the ``patchbomb.bundletype`` config option to
702 control the bundle type as with :hg:`bundle --type`.
702 control the bundle type as with :hg:`bundle --type`.
703
703
704 With -m/--mbox, instead of previewing each patchbomb message in a
704 With -m/--mbox, instead of previewing each patchbomb message in a
705 pager or sending the messages directly, it will create a UNIX
705 pager or sending the messages directly, it will create a UNIX
706 mailbox file with the patch emails. This mailbox file can be
706 mailbox file with the patch emails. This mailbox file can be
707 previewed with any mail user agent which supports UNIX mbox
707 previewed with any mail user agent which supports UNIX mbox
708 files.
708 files.
709
709
710 With -n/--test, all steps will run, but mail will not be sent.
710 With -n/--test, all steps will run, but mail will not be sent.
711 You will be prompted for an email recipient address, a subject and
711 You will be prompted for an email recipient address, a subject and
712 an introductory message describing the patches of your patchbomb.
712 an introductory message describing the patches of your patchbomb.
713 Then when all is done, patchbomb messages are displayed.
713 Then when all is done, patchbomb messages are displayed.
714
714
715 In case email sending fails, you will find a backup of your series
715 In case email sending fails, you will find a backup of your series
716 introductory message in ``.hg/last-email.txt``.
716 introductory message in ``.hg/last-email.txt``.
717
717
718 The default behavior of this command can be customized through
718 The default behavior of this command can be customized through
719 configuration. (See :hg:`help patchbomb` for details)
719 configuration. (See :hg:`help patchbomb` for details)
720
720
721 Examples::
721 Examples::
722
722
723 hg email -r 3000 # send patch 3000 only
723 hg email -r 3000 # send patch 3000 only
724 hg email -r 3000 -r 3001 # send patches 3000 and 3001
724 hg email -r 3000 -r 3001 # send patches 3000 and 3001
725 hg email -r 3000:3005 # send patches 3000 through 3005
725 hg email -r 3000:3005 # send patches 3000 through 3005
726 hg email 3000 # send patch 3000 (deprecated)
726 hg email 3000 # send patch 3000 (deprecated)
727
727
728 hg email -o # send all patches not in default
728 hg email -o # send all patches not in default
729 hg email -o DEST # send all patches not in DEST
729 hg email -o DEST # send all patches not in DEST
730 hg email -o -r 3000 # send all ancestors of 3000 not in default
730 hg email -o -r 3000 # send all ancestors of 3000 not in default
731 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
731 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
732
732
733 hg email -B feature # send all ancestors of feature bookmark
733 hg email -B feature # send all ancestors of feature bookmark
734
734
735 hg email -b # send bundle of all patches not in default
735 hg email -b # send bundle of all patches not in default
736 hg email -b DEST # send bundle of all patches not in DEST
736 hg email -b DEST # send bundle of all patches not in DEST
737 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
737 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
738 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
738 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
739
739
740 hg email -o -m mbox && # generate an mbox file...
740 hg email -o -m mbox && # generate an mbox file...
741 mutt -R -f mbox # ... and view it with mutt
741 mutt -R -f mbox # ... and view it with mutt
742 hg email -o -m mbox && # generate an mbox file ...
742 hg email -o -m mbox && # generate an mbox file ...
743 formail -s sendmail \\ # ... and use formail to send from the mbox
743 formail -s sendmail \\ # ... and use formail to send from the mbox
744 -bm -t < mbox # ... using sendmail
744 -bm -t < mbox # ... using sendmail
745
745
746 Before using this command, you will need to enable email in your
746 Before using this command, you will need to enable email in your
747 hgrc. See the [email] section in hgrc(5) for details.
747 hgrc. See the [email] section in hgrc(5) for details.
748 '''
748 '''
749 opts = pycompat.byteskwargs(opts)
749 opts = pycompat.byteskwargs(opts)
750
750
751 _charsets = mail._charsets(ui)
751 _charsets = mail._charsets(ui)
752
752
753 bundle = opts.get(b'bundle')
753 bundle = opts.get(b'bundle')
754 date = opts.get(b'date')
754 date = opts.get(b'date')
755 mbox = opts.get(b'mbox')
755 mbox = opts.get(b'mbox')
756 outgoing = opts.get(b'outgoing')
756 outgoing = opts.get(b'outgoing')
757 rev = opts.get(b'rev')
757 rev = opts.get(b'rev')
758 bookmark = opts.get(b'bookmark')
758 bookmark = opts.get(b'bookmark')
759
759
760 if not (opts.get(b'test') or mbox):
760 if not (opts.get(b'test') or mbox):
761 # really sending
761 # really sending
762 mail.validateconfig(ui)
762 mail.validateconfig(ui)
763
763
764 if not (revs or rev or outgoing or bundle or bookmark):
764 if not (revs or rev or outgoing or bundle or bookmark):
765 raise error.Abort(
765 raise error.Abort(
766 _(b'specify at least one changeset with -B, -r or -o')
766 _(b'specify at least one changeset with -B, -r or -o')
767 )
767 )
768
768
769 if outgoing and bundle:
769 if outgoing and bundle:
770 raise error.Abort(
770 raise error.Abort(
771 _(
771 _(
772 b"--outgoing mode always on with --bundle;"
772 b"--outgoing mode always on with --bundle;"
773 b" do not re-specify --outgoing"
773 b" do not re-specify --outgoing"
774 )
774 )
775 )
775 )
776 if rev and bookmark:
776 if rev and bookmark:
777 raise error.Abort(_(b"-r and -B are mutually exclusive"))
777 raise error.Abort(_(b"-r and -B are mutually exclusive"))
778
778
779 if outgoing or bundle:
779 if outgoing or bundle:
780 if len(revs) > 1:
780 if len(revs) > 1:
781 raise error.Abort(_(b"too many destinations"))
781 raise error.Abort(_(b"too many destinations"))
782 if revs:
782 if revs:
783 dest = revs[0]
783 dest = revs[0]
784 else:
784 else:
785 dest = None
785 dest = None
786 revs = []
786 revs = []
787
787
788 if rev:
788 if rev:
789 if revs:
789 if revs:
790 raise error.Abort(_(b'use only one form to specify the revision'))
790 raise error.Abort(_(b'use only one form to specify the revision'))
791 revs = rev
791 revs = rev
792 elif bookmark:
792 elif bookmark:
793 if bookmark not in repo._bookmarks:
793 if bookmark not in repo._bookmarks:
794 raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
794 raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
795 revs = scmutil.bookmarkrevs(repo, bookmark)
795 revs = scmutil.bookmarkrevs(repo, bookmark)
796
796
797 revs = scmutil.revrange(repo, revs)
797 revs = scmutil.revrange(repo, revs)
798 if outgoing:
798 if outgoing:
799 revs = _getoutgoing(repo, dest, revs)
799 revs = _getoutgoing(repo, dest, revs)
800 if bundle:
800 if bundle:
801 opts[b'revs'] = [b"%d" % r for r in revs]
801 opts[b'revs'] = [b"%d" % r for r in revs]
802
802
803 # check if revision exist on the public destination
803 # check if revision exist on the public destination
804 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
804 publicurl = repo.ui.config(b'patchbomb', b'publicurl')
805 if publicurl:
805 if publicurl:
806 repo.ui.debug(b'checking that revision exist in the public repo\n')
806 repo.ui.debug(b'checking that revision exist in the public repo\n')
807 try:
807 try:
808 publicpeer = hg.peer(repo, {}, publicurl)
808 publicpeer = hg.peer(repo, {}, publicurl)
809 except error.RepoError:
809 except error.RepoError:
810 repo.ui.write_err(
810 repo.ui.write_err(
811 _(b'unable to access public repo: %s\n') % publicurl
811 _(b'unable to access public repo: %s\n') % publicurl
812 )
812 )
813 raise
813 raise
814 if not publicpeer.capable(b'known'):
814 if not publicpeer.capable(b'known'):
815 repo.ui.debug(b'skipping existence checks: public repo too old\n')
815 repo.ui.debug(b'skipping existence checks: public repo too old\n')
816 else:
816 else:
817 out = [repo[r] for r in revs]
817 out = [repo[r] for r in revs]
818 known = publicpeer.known(h.node() for h in out)
818 known = publicpeer.known(h.node() for h in out)
819 missing = []
819 missing = []
820 for idx, h in enumerate(out):
820 for idx, h in enumerate(out):
821 if not known[idx]:
821 if not known[idx]:
822 missing.append(h)
822 missing.append(h)
823 if missing:
823 if missing:
824 if len(missing) > 1:
824 if len(missing) > 1:
825 msg = _(b'public "%s" is missing %s and %i others')
825 msg = _(b'public "%s" is missing %s and %i others')
826 msg %= (publicurl, missing[0], len(missing) - 1)
826 msg %= (publicurl, missing[0], len(missing) - 1)
827 else:
827 else:
828 msg = _(b'public url %s is missing %s')
828 msg = _(b'public url %s is missing %s')
829 msg %= (publicurl, missing[0])
829 msg %= (publicurl, missing[0])
830 missingrevs = [ctx.rev() for ctx in missing]
830 missingrevs = [ctx.rev() for ctx in missing]
831 revhint = b' '.join(
831 revhint = b' '.join(
832 b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs)
832 b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs)
833 )
833 )
834 hint = _(b"use 'hg push %s %s'") % (publicurl, revhint)
834 hint = _(b"use 'hg push %s %s'") % (publicurl, revhint)
835 raise error.Abort(msg, hint=hint)
835 raise error.Abort(msg, hint=hint)
836
836
837 # start
837 # start
838 if date:
838 if date:
839 start_time = dateutil.parsedate(date)
839 start_time = dateutil.parsedate(date)
840 else:
840 else:
841 start_time = dateutil.makedate()
841 start_time = dateutil.makedate()
842
842
843 def genmsgid(id):
843 def genmsgid(id):
844 return _msgid(id[:20], int(start_time[0]))
844 return _msgid(id[:20], int(start_time[0]))
845
845
846 # deprecated config: patchbomb.from
846 # deprecated config: patchbomb.from
847 sender = (
847 sender = (
848 opts.get(b'from')
848 opts.get(b'from')
849 or ui.config(b'email', b'from')
849 or ui.config(b'email', b'from')
850 or ui.config(b'patchbomb', b'from')
850 or ui.config(b'patchbomb', b'from')
851 or prompt(ui, b'From', ui.username())
851 or prompt(ui, b'From', ui.username())
852 )
852 )
853
853
854 if bundle:
854 if bundle:
855 stropts = pycompat.strkwargs(opts)
855 stropts = pycompat.strkwargs(opts)
856 bundledata = _getbundle(repo, dest, **stropts)
856 bundledata = _getbundle(repo, dest, **stropts)
857 bundleopts = stropts.copy()
857 bundleopts = stropts.copy()
858 bundleopts.pop(r'bundle', None) # already processed
858 bundleopts.pop(r'bundle', None) # already processed
859 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
859 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
860 else:
860 else:
861 msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))
861 msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))
862
862
863 showaddrs = []
863 showaddrs = []
864
864
865 def getaddrs(header, ask=False, default=None):
865 def getaddrs(header, ask=False, default=None):
866 configkey = header.lower()
866 configkey = header.lower()
867 opt = header.replace(b'-', b'_').lower()
867 opt = header.replace(b'-', b'_').lower()
868 addrs = opts.get(opt)
868 addrs = opts.get(opt)
869 if addrs:
869 if addrs:
870 showaddrs.append(b'%s: %s' % (header, b', '.join(addrs)))
870 showaddrs.append(b'%s: %s' % (header, b', '.join(addrs)))
871 return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test'))
871 return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test'))
872
872
873 # not on the command line: fallback to config and then maybe ask
873 # not on the command line: fallback to config and then maybe ask
874 addr = ui.config(b'email', configkey) or ui.config(
874 addr = ui.config(b'email', configkey) or ui.config(
875 b'patchbomb', configkey
875 b'patchbomb', configkey
876 )
876 )
877 if not addr:
877 if not addr:
878 specified = ui.hasconfig(b'email', configkey) or ui.hasconfig(
878 specified = ui.hasconfig(b'email', configkey) or ui.hasconfig(
879 b'patchbomb', configkey
879 b'patchbomb', configkey
880 )
880 )
881 if not specified and ask:
881 if not specified and ask:
882 addr = prompt(ui, header, default=default)
882 addr = prompt(ui, header, default=default)
883 if addr:
883 if addr:
884 showaddrs.append(b'%s: %s' % (header, addr))
884 showaddrs.append(b'%s: %s' % (header, addr))
885 return mail.addrlistencode(ui, [addr], _charsets, opts.get(b'test'))
885 return mail.addrlistencode(ui, [addr], _charsets, opts.get(b'test'))
886 elif default:
886 elif default:
887 return mail.addrlistencode(
887 return mail.addrlistencode(
888 ui, [default], _charsets, opts.get(b'test')
888 ui, [default], _charsets, opts.get(b'test')
889 )
889 )
890 return []
890 return []
891
891
892 to = getaddrs(b'To', ask=True)
892 to = getaddrs(b'To', ask=True)
893 if not to:
893 if not to:
894 # we can get here in non-interactive mode
894 # we can get here in non-interactive mode
895 raise error.Abort(_(b'no recipient addresses provided'))
895 raise error.Abort(_(b'no recipient addresses provided'))
896 cc = getaddrs(b'Cc', ask=True, default=b'')
896 cc = getaddrs(b'Cc', ask=True, default=b'')
897 bcc = getaddrs(b'Bcc')
897 bcc = getaddrs(b'Bcc')
898 replyto = getaddrs(b'Reply-To')
898 replyto = getaddrs(b'Reply-To')
899
899
900 confirm = ui.configbool(b'patchbomb', b'confirm')
900 confirm = ui.configbool(b'patchbomb', b'confirm')
901 confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm'))
901 confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm'))
902
902
903 if confirm:
903 if confirm:
904 ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary')
904 ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary')
905 ui.write((b'From: %s\n' % sender), label=b'patchbomb.from')
905 ui.write((b'From: %s\n' % sender), label=b'patchbomb.from')
906 for addr in showaddrs:
906 for addr in showaddrs:
907 ui.write(b'%s\n' % addr, label=b'patchbomb.to')
907 ui.write(b'%s\n' % addr, label=b'patchbomb.to')
908 for m, subj, ds in msgs:
908 for m, subj, ds in msgs:
909 ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject')
909 ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject')
910 if ds:
910 if ds:
911 ui.write(ds, label=b'patchbomb.diffstats')
911 ui.write(ds, label=b'patchbomb.diffstats')
912 ui.write(b'\n')
912 ui.write(b'\n')
913 if ui.promptchoice(
913 if ui.promptchoice(
914 _(b'are you sure you want to send (yn)?' b'$$ &Yes $$ &No')
914 _(b'are you sure you want to send (yn)?' b'$$ &Yes $$ &No')
915 ):
915 ):
916 raise error.Abort(_(b'patchbomb canceled'))
916 raise error.Abort(_(b'patchbomb canceled'))
917
917
918 ui.write(b'\n')
918 ui.write(b'\n')
919
919
920 parent = opts.get(b'in_reply_to') or None
920 parent = opts.get(b'in_reply_to') or None
921 # angle brackets may be omitted, they're not semantically part of the msg-id
921 # angle brackets may be omitted, they're not semantically part of the msg-id
922 if parent is not None:
922 if parent is not None:
923 if not parent.startswith(b'<'):
923 if not parent.startswith(b'<'):
924 parent = b'<' + parent
924 parent = b'<' + parent
925 if not parent.endswith(b'>'):
925 if not parent.endswith(b'>'):
926 parent += b'>'
926 parent += b'>'
927
927
928 sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
928 sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
929 sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test'))
929 sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test'))
930 sendmail = None
930 sendmail = None
931 firstpatch = None
931 firstpatch = None
932 progress = ui.makeprogress(
932 progress = ui.makeprogress(
933 _(b'sending'), unit=_(b'emails'), total=len(msgs)
933 _(b'sending'), unit=_(b'emails'), total=len(msgs)
934 )
934 )
935 for i, (m, subj, ds) in enumerate(msgs):
935 for i, (m, subj, ds) in enumerate(msgs):
936 try:
936 try:
937 m[b'Message-Id'] = genmsgid(m[b'X-Mercurial-Node'])
937 m[b'Message-Id'] = genmsgid(m[b'X-Mercurial-Node'])
938 if not firstpatch:
938 if not firstpatch:
939 firstpatch = m[b'Message-Id']
939 firstpatch = m[b'Message-Id']
940 m[b'X-Mercurial-Series-Id'] = firstpatch
940 m[b'X-Mercurial-Series-Id'] = firstpatch
941 except TypeError:
941 except TypeError:
942 m[b'Message-Id'] = genmsgid(b'patchbomb')
942 m[b'Message-Id'] = genmsgid(b'patchbomb')
943 if parent:
943 if parent:
944 m[b'In-Reply-To'] = parent
944 m[b'In-Reply-To'] = parent
945 m[b'References'] = parent
945 m[b'References'] = parent
946 if not parent or b'X-Mercurial-Node' not in m:
946 if not parent or b'X-Mercurial-Node' not in m:
947 parent = m[b'Message-Id']
947 parent = m[b'Message-Id']
948
948
949 m[b'User-Agent'] = b'Mercurial-patchbomb/%s' % util.version()
949 m[b'User-Agent'] = b'Mercurial-patchbomb/%s' % util.version()
950 m[b'Date'] = eutil.formatdate(start_time[0], localtime=True)
950 m[b'Date'] = eutil.formatdate(start_time[0], localtime=True)
951
951
952 start_time = (start_time[0] + 1, start_time[1])
952 start_time = (start_time[0] + 1, start_time[1])
953 m[b'From'] = sender
953 m[b'From'] = sender
954 m[b'To'] = b', '.join(to)
954 m[b'To'] = b', '.join(to)
955 if cc:
955 if cc:
956 m[b'Cc'] = b', '.join(cc)
956 m[b'Cc'] = b', '.join(cc)
957 if bcc:
957 if bcc:
958 m[b'Bcc'] = b', '.join(bcc)
958 m[b'Bcc'] = b', '.join(bcc)
959 if replyto:
959 if replyto:
960 m[b'Reply-To'] = b', '.join(replyto)
960 m[b'Reply-To'] = b', '.join(replyto)
961 # Fix up all headers to be native strings.
961 # Fix up all headers to be native strings.
962 # TODO(durin42): this should probably be cleaned up above in the future.
962 # TODO(durin42): this should probably be cleaned up above in the future.
963 if pycompat.ispy3:
963 if pycompat.ispy3:
964 for hdr, val in list(m.items()):
964 for hdr, val in list(m.items()):
965 change = False
965 change = False
966 if isinstance(hdr, bytes):
966 if isinstance(hdr, bytes):
967 del m[hdr]
967 del m[hdr]
968 hdr = pycompat.strurl(hdr)
968 hdr = pycompat.strurl(hdr)
969 change = True
969 change = True
970 if isinstance(val, bytes):
970 if isinstance(val, bytes):
971 val = pycompat.strurl(val)
971 val = pycompat.strurl(val)
972 if not change:
972 if not change:
973 # prevent duplicate headers
973 # prevent duplicate headers
974 del m[hdr]
974 del m[hdr]
975 change = True
975 change = True
976 if change:
976 if change:
977 m[hdr] = val
977 m[hdr] = val
978 if opts.get(b'test'):
978 if opts.get(b'test'):
979 ui.status(_(b'displaying '), subj, b' ...\n')
979 ui.status(_(b'displaying '), subj, b' ...\n')
980 ui.pager(b'email')
980 ui.pager(b'email')
981 generator = _bytesgenerator(ui, mangle_from_=False)
981 generator = _bytesgenerator(ui, mangle_from_=False)
982 try:
982 try:
983 generator.flatten(m, 0)
983 generator.flatten(m, 0)
984 ui.write(b'\n')
984 ui.write(b'\n')
985 except IOError as inst:
985 except IOError as inst:
986 if inst.errno != errno.EPIPE:
986 if inst.errno != errno.EPIPE:
987 raise
987 raise
988 else:
988 else:
989 if not sendmail:
989 if not sendmail:
990 sendmail = mail.connect(ui, mbox=mbox)
990 sendmail = mail.connect(ui, mbox=mbox)
991 ui.status(_(b'sending '), subj, b' ...\n')
991 ui.status(_(b'sending '), subj, b' ...\n')
992 progress.update(i, item=subj)
992 progress.update(i, item=subj)
993 if not mbox:
993 if not mbox:
994 # Exim does not remove the Bcc field
994 # Exim does not remove the Bcc field
995 del m[b'Bcc']
995 del m[b'Bcc']
996 fp = stringio()
996 fp = stringio()
997 generator = _bytesgenerator(fp, mangle_from_=False)
997 generator = _bytesgenerator(fp, mangle_from_=False)
998 generator.flatten(m, 0)
998 generator.flatten(m, 0)
999 alldests = to + bcc + cc
999 alldests = to + bcc + cc
1000 alldests = [encoding.strfromlocal(d) for d in alldests]
1000 alldests = [encoding.strfromlocal(d) for d in alldests]
1001 sendmail(sender_addr, alldests, fp.getvalue())
1001 sendmail(sender_addr, alldests, fp.getvalue())
1002
1002
1003 progress.complete()
1003 progress.complete()
@@ -1,762 +1,762 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
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 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import contextlib
110 import contextlib
111 import itertools
111 import itertools
112 import os
112 import os
113
113
114 from .i18n import _
114 from .i18n import _
115 from .node import (
115 from .node import (
116 hex,
116 hex,
117 short,
117 short,
118 )
118 )
119 from .thirdparty import attr
119 from .thirdparty import attr
120
120
121 from . import (
121 from . import (
122 error,
122 error,
123 pycompat,
123 pycompat,
124 templatefilters,
124 templatefilters,
125 templatekw,
125 templatekw,
126 templater,
126 templater,
127 templateutil,
127 templateutil,
128 util,
128 util,
129 )
129 )
130 from .utils import (
130 from .utils import (
131 cborutil,
131 cborutil,
132 dateutil,
132 dateutil,
133 stringutil,
133 stringutil,
134 )
134 )
135
135
136 pickle = util.pickle
136 pickle = util.pickle
137
137
138
138
139 class _nullconverter(object):
139 class _nullconverter(object):
140 '''convert non-primitive data types to be processed by formatter'''
140 '''convert non-primitive data types to be processed by formatter'''
141
141
142 # set to True if context object should be stored as item
142 # set to True if context object should be stored as item
143 storecontext = False
143 storecontext = False
144
144
145 @staticmethod
145 @staticmethod
146 def wrapnested(data, tmpl, sep):
146 def wrapnested(data, tmpl, sep):
147 '''wrap nested data by appropriate type'''
147 '''wrap nested data by appropriate type'''
148 return data
148 return data
149
149
150 @staticmethod
150 @staticmethod
151 def formatdate(date, fmt):
151 def formatdate(date, fmt):
152 '''convert date tuple to appropriate format'''
152 '''convert date tuple to appropriate format'''
153 # timestamp can be float, but the canonical form should be int
153 # timestamp can be float, but the canonical form should be int
154 ts, tz = date
154 ts, tz = date
155 return (int(ts), tz)
155 return (int(ts), tz)
156
156
157 @staticmethod
157 @staticmethod
158 def formatdict(data, key, value, fmt, sep):
158 def formatdict(data, key, value, fmt, sep):
159 '''convert dict or key-value pairs to appropriate dict format'''
159 '''convert dict or key-value pairs to appropriate dict format'''
160 # use plain dict instead of util.sortdict so that data can be
160 # use plain dict instead of util.sortdict so that data can be
161 # serialized as a builtin dict in pickle output
161 # serialized as a builtin dict in pickle output
162 return dict(data)
162 return dict(data)
163
163
164 @staticmethod
164 @staticmethod
165 def formatlist(data, name, fmt, sep):
165 def formatlist(data, name, fmt, sep):
166 '''convert iterable to appropriate list format'''
166 '''convert iterable to appropriate list format'''
167 return list(data)
167 return list(data)
168
168
169
169
170 class baseformatter(object):
170 class baseformatter(object):
171 def __init__(self, ui, topic, opts, converter):
171 def __init__(self, ui, topic, opts, converter):
172 self._ui = ui
172 self._ui = ui
173 self._topic = topic
173 self._topic = topic
174 self._opts = opts
174 self._opts = opts
175 self._converter = converter
175 self._converter = converter
176 self._item = None
176 self._item = None
177 # function to convert node to string suitable for this output
177 # function to convert node to string suitable for this output
178 self.hexfunc = hex
178 self.hexfunc = hex
179
179
180 def __enter__(self):
180 def __enter__(self):
181 return self
181 return self
182
182
183 def __exit__(self, exctype, excvalue, traceback):
183 def __exit__(self, exctype, excvalue, traceback):
184 if exctype is None:
184 if exctype is None:
185 self.end()
185 self.end()
186
186
187 def _showitem(self):
187 def _showitem(self):
188 '''show a formatted item once all data is collected'''
188 '''show a formatted item once all data is collected'''
189
189
190 def startitem(self):
190 def startitem(self):
191 '''begin an item in the format list'''
191 '''begin an item in the format list'''
192 if self._item is not None:
192 if self._item is not None:
193 self._showitem()
193 self._showitem()
194 self._item = {}
194 self._item = {}
195
195
196 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
196 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
197 '''convert date tuple to appropriate format'''
197 '''convert date tuple to appropriate format'''
198 return self._converter.formatdate(date, fmt)
198 return self._converter.formatdate(date, fmt)
199
199
200 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
200 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
201 '''convert dict or key-value pairs to appropriate dict format'''
201 '''convert dict or key-value pairs to appropriate dict format'''
202 return self._converter.formatdict(data, key, value, fmt, sep)
202 return self._converter.formatdict(data, key, value, fmt, sep)
203
203
204 def formatlist(self, data, name, fmt=None, sep=b' '):
204 def formatlist(self, data, name, fmt=None, sep=b' '):
205 '''convert iterable to appropriate list format'''
205 '''convert iterable to appropriate list format'''
206 # name is mandatory argument for now, but it could be optional if
206 # name is mandatory argument for now, but it could be optional if
207 # we have default template keyword, e.g. {item}
207 # we have default template keyword, e.g. {item}
208 return self._converter.formatlist(data, name, fmt, sep)
208 return self._converter.formatlist(data, name, fmt, sep)
209
209
210 def context(self, **ctxs):
210 def context(self, **ctxs):
211 '''insert context objects to be used to render template keywords'''
211 '''insert context objects to be used to render template keywords'''
212 ctxs = pycompat.byteskwargs(ctxs)
212 ctxs = pycompat.byteskwargs(ctxs)
213 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
213 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
214 if self._converter.storecontext:
214 if self._converter.storecontext:
215 # populate missing resources in fctx -> ctx -> repo order
215 # populate missing resources in fctx -> ctx -> repo order
216 if b'fctx' in ctxs and b'ctx' not in ctxs:
216 if b'fctx' in ctxs and b'ctx' not in ctxs:
217 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
217 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
218 if b'ctx' in ctxs and b'repo' not in ctxs:
218 if b'ctx' in ctxs and b'repo' not in ctxs:
219 ctxs[b'repo'] = ctxs[b'ctx'].repo()
219 ctxs[b'repo'] = ctxs[b'ctx'].repo()
220 self._item.update(ctxs)
220 self._item.update(ctxs)
221
221
222 def datahint(self):
222 def datahint(self):
223 '''set of field names to be referenced'''
223 '''set of field names to be referenced'''
224 return set()
224 return set()
225
225
226 def data(self, **data):
226 def data(self, **data):
227 '''insert data into item that's not shown in default output'''
227 '''insert data into item that's not shown in default output'''
228 data = pycompat.byteskwargs(data)
228 data = pycompat.byteskwargs(data)
229 self._item.update(data)
229 self._item.update(data)
230
230
231 def write(self, fields, deftext, *fielddata, **opts):
231 def write(self, fields, deftext, *fielddata, **opts):
232 '''do default text output while assigning data to item'''
232 '''do default text output while assigning data to item'''
233 fieldkeys = fields.split()
233 fieldkeys = fields.split()
234 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
234 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
235 self._item.update(zip(fieldkeys, fielddata))
235 self._item.update(zip(fieldkeys, fielddata))
236
236
237 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
237 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
238 '''do conditional write (primarily for plain formatter)'''
238 '''do conditional write (primarily for plain formatter)'''
239 fieldkeys = fields.split()
239 fieldkeys = fields.split()
240 assert len(fieldkeys) == len(fielddata)
240 assert len(fieldkeys) == len(fielddata)
241 self._item.update(zip(fieldkeys, fielddata))
241 self._item.update(zip(fieldkeys, fielddata))
242
242
243 def plain(self, text, **opts):
243 def plain(self, text, **opts):
244 '''show raw text for non-templated mode'''
244 '''show raw text for non-templated mode'''
245
245
246 def isplain(self):
246 def isplain(self):
247 '''check for plain formatter usage'''
247 '''check for plain formatter usage'''
248 return False
248 return False
249
249
250 def nested(self, field, tmpl=None, sep=b''):
250 def nested(self, field, tmpl=None, sep=b''):
251 '''sub formatter to store nested data in the specified field'''
251 '''sub formatter to store nested data in the specified field'''
252 data = []
252 data = []
253 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
253 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
254 return _nestedformatter(self._ui, self._converter, data)
254 return _nestedformatter(self._ui, self._converter, data)
255
255
256 def end(self):
256 def end(self):
257 '''end output for the formatter'''
257 '''end output for the formatter'''
258 if self._item is not None:
258 if self._item is not None:
259 self._showitem()
259 self._showitem()
260
260
261
261
262 def nullformatter(ui, topic, opts):
262 def nullformatter(ui, topic, opts):
263 '''formatter that prints nothing'''
263 '''formatter that prints nothing'''
264 return baseformatter(ui, topic, opts, converter=_nullconverter)
264 return baseformatter(ui, topic, opts, converter=_nullconverter)
265
265
266
266
267 class _nestedformatter(baseformatter):
267 class _nestedformatter(baseformatter):
268 '''build sub items and store them in the parent formatter'''
268 '''build sub items and store them in the parent formatter'''
269
269
270 def __init__(self, ui, converter, data):
270 def __init__(self, ui, converter, data):
271 baseformatter.__init__(
271 baseformatter.__init__(
272 self, ui, topic=b'', opts={}, converter=converter
272 self, ui, topic=b'', opts={}, converter=converter
273 )
273 )
274 self._data = data
274 self._data = data
275
275
276 def _showitem(self):
276 def _showitem(self):
277 self._data.append(self._item)
277 self._data.append(self._item)
278
278
279
279
280 def _iteritems(data):
280 def _iteritems(data):
281 '''iterate key-value pairs in stable order'''
281 '''iterate key-value pairs in stable order'''
282 if isinstance(data, dict):
282 if isinstance(data, dict):
283 return sorted(data.iteritems())
283 return sorted(data.iteritems())
284 return data
284 return data
285
285
286
286
287 class _plainconverter(object):
287 class _plainconverter(object):
288 '''convert non-primitive data types to text'''
288 '''convert non-primitive data types to text'''
289
289
290 storecontext = False
290 storecontext = False
291
291
292 @staticmethod
292 @staticmethod
293 def wrapnested(data, tmpl, sep):
293 def wrapnested(data, tmpl, sep):
294 raise error.ProgrammingError(b'plainformatter should never be nested')
294 raise error.ProgrammingError(b'plainformatter should never be nested')
295
295
296 @staticmethod
296 @staticmethod
297 def formatdate(date, fmt):
297 def formatdate(date, fmt):
298 '''stringify date tuple in the given format'''
298 '''stringify date tuple in the given format'''
299 return dateutil.datestr(date, fmt)
299 return dateutil.datestr(date, fmt)
300
300
301 @staticmethod
301 @staticmethod
302 def formatdict(data, key, value, fmt, sep):
302 def formatdict(data, key, value, fmt, sep):
303 '''stringify key-value pairs separated by sep'''
303 '''stringify key-value pairs separated by sep'''
304 prefmt = pycompat.identity
304 prefmt = pycompat.identity
305 if fmt is None:
305 if fmt is None:
306 fmt = b'%s=%s'
306 fmt = b'%s=%s'
307 prefmt = pycompat.bytestr
307 prefmt = pycompat.bytestr
308 return sep.join(
308 return sep.join(
309 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
309 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
310 )
310 )
311
311
312 @staticmethod
312 @staticmethod
313 def formatlist(data, name, fmt, sep):
313 def formatlist(data, name, fmt, sep):
314 '''stringify iterable separated by sep'''
314 '''stringify iterable separated by sep'''
315 prefmt = pycompat.identity
315 prefmt = pycompat.identity
316 if fmt is None:
316 if fmt is None:
317 fmt = b'%s'
317 fmt = b'%s'
318 prefmt = pycompat.bytestr
318 prefmt = pycompat.bytestr
319 return sep.join(fmt % prefmt(e) for e in data)
319 return sep.join(fmt % prefmt(e) for e in data)
320
320
321
321
322 class plainformatter(baseformatter):
322 class plainformatter(baseformatter):
323 '''the default text output scheme'''
323 '''the default text output scheme'''
324
324
325 def __init__(self, ui, out, topic, opts):
325 def __init__(self, ui, out, topic, opts):
326 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
326 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
327 if ui.debugflag:
327 if ui.debugflag:
328 self.hexfunc = hex
328 self.hexfunc = hex
329 else:
329 else:
330 self.hexfunc = short
330 self.hexfunc = short
331 if ui is out:
331 if ui is out:
332 self._write = ui.write
332 self._write = ui.write
333 else:
333 else:
334 self._write = lambda s, **opts: out.write(s)
334 self._write = lambda s, **opts: out.write(s)
335
335
336 def startitem(self):
336 def startitem(self):
337 pass
337 pass
338
338
339 def data(self, **data):
339 def data(self, **data):
340 pass
340 pass
341
341
342 def write(self, fields, deftext, *fielddata, **opts):
342 def write(self, fields, deftext, *fielddata, **opts):
343 self._write(deftext % fielddata, **opts)
343 self._write(deftext % fielddata, **opts)
344
344
345 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
345 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
346 '''do conditional write'''
346 '''do conditional write'''
347 if cond:
347 if cond:
348 self._write(deftext % fielddata, **opts)
348 self._write(deftext % fielddata, **opts)
349
349
350 def plain(self, text, **opts):
350 def plain(self, text, **opts):
351 self._write(text, **opts)
351 self._write(text, **opts)
352
352
353 def isplain(self):
353 def isplain(self):
354 return True
354 return True
355
355
356 def nested(self, field, tmpl=None, sep=b''):
356 def nested(self, field, tmpl=None, sep=b''):
357 # nested data will be directly written to ui
357 # nested data will be directly written to ui
358 return self
358 return self
359
359
360 def end(self):
360 def end(self):
361 pass
361 pass
362
362
363
363
364 class debugformatter(baseformatter):
364 class debugformatter(baseformatter):
365 def __init__(self, ui, out, topic, opts):
365 def __init__(self, ui, out, topic, opts):
366 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
366 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
367 self._out = out
367 self._out = out
368 self._out.write(b"%s = [\n" % self._topic)
368 self._out.write(b"%s = [\n" % self._topic)
369
369
370 def _showitem(self):
370 def _showitem(self):
371 self._out.write(
371 self._out.write(
372 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
372 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
373 )
373 )
374
374
375 def end(self):
375 def end(self):
376 baseformatter.end(self)
376 baseformatter.end(self)
377 self._out.write(b"]\n")
377 self._out.write(b"]\n")
378
378
379
379
380 class pickleformatter(baseformatter):
380 class pickleformatter(baseformatter):
381 def __init__(self, ui, out, topic, opts):
381 def __init__(self, ui, out, topic, opts):
382 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
382 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
383 self._out = out
383 self._out = out
384 self._data = []
384 self._data = []
385
385
386 def _showitem(self):
386 def _showitem(self):
387 self._data.append(self._item)
387 self._data.append(self._item)
388
388
389 def end(self):
389 def end(self):
390 baseformatter.end(self)
390 baseformatter.end(self)
391 self._out.write(pickle.dumps(self._data))
391 self._out.write(pickle.dumps(self._data))
392
392
393
393
394 class cborformatter(baseformatter):
394 class cborformatter(baseformatter):
395 '''serialize items as an indefinite-length CBOR array'''
395 '''serialize items as an indefinite-length CBOR array'''
396
396
397 def __init__(self, ui, out, topic, opts):
397 def __init__(self, ui, out, topic, opts):
398 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
398 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
399 self._out = out
399 self._out = out
400 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
400 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
401
401
402 def _showitem(self):
402 def _showitem(self):
403 self._out.write(b''.join(cborutil.streamencode(self._item)))
403 self._out.write(b''.join(cborutil.streamencode(self._item)))
404
404
405 def end(self):
405 def end(self):
406 baseformatter.end(self)
406 baseformatter.end(self)
407 self._out.write(cborutil.BREAK)
407 self._out.write(cborutil.BREAK)
408
408
409
409
410 class jsonformatter(baseformatter):
410 class jsonformatter(baseformatter):
411 def __init__(self, ui, out, topic, opts):
411 def __init__(self, ui, out, topic, opts):
412 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
412 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
413 self._out = out
413 self._out = out
414 self._out.write(b"[")
414 self._out.write(b"[")
415 self._first = True
415 self._first = True
416
416
417 def _showitem(self):
417 def _showitem(self):
418 if self._first:
418 if self._first:
419 self._first = False
419 self._first = False
420 else:
420 else:
421 self._out.write(b",")
421 self._out.write(b",")
422
422
423 self._out.write(b"\n {\n")
423 self._out.write(b"\n {\n")
424 first = True
424 first = True
425 for k, v in sorted(self._item.items()):
425 for k, v in sorted(self._item.items()):
426 if first:
426 if first:
427 first = False
427 first = False
428 else:
428 else:
429 self._out.write(b",\n")
429 self._out.write(b",\n")
430 u = templatefilters.json(v, paranoid=False)
430 u = templatefilters.json(v, paranoid=False)
431 self._out.write(b' "%s": %s' % (k, u))
431 self._out.write(b' "%s": %s' % (k, u))
432 self._out.write(b"\n }")
432 self._out.write(b"\n }")
433
433
434 def end(self):
434 def end(self):
435 baseformatter.end(self)
435 baseformatter.end(self)
436 self._out.write(b"\n]\n")
436 self._out.write(b"\n]\n")
437
437
438
438
439 class _templateconverter(object):
439 class _templateconverter(object):
440 '''convert non-primitive data types to be processed by templater'''
440 '''convert non-primitive data types to be processed by templater'''
441
441
442 storecontext = True
442 storecontext = True
443
443
444 @staticmethod
444 @staticmethod
445 def wrapnested(data, tmpl, sep):
445 def wrapnested(data, tmpl, sep):
446 '''wrap nested data by templatable type'''
446 '''wrap nested data by templatable type'''
447 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
447 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
448
448
449 @staticmethod
449 @staticmethod
450 def formatdate(date, fmt):
450 def formatdate(date, fmt):
451 '''return date tuple'''
451 '''return date tuple'''
452 return templateutil.date(date)
452 return templateutil.date(date)
453
453
454 @staticmethod
454 @staticmethod
455 def formatdict(data, key, value, fmt, sep):
455 def formatdict(data, key, value, fmt, sep):
456 '''build object that can be evaluated as either plain string or dict'''
456 '''build object that can be evaluated as either plain string or dict'''
457 data = util.sortdict(_iteritems(data))
457 data = util.sortdict(_iteritems(data))
458
458
459 def f():
459 def f():
460 yield _plainconverter.formatdict(data, key, value, fmt, sep)
460 yield _plainconverter.formatdict(data, key, value, fmt, sep)
461
461
462 return templateutil.hybriddict(
462 return templateutil.hybriddict(
463 data, key=key, value=value, fmt=fmt, gen=f
463 data, key=key, value=value, fmt=fmt, gen=f
464 )
464 )
465
465
466 @staticmethod
466 @staticmethod
467 def formatlist(data, name, fmt, sep):
467 def formatlist(data, name, fmt, sep):
468 '''build object that can be evaluated as either plain string or list'''
468 '''build object that can be evaluated as either plain string or list'''
469 data = list(data)
469 data = list(data)
470
470
471 def f():
471 def f():
472 yield _plainconverter.formatlist(data, name, fmt, sep)
472 yield _plainconverter.formatlist(data, name, fmt, sep)
473
473
474 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
474 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
475
475
476
476
477 class templateformatter(baseformatter):
477 class templateformatter(baseformatter):
478 def __init__(self, ui, out, topic, opts):
478 def __init__(self, ui, out, topic, opts, spec):
479 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
479 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
480 self._out = out
480 self._out = out
481 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
482 self._tref = spec.ref
481 self._tref = spec.ref
483 self._t = loadtemplater(
482 self._t = loadtemplater(
484 ui,
483 ui,
485 spec,
484 spec,
486 defaults=templatekw.keywords,
485 defaults=templatekw.keywords,
487 resources=templateresources(ui),
486 resources=templateresources(ui),
488 cache=templatekw.defaulttempl,
487 cache=templatekw.defaulttempl,
489 )
488 )
490 self._parts = templatepartsmap(
489 self._parts = templatepartsmap(
491 spec, self._t, [b'docheader', b'docfooter', b'separator']
490 spec, self._t, [b'docheader', b'docfooter', b'separator']
492 )
491 )
493 self._counter = itertools.count()
492 self._counter = itertools.count()
494 self._renderitem(b'docheader', {})
493 self._renderitem(b'docheader', {})
495
494
496 def _showitem(self):
495 def _showitem(self):
497 item = self._item.copy()
496 item = self._item.copy()
498 item[b'index'] = index = next(self._counter)
497 item[b'index'] = index = next(self._counter)
499 if index > 0:
498 if index > 0:
500 self._renderitem(b'separator', {})
499 self._renderitem(b'separator', {})
501 self._renderitem(self._tref, item)
500 self._renderitem(self._tref, item)
502
501
503 def _renderitem(self, part, item):
502 def _renderitem(self, part, item):
504 if part not in self._parts:
503 if part not in self._parts:
505 return
504 return
506 ref = self._parts[part]
505 ref = self._parts[part]
507 self._out.write(self._t.render(ref, item))
506 self._out.write(self._t.render(ref, item))
508
507
509 @util.propertycache
508 @util.propertycache
510 def _symbolsused(self):
509 def _symbolsused(self):
511 return self._t.symbolsused(self._tref)
510 return self._t.symbolsused(self._tref)
512
511
513 def datahint(self):
512 def datahint(self):
514 '''set of field names to be referenced from the template'''
513 '''set of field names to be referenced from the template'''
515 return self._symbolsused[0]
514 return self._symbolsused[0]
516
515
517 def end(self):
516 def end(self):
518 baseformatter.end(self)
517 baseformatter.end(self)
519 self._renderitem(b'docfooter', {})
518 self._renderitem(b'docfooter', {})
520
519
521
520
522 @attr.s(frozen=True)
521 @attr.s(frozen=True)
523 class templatespec(object):
522 class templatespec(object):
524 ref = attr.ib()
523 ref = attr.ib()
525 tmpl = attr.ib()
524 tmpl = attr.ib()
526 mapfile = attr.ib()
525 mapfile = attr.ib()
527
526
528
527
529 def lookuptemplate(ui, topic, tmpl):
528 def lookuptemplate(ui, topic, tmpl):
530 """Find the template matching the given -T/--template spec 'tmpl'
529 """Find the template matching the given -T/--template spec 'tmpl'
531
530
532 'tmpl' can be any of the following:
531 'tmpl' can be any of the following:
533
532
534 - a literal template (e.g. '{rev}')
533 - a literal template (e.g. '{rev}')
535 - a map-file name or path (e.g. 'changelog')
534 - a map-file name or path (e.g. 'changelog')
536 - a reference to [templates] in config file
535 - a reference to [templates] in config file
537 - a path to raw template file
536 - a path to raw template file
538
537
539 A map file defines a stand-alone template environment. If a map file
538 A map file defines a stand-alone template environment. If a map file
540 selected, all templates defined in the file will be loaded, and the
539 selected, all templates defined in the file will be loaded, and the
541 template matching the given topic will be rendered. Aliases won't be
540 template matching the given topic will be rendered. Aliases won't be
542 loaded from user config, but from the map file.
541 loaded from user config, but from the map file.
543
542
544 If no map file selected, all templates in [templates] section will be
543 If no map file selected, all templates in [templates] section will be
545 available as well as aliases in [templatealias].
544 available as well as aliases in [templatealias].
546 """
545 """
547
546
548 # looks like a literal template?
547 # looks like a literal template?
549 if b'{' in tmpl:
548 if b'{' in tmpl:
550 return templatespec(b'', tmpl, None)
549 return templatespec(b'', tmpl, None)
551
550
552 # perhaps a stock style?
551 # perhaps a stock style?
553 if not os.path.split(tmpl)[0]:
552 if not os.path.split(tmpl)[0]:
554 mapname = templater.templatepath(
553 mapname = templater.templatepath(
555 b'map-cmdline.' + tmpl
554 b'map-cmdline.' + tmpl
556 ) or templater.templatepath(tmpl)
555 ) or templater.templatepath(tmpl)
557 if mapname and os.path.isfile(mapname):
556 if mapname and os.path.isfile(mapname):
558 return templatespec(topic, None, mapname)
557 return templatespec(topic, None, mapname)
559
558
560 # perhaps it's a reference to [templates]
559 # perhaps it's a reference to [templates]
561 if ui.config(b'templates', tmpl):
560 if ui.config(b'templates', tmpl):
562 return templatespec(tmpl, None, None)
561 return templatespec(tmpl, None, None)
563
562
564 if tmpl == b'list':
563 if tmpl == b'list':
565 ui.write(_(b"available styles: %s\n") % templater.stylelist())
564 ui.write(_(b"available styles: %s\n") % templater.stylelist())
566 raise error.Abort(_(b"specify a template"))
565 raise error.Abort(_(b"specify a template"))
567
566
568 # perhaps it's a path to a map or a template
567 # perhaps it's a path to a map or a template
569 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
568 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
570 # is it a mapfile for a style?
569 # is it a mapfile for a style?
571 if os.path.basename(tmpl).startswith(b"map-"):
570 if os.path.basename(tmpl).startswith(b"map-"):
572 return templatespec(topic, None, os.path.realpath(tmpl))
571 return templatespec(topic, None, os.path.realpath(tmpl))
573 with util.posixfile(tmpl, b'rb') as f:
572 with util.posixfile(tmpl, b'rb') as f:
574 tmpl = f.read()
573 tmpl = f.read()
575 return templatespec(b'', tmpl, None)
574 return templatespec(b'', tmpl, None)
576
575
577 # constant string?
576 # constant string?
578 return templatespec(b'', tmpl, None)
577 return templatespec(b'', tmpl, None)
579
578
580
579
581 def templatepartsmap(spec, t, partnames):
580 def templatepartsmap(spec, t, partnames):
582 """Create a mapping of {part: ref}"""
581 """Create a mapping of {part: ref}"""
583 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
582 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
584 if spec.mapfile:
583 if spec.mapfile:
585 partsmap.update((p, p) for p in partnames if p in t)
584 partsmap.update((p, p) for p in partnames if p in t)
586 elif spec.ref:
585 elif spec.ref:
587 for part in partnames:
586 for part in partnames:
588 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
587 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
589 if ref in t:
588 if ref in t:
590 partsmap[part] = ref
589 partsmap[part] = ref
591 return partsmap
590 return partsmap
592
591
593
592
594 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
593 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
595 """Create a templater from either a literal template or loading from
594 """Create a templater from either a literal template or loading from
596 a map file"""
595 a map file"""
597 assert not (spec.tmpl and spec.mapfile)
596 assert not (spec.tmpl and spec.mapfile)
598 if spec.mapfile:
597 if spec.mapfile:
599 frommapfile = templater.templater.frommapfile
598 frommapfile = templater.templater.frommapfile
600 return frommapfile(
599 return frommapfile(
601 spec.mapfile, defaults=defaults, resources=resources, cache=cache
600 spec.mapfile, defaults=defaults, resources=resources, cache=cache
602 )
601 )
603 return maketemplater(
602 return maketemplater(
604 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
603 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
605 )
604 )
606
605
607
606
608 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
607 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
609 """Create a templater from a string template 'tmpl'"""
608 """Create a templater from a string template 'tmpl'"""
610 aliases = ui.configitems(b'templatealias')
609 aliases = ui.configitems(b'templatealias')
611 t = templater.templater(
610 t = templater.templater(
612 defaults=defaults, resources=resources, cache=cache, aliases=aliases
611 defaults=defaults, resources=resources, cache=cache, aliases=aliases
613 )
612 )
614 t.cache.update(
613 t.cache.update(
615 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
614 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
616 )
615 )
617 if tmpl:
616 if tmpl:
618 t.cache[b''] = tmpl
617 t.cache[b''] = tmpl
619 return t
618 return t
620
619
621
620
622 # marker to denote a resource to be loaded on demand based on mapping values
621 # marker to denote a resource to be loaded on demand based on mapping values
623 # (e.g. (ctx, path) -> fctx)
622 # (e.g. (ctx, path) -> fctx)
624 _placeholder = object()
623 _placeholder = object()
625
624
626
625
627 class templateresources(templater.resourcemapper):
626 class templateresources(templater.resourcemapper):
628 """Resource mapper designed for the default templatekw and function"""
627 """Resource mapper designed for the default templatekw and function"""
629
628
630 def __init__(self, ui, repo=None):
629 def __init__(self, ui, repo=None):
631 self._resmap = {
630 self._resmap = {
632 b'cache': {}, # for templatekw/funcs to store reusable data
631 b'cache': {}, # for templatekw/funcs to store reusable data
633 b'repo': repo,
632 b'repo': repo,
634 b'ui': ui,
633 b'ui': ui,
635 }
634 }
636
635
637 def availablekeys(self, mapping):
636 def availablekeys(self, mapping):
638 return {
637 return {
639 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
638 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
640 }
639 }
641
640
642 def knownkeys(self):
641 def knownkeys(self):
643 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
642 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
644
643
645 def lookup(self, mapping, key):
644 def lookup(self, mapping, key):
646 if key not in self.knownkeys():
645 if key not in self.knownkeys():
647 return None
646 return None
648 v = self._getsome(mapping, key)
647 v = self._getsome(mapping, key)
649 if v is _placeholder:
648 if v is _placeholder:
650 v = mapping[key] = self._loadermap[key](self, mapping)
649 v = mapping[key] = self._loadermap[key](self, mapping)
651 return v
650 return v
652
651
653 def populatemap(self, context, origmapping, newmapping):
652 def populatemap(self, context, origmapping, newmapping):
654 mapping = {}
653 mapping = {}
655 if self._hasnodespec(newmapping):
654 if self._hasnodespec(newmapping):
656 mapping[b'revcache'] = {} # per-ctx cache
655 mapping[b'revcache'] = {} # per-ctx cache
657 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
656 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
658 orignode = templateutil.runsymbol(context, origmapping, b'node')
657 orignode = templateutil.runsymbol(context, origmapping, b'node')
659 mapping[b'originalnode'] = orignode
658 mapping[b'originalnode'] = orignode
660 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
659 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
661 # its existence to be reported by availablekeys()
660 # its existence to be reported by availablekeys()
662 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
661 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
663 mapping[b'ctx'] = _placeholder
662 mapping[b'ctx'] = _placeholder
664 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
663 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
665 mapping[b'fctx'] = _placeholder
664 mapping[b'fctx'] = _placeholder
666 return mapping
665 return mapping
667
666
668 def _getsome(self, mapping, key):
667 def _getsome(self, mapping, key):
669 v = mapping.get(key)
668 v = mapping.get(key)
670 if v is not None:
669 if v is not None:
671 return v
670 return v
672 return self._resmap.get(key)
671 return self._resmap.get(key)
673
672
674 def _hasliteral(self, mapping, key):
673 def _hasliteral(self, mapping, key):
675 """Test if a literal value is set or unset in the given mapping"""
674 """Test if a literal value is set or unset in the given mapping"""
676 return key in mapping and not callable(mapping[key])
675 return key in mapping and not callable(mapping[key])
677
676
678 def _getliteral(self, mapping, key):
677 def _getliteral(self, mapping, key):
679 """Return value of the given name if it is a literal"""
678 """Return value of the given name if it is a literal"""
680 v = mapping.get(key)
679 v = mapping.get(key)
681 if callable(v):
680 if callable(v):
682 return None
681 return None
683 return v
682 return v
684
683
685 def _hasnodespec(self, mapping):
684 def _hasnodespec(self, mapping):
686 """Test if context revision is set or unset in the given mapping"""
685 """Test if context revision is set or unset in the given mapping"""
687 return b'node' in mapping or b'ctx' in mapping
686 return b'node' in mapping or b'ctx' in mapping
688
687
689 def _loadctx(self, mapping):
688 def _loadctx(self, mapping):
690 repo = self._getsome(mapping, b'repo')
689 repo = self._getsome(mapping, b'repo')
691 node = self._getliteral(mapping, b'node')
690 node = self._getliteral(mapping, b'node')
692 if repo is None or node is None:
691 if repo is None or node is None:
693 return
692 return
694 try:
693 try:
695 return repo[node]
694 return repo[node]
696 except error.RepoLookupError:
695 except error.RepoLookupError:
697 return None # maybe hidden/non-existent node
696 return None # maybe hidden/non-existent node
698
697
699 def _loadfctx(self, mapping):
698 def _loadfctx(self, mapping):
700 ctx = self._getsome(mapping, b'ctx')
699 ctx = self._getsome(mapping, b'ctx')
701 path = self._getliteral(mapping, b'path')
700 path = self._getliteral(mapping, b'path')
702 if ctx is None or path is None:
701 if ctx is None or path is None:
703 return None
702 return None
704 try:
703 try:
705 return ctx[path]
704 return ctx[path]
706 except error.LookupError:
705 except error.LookupError:
707 return None # maybe removed file?
706 return None # maybe removed file?
708
707
709 _loadermap = {
708 _loadermap = {
710 b'ctx': _loadctx,
709 b'ctx': _loadctx,
711 b'fctx': _loadfctx,
710 b'fctx': _loadfctx,
712 }
711 }
713
712
714
713
715 def formatter(ui, out, topic, opts):
714 def formatter(ui, out, topic, opts):
716 template = opts.get(b"template", b"")
715 template = opts.get(b"template", b"")
717 if template == b"cbor":
716 if template == b"cbor":
718 return cborformatter(ui, out, topic, opts)
717 return cborformatter(ui, out, topic, opts)
719 elif template == b"json":
718 elif template == b"json":
720 return jsonformatter(ui, out, topic, opts)
719 return jsonformatter(ui, out, topic, opts)
721 elif template == b"pickle":
720 elif template == b"pickle":
722 return pickleformatter(ui, out, topic, opts)
721 return pickleformatter(ui, out, topic, opts)
723 elif template == b"debug":
722 elif template == b"debug":
724 return debugformatter(ui, out, topic, opts)
723 return debugformatter(ui, out, topic, opts)
725 elif template != b"":
724 elif template != b"":
726 return templateformatter(ui, out, topic, opts)
725 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
726 return templateformatter(ui, out, topic, opts, spec)
727 # developer config: ui.formatdebug
727 # developer config: ui.formatdebug
728 elif ui.configbool(b'ui', b'formatdebug'):
728 elif ui.configbool(b'ui', b'formatdebug'):
729 return debugformatter(ui, out, topic, opts)
729 return debugformatter(ui, out, topic, opts)
730 # deprecated config: ui.formatjson
730 # deprecated config: ui.formatjson
731 elif ui.configbool(b'ui', b'formatjson'):
731 elif ui.configbool(b'ui', b'formatjson'):
732 return jsonformatter(ui, out, topic, opts)
732 return jsonformatter(ui, out, topic, opts)
733 return plainformatter(ui, out, topic, opts)
733 return plainformatter(ui, out, topic, opts)
734
734
735
735
736 @contextlib.contextmanager
736 @contextlib.contextmanager
737 def openformatter(ui, filename, topic, opts):
737 def openformatter(ui, filename, topic, opts):
738 """Create a formatter that writes outputs to the specified file
738 """Create a formatter that writes outputs to the specified file
739
739
740 Must be invoked using the 'with' statement.
740 Must be invoked using the 'with' statement.
741 """
741 """
742 with util.posixfile(filename, b'wb') as out:
742 with util.posixfile(filename, b'wb') as out:
743 with formatter(ui, out, topic, opts) as fm:
743 with formatter(ui, out, topic, opts) as fm:
744 yield fm
744 yield fm
745
745
746
746
747 @contextlib.contextmanager
747 @contextlib.contextmanager
748 def _neverending(fm):
748 def _neverending(fm):
749 yield fm
749 yield fm
750
750
751
751
752 def maybereopen(fm, filename):
752 def maybereopen(fm, filename):
753 """Create a formatter backed by file if filename specified, else return
753 """Create a formatter backed by file if filename specified, else return
754 the given formatter
754 the given formatter
755
755
756 Must be invoked using the 'with' statement. This will never call fm.end()
756 Must be invoked using the 'with' statement. This will never call fm.end()
757 of the given formatter.
757 of the given formatter.
758 """
758 """
759 if filename:
759 if filename:
760 return openformatter(fm._ui, filename, fm._topic, fm._opts)
760 return openformatter(fm._ui, filename, fm._topic, fm._opts)
761 else:
761 else:
762 return _neverending(fm)
762 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now