##// END OF EJS Templates
templater: split template functions to new module...
Yuya Nishihara -
r36940:521f6c7e default
parent child Browse files
Show More
@@ -1,312 +1,313
1 1 # If you want to change PREFIX, do not just edit it below. The changed
2 2 # value wont get passed on to recursive make calls. You should instead
3 3 # override the variable on the command like:
4 4 #
5 5 # % make PREFIX=/opt/ install
6 6
7 7 export PREFIX=/usr/local
8 8 PYTHON=python
9 9 $(eval HGROOT := $(shell pwd))
10 10 HGPYTHONS ?= $(HGROOT)/build/pythons
11 11 PURE=
12 12 PYFILES:=$(shell find mercurial hgext doc -name '*.py')
13 13 DOCFILES=mercurial/help/*.txt
14 14 export LANGUAGE=C
15 15 export LC_ALL=C
16 16 TESTFLAGS ?= $(shell echo $$HGTESTFLAGS)
17 17 OSXVERSIONFLAGS ?= $(shell echo $$OSXVERSIONFLAGS)
18 18
19 19 # Set this to e.g. "mingw32" to use a non-default compiler.
20 20 COMPILER=
21 21
22 22 COMPILERFLAG_tmp_ =
23 23 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
24 24 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
25 25
26 26 help:
27 27 @echo 'Commonly used make targets:'
28 28 @echo ' all - build program and documentation'
29 29 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
30 30 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
31 31 @echo ' local - build for inplace usage'
32 32 @echo ' tests - run all tests in the automatic test suite'
33 33 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
34 34 @echo ' dist - run all tests and create a source tarball in dist/'
35 35 @echo ' clean - remove files created by other targets'
36 36 @echo ' (except installed files or dist source tarball)'
37 37 @echo ' update-pot - update i18n/hg.pot'
38 38 @echo
39 39 @echo 'Example for a system-wide installation under /usr/local:'
40 40 @echo ' make all && su -c "make install" && hg version'
41 41 @echo
42 42 @echo 'Example for a local installation (usable in this directory):'
43 43 @echo ' make local && ./hg version'
44 44
45 45 all: build doc
46 46
47 47 local:
48 48 $(PYTHON) setup.py $(PURE) \
49 49 build_py -c -d . \
50 50 build_ext $(COMPILERFLAG) -i \
51 51 build_hgexe $(COMPILERFLAG) -i \
52 52 build_mo
53 53 env HGRCPATH= $(PYTHON) hg version
54 54
55 55 build:
56 56 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
57 57
58 58 wheel:
59 59 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
60 60
61 61 doc:
62 62 $(MAKE) -C doc
63 63
64 64 cleanbutpackages:
65 65 -$(PYTHON) setup.py clean --all # ignore errors from this command
66 66 find contrib doc hgext hgext3rd i18n mercurial tests hgdemandimport \
67 67 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
68 68 rm -f MANIFEST MANIFEST.in hgext/__index__.py tests/*.err
69 69 rm -f mercurial/__modulepolicy__.py
70 70 if test -d .hg; then rm -f mercurial/__version__.py; fi
71 71 rm -rf build mercurial/locale
72 72 $(MAKE) -C doc clean
73 73 $(MAKE) -C contrib/chg distclean
74 74
75 75 clean: cleanbutpackages
76 76 rm -rf packages
77 77
78 78 install: install-bin install-doc
79 79
80 80 install-bin: build
81 81 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
82 82
83 83 install-doc: doc
84 84 cd doc && $(MAKE) $(MFLAGS) install
85 85
86 86 install-home: install-home-bin install-home-doc
87 87
88 88 install-home-bin: build
89 89 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
90 90
91 91 install-home-doc: doc
92 92 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
93 93
94 94 MANIFEST-doc:
95 95 $(MAKE) -C doc MANIFEST
96 96
97 97 MANIFEST.in: MANIFEST-doc
98 98 hg manifest | sed -e 's/^/include /' > MANIFEST.in
99 99 echo include mercurial/__version__.py >> MANIFEST.in
100 100 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
101 101
102 102 dist: tests dist-notests
103 103
104 104 dist-notests: doc MANIFEST.in
105 105 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
106 106
107 107 check: tests
108 108
109 109 tests:
110 110 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
111 111
112 112 test-%:
113 113 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
114 114
115 115 testpy-%:
116 116 @echo Looking for Python $* in $(HGPYTHONS)
117 117 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
118 118 cd $$(mktemp --directory --tmpdir) && \
119 119 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
120 120 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
121 121
122 122 check-code:
123 123 hg manifest | xargs python contrib/check-code.py
124 124
125 125 format-c:
126 126 clang-format --style file -i \
127 127 `hg files 'set:(**.c or **.cc or **.h) and not "listfile:contrib/clang-format-blacklist"'`
128 128
129 129 update-pot: i18n/hg.pot
130 130
131 131 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
132 132 $(PYTHON) i18n/hggettext mercurial/commands.py \
133 133 hgext/*.py hgext/*/__init__.py \
134 134 mercurial/fileset.py mercurial/revset.py \
135 mercurial/templatefilters.py mercurial/templatekw.py \
136 mercurial/templater.py \
135 mercurial/templatefilters.py \
136 mercurial/templatefuncs.py \
137 mercurial/templatekw.py \
137 138 mercurial/filemerge.py \
138 139 mercurial/hgweb/webcommands.py \
139 140 mercurial/util.py \
140 141 $(DOCFILES) > i18n/hg.pot.tmp
141 142 # All strings marked for translation in Mercurial contain
142 143 # ASCII characters only. But some files contain string
143 144 # literals like this '\037\213'. xgettext thinks it has to
144 145 # parse them even though they are not marked for translation.
145 146 # Extracting with an explicit encoding of ISO-8859-1 will make
146 147 # xgettext "parse" and ignore them.
147 148 echo $(PYFILES) | xargs \
148 149 xgettext --package-name "Mercurial" \
149 150 --msgid-bugs-address "<mercurial-devel@mercurial-scm.org>" \
150 151 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
151 152 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
152 153 -d hg -p i18n -o hg.pot.tmp
153 154 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
154 155 # The target file is not created before the last step. So it never is in
155 156 # an intermediate state.
156 157 mv -f i18n/hg.pot.tmp i18n/hg.pot
157 158
158 159 %.po: i18n/hg.pot
159 160 # work on a temporary copy for never having a half completed target
160 161 cp $@ $@.tmp
161 162 msgmerge --no-location --update $@.tmp $^
162 163 mv -f $@.tmp $@
163 164
164 165 # Packaging targets
165 166
166 167 osx:
167 168 rm -rf build/mercurial
168 169 /usr/bin/python2.7 setup.py install --optimize=1 \
169 170 --root=build/mercurial/ --prefix=/usr/local/ \
170 171 --install-lib=/Library/Python/2.7/site-packages/
171 172 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
172 173 # Place a bogon .DS_Store file in the target dir so we can be
173 174 # sure it doesn't get included in the final package.
174 175 touch build/mercurial/.DS_Store
175 176 # install zsh completions - this location appears to be
176 177 # searched by default as of macOS Sierra.
177 178 install -d build/mercurial/usr/local/share/zsh/site-functions/
178 179 install -m 0644 contrib/zsh_completion build/mercurial/usr/local/share/zsh/site-functions/_hg
179 180 # install bash completions - there doesn't appear to be a
180 181 # place that's searched by default for bash, so we'll follow
181 182 # the lead of Apple's git install and just put it in a
182 183 # location of our own.
183 184 install -d build/mercurial/usr/local/hg/contrib/
184 185 install -m 0644 contrib/bash_completion build/mercurial/usr/local/hg/contrib/hg-completion.bash
185 186 make -C contrib/chg \
186 187 HGPATH=/usr/local/bin/hg \
187 188 PYTHON=/usr/bin/python2.7 \
188 189 HGEXTDIR=/Library/Python/2.7/site-packages/hgext \
189 190 DESTDIR=../../build/mercurial \
190 191 PREFIX=/usr/local \
191 192 clean install
192 193 mkdir -p $${OUTPUTDIR:-dist}
193 194 HGVER=$$(python contrib/genosxversion.py $(OSXVERSIONFLAGS) build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py) && \
194 195 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
195 196 pkgbuild --filter \\.DS_Store --root build/mercurial/ \
196 197 --identifier org.mercurial-scm.mercurial \
197 198 --version "$${HGVER}" \
198 199 build/mercurial.pkg && \
199 200 productbuild --distribution contrib/macosx/distribution.xml \
200 201 --package-path build/ \
201 202 --version "$${HGVER}" \
202 203 --resources contrib/macosx/ \
203 204 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
204 205
205 206 deb:
206 207 contrib/builddeb
207 208
208 209 ppa:
209 210 contrib/builddeb --source-only
210 211
211 212 contrib/docker/debian-%: contrib/docker/debian.template
212 213 sed "s/__CODENAME__/$*/" $< > $@
213 214
214 215 docker-debian-jessie: contrib/docker/debian-jessie
215 216 mkdir -p packages/debian-jessie
216 217 contrib/dockerdeb debian jessie
217 218
218 219 docker-debian-stretch: contrib/docker/debian-stretch
219 220 mkdir -p packages/debian-stretch
220 221 contrib/dockerdeb debian stretch
221 222
222 223 contrib/docker/ubuntu-%: contrib/docker/ubuntu.template
223 224 sed "s/__CODENAME__/$*/" $< > $@
224 225
225 226 docker-ubuntu-trusty: contrib/docker/ubuntu-trusty
226 227 contrib/dockerdeb ubuntu trusty
227 228
228 229 docker-ubuntu-trusty-ppa: contrib/docker/ubuntu-trusty
229 230 contrib/dockerdeb ubuntu trusty --source-only
230 231
231 232 docker-ubuntu-xenial: contrib/docker/ubuntu-xenial
232 233 contrib/dockerdeb ubuntu xenial
233 234
234 235 docker-ubuntu-xenial-ppa: contrib/docker/ubuntu-xenial
235 236 contrib/dockerdeb ubuntu xenial --source-only
236 237
237 238 docker-ubuntu-artful: contrib/docker/ubuntu-artful
238 239 contrib/dockerdeb ubuntu artful
239 240
240 241 docker-ubuntu-artful-ppa: contrib/docker/ubuntu-artful
241 242 contrib/dockerdeb ubuntu artful --source-only
242 243
243 244 fedora20:
244 245 mkdir -p packages/fedora20
245 246 contrib/buildrpm
246 247 cp rpmbuild/RPMS/*/* packages/fedora20
247 248 cp rpmbuild/SRPMS/* packages/fedora20
248 249 rm -rf rpmbuild
249 250
250 251 docker-fedora20:
251 252 mkdir -p packages/fedora20
252 253 contrib/dockerrpm fedora20
253 254
254 255 fedora21:
255 256 mkdir -p packages/fedora21
256 257 contrib/buildrpm
257 258 cp rpmbuild/RPMS/*/* packages/fedora21
258 259 cp rpmbuild/SRPMS/* packages/fedora21
259 260 rm -rf rpmbuild
260 261
261 262 docker-fedora21:
262 263 mkdir -p packages/fedora21
263 264 contrib/dockerrpm fedora21
264 265
265 266 centos5:
266 267 mkdir -p packages/centos5
267 268 contrib/buildrpm --withpython
268 269 cp rpmbuild/RPMS/*/* packages/centos5
269 270 cp rpmbuild/SRPMS/* packages/centos5
270 271
271 272 docker-centos5:
272 273 mkdir -p packages/centos5
273 274 contrib/dockerrpm centos5 --withpython
274 275
275 276 centos6:
276 277 mkdir -p packages/centos6
277 278 contrib/buildrpm --withpython
278 279 cp rpmbuild/RPMS/*/* packages/centos6
279 280 cp rpmbuild/SRPMS/* packages/centos6
280 281
281 282 docker-centos6:
282 283 mkdir -p packages/centos6
283 284 contrib/dockerrpm centos6 --withpython
284 285
285 286 centos7:
286 287 mkdir -p packages/centos7
287 288 contrib/buildrpm
288 289 cp rpmbuild/RPMS/*/* packages/centos7
289 290 cp rpmbuild/SRPMS/* packages/centos7
290 291
291 292 docker-centos7:
292 293 mkdir -p packages/centos7
293 294 contrib/dockerrpm centos7
294 295
295 296 linux-wheels: linux-wheels-x86_64 linux-wheels-i686
296 297
297 298 linux-wheels-x86_64:
298 299 docker run -e "HGTEST_JOBS=$(shell nproc)" --rm -ti -v `pwd`:/src quay.io/pypa/manylinux1_x86_64 /src/contrib/build-linux-wheels.sh
299 300
300 301 linux-wheels-i686:
301 302 docker run -e "HGTEST_JOBS=$(shell nproc)" --rm -ti -v `pwd`:/src quay.io/pypa/manylinux1_i686 linux32 /src/contrib/build-linux-wheels.sh
302 303
303 304 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
304 305 install-doc install-home install-home-bin install-home-doc \
305 306 dist dist-notests check tests check-code format-c update-pot \
306 307 osx deb ppa docker-debian-jessie docker-debian-stretch \
307 308 docker-ubuntu-trusty docker-ubuntu-trusty-ppa \
308 309 docker-ubuntu-xenial docker-ubuntu-xenial-ppa \
309 310 docker-ubuntu-artful docker-ubuntu-artful-ppa \
310 311 fedora20 docker-fedora20 fedora21 docker-fedora21 \
311 312 centos5 docker-centos5 centos6 docker-centos6 centos7 docker-centos7 \
312 313 linux-wheels
@@ -1,733 +1,733
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import functools
11 11 import imp
12 12 import inspect
13 13 import os
14 14
15 15 from .i18n import (
16 16 _,
17 17 gettext,
18 18 )
19 19
20 20 from . import (
21 21 cmdutil,
22 22 configitems,
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 _extensions = {}
29 29 _disabledextensions = {}
30 30 _aftercallbacks = {}
31 31 _order = []
32 32 _builtin = {
33 33 'hbisect',
34 34 'bookmarks',
35 35 'color',
36 36 'parentrevspec',
37 37 'progress',
38 38 'interhg',
39 39 'inotify',
40 40 'hgcia'
41 41 }
42 42
43 43 def extensions(ui=None):
44 44 if ui:
45 45 def enabled(name):
46 46 for format in ['%s', 'hgext.%s']:
47 47 conf = ui.config('extensions', format % name)
48 48 if conf is not None and not conf.startswith('!'):
49 49 return True
50 50 else:
51 51 enabled = lambda name: True
52 52 for name in _order:
53 53 module = _extensions[name]
54 54 if module and enabled(name):
55 55 yield name, module
56 56
57 57 def find(name):
58 58 '''return module with given extension name'''
59 59 mod = None
60 60 try:
61 61 mod = _extensions[name]
62 62 except KeyError:
63 63 for k, v in _extensions.iteritems():
64 64 if k.endswith('.' + name) or k.endswith('/' + name):
65 65 mod = v
66 66 break
67 67 if not mod:
68 68 raise KeyError(name)
69 69 return mod
70 70
71 71 def loadpath(path, module_name):
72 72 module_name = module_name.replace('.', '_')
73 73 path = util.normpath(util.expandpath(path))
74 74 module_name = pycompat.fsdecode(module_name)
75 75 path = pycompat.fsdecode(path)
76 76 if os.path.isdir(path):
77 77 # module/__init__.py style
78 78 d, f = os.path.split(path)
79 79 fd, fpath, desc = imp.find_module(f, [d])
80 80 return imp.load_module(module_name, fd, fpath, desc)
81 81 else:
82 82 try:
83 83 return imp.load_source(module_name, path)
84 84 except IOError as exc:
85 85 if not exc.filename:
86 86 exc.filename = path # python does not fill this
87 87 raise
88 88
89 89 def _importh(name):
90 90 """import and return the <name> module"""
91 91 mod = __import__(pycompat.sysstr(name))
92 92 components = name.split('.')
93 93 for comp in components[1:]:
94 94 mod = getattr(mod, comp)
95 95 return mod
96 96
97 97 def _importext(name, path=None, reportfunc=None):
98 98 if path:
99 99 # the module will be loaded in sys.modules
100 100 # choose an unique name so that it doesn't
101 101 # conflicts with other modules
102 102 mod = loadpath(path, 'hgext.%s' % name)
103 103 else:
104 104 try:
105 105 mod = _importh("hgext.%s" % name)
106 106 except ImportError as err:
107 107 if reportfunc:
108 108 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
109 109 try:
110 110 mod = _importh("hgext3rd.%s" % name)
111 111 except ImportError as err:
112 112 if reportfunc:
113 113 reportfunc(err, "hgext3rd.%s" % name, name)
114 114 mod = _importh(name)
115 115 return mod
116 116
117 117 def _reportimporterror(ui, err, failed, next):
118 118 # note: this ui.debug happens before --debug is processed,
119 119 # Use --config ui.debug=1 to see them.
120 120 ui.debug('could not import %s (%s): trying %s\n'
121 121 % (failed, util.forcebytestr(err), next))
122 122 if ui.debugflag:
123 123 ui.traceback()
124 124
125 125 def _rejectunicode(name, xs):
126 126 if isinstance(xs, (list, set, tuple)):
127 127 for x in xs:
128 128 _rejectunicode(name, x)
129 129 elif isinstance(xs, dict):
130 130 for k, v in xs.items():
131 131 _rejectunicode(name, k)
132 132 _rejectunicode(b'%s.%s' % (name, util.forcebytestr(k)), v)
133 133 elif isinstance(xs, type(u'')):
134 134 raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
135 135 hint="use b'' to make it byte string")
136 136
137 137 # attributes set by registrar.command
138 138 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
139 139
140 140 def _validatecmdtable(ui, cmdtable):
141 141 """Check if extension commands have required attributes"""
142 142 for c, e in cmdtable.iteritems():
143 143 f = e[0]
144 144 if getattr(f, '_deprecatedregistrar', False):
145 145 ui.deprecwarn("cmdutil.command is deprecated, use "
146 146 "registrar.command to register '%s'" % c, '4.6')
147 147 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
148 148 if not missing:
149 149 continue
150 150 raise error.ProgrammingError(
151 151 'missing attributes: %s' % ', '.join(missing),
152 152 hint="use @command decorator to register '%s'" % c)
153 153
154 154 def _validatetables(ui, mod):
155 155 """Sanity check for loadable tables provided by extension module"""
156 156 for t in ['cmdtable', 'colortable', 'configtable']:
157 157 _rejectunicode(t, getattr(mod, t, {}))
158 158 for t in ['filesetpredicate', 'internalmerge', 'revsetpredicate',
159 159 'templatefilter', 'templatefunc', 'templatekeyword']:
160 160 o = getattr(mod, t, None)
161 161 if o:
162 162 _rejectunicode(t, o._table)
163 163 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
164 164
165 165 def load(ui, name, path):
166 166 if name.startswith('hgext.') or name.startswith('hgext/'):
167 167 shortname = name[6:]
168 168 else:
169 169 shortname = name
170 170 if shortname in _builtin:
171 171 return None
172 172 if shortname in _extensions:
173 173 return _extensions[shortname]
174 174 _extensions[shortname] = None
175 175 mod = _importext(name, path, bind(_reportimporterror, ui))
176 176
177 177 # Before we do anything with the extension, check against minimum stated
178 178 # compatibility. This gives extension authors a mechanism to have their
179 179 # extensions short circuit when loaded with a known incompatible version
180 180 # of Mercurial.
181 181 minver = getattr(mod, 'minimumhgversion', None)
182 182 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
183 183 ui.warn(_('(third party extension %s requires version %s or newer '
184 184 'of Mercurial; disabling)\n') % (shortname, minver))
185 185 return
186 186 _validatetables(ui, mod)
187 187
188 188 _extensions[shortname] = mod
189 189 _order.append(shortname)
190 190 for fn in _aftercallbacks.get(shortname, []):
191 191 fn(loaded=True)
192 192 return mod
193 193
194 194 def _runuisetup(name, ui):
195 195 uisetup = getattr(_extensions[name], 'uisetup', None)
196 196 if uisetup:
197 197 try:
198 198 uisetup(ui)
199 199 except Exception as inst:
200 200 ui.traceback(force=True)
201 201 msg = util.forcebytestr(inst)
202 202 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
203 203 return False
204 204 return True
205 205
206 206 def _runextsetup(name, ui):
207 207 extsetup = getattr(_extensions[name], 'extsetup', None)
208 208 if extsetup:
209 209 try:
210 210 try:
211 211 extsetup(ui)
212 212 except TypeError:
213 213 if pycompat.getargspec(extsetup).args:
214 214 raise
215 215 extsetup() # old extsetup with no ui argument
216 216 except Exception as inst:
217 217 ui.traceback(force=True)
218 218 msg = util.forcebytestr(inst)
219 219 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
220 220 return False
221 221 return True
222 222
223 223 def loadall(ui, whitelist=None):
224 224 result = ui.configitems("extensions")
225 225 if whitelist is not None:
226 226 result = [(k, v) for (k, v) in result if k in whitelist]
227 227 newindex = len(_order)
228 228 for (name, path) in result:
229 229 if path:
230 230 if path[0:1] == '!':
231 231 _disabledextensions[name] = path[1:]
232 232 continue
233 233 try:
234 234 load(ui, name, path)
235 235 except Exception as inst:
236 236 msg = util.forcebytestr(inst)
237 237 if path:
238 238 ui.warn(_("*** failed to import extension %s from %s: %s\n")
239 239 % (name, path, msg))
240 240 else:
241 241 ui.warn(_("*** failed to import extension %s: %s\n")
242 242 % (name, msg))
243 243 if isinstance(inst, error.Hint) and inst.hint:
244 244 ui.warn(_("*** (%s)\n") % inst.hint)
245 245 ui.traceback()
246 246 # list of (objname, loadermod, loadername) tuple:
247 247 # - objname is the name of an object in extension module,
248 248 # from which extra information is loaded
249 249 # - loadermod is the module where loader is placed
250 250 # - loadername is the name of the function,
251 251 # which takes (ui, extensionname, extraobj) arguments
252 252 #
253 253 # This one is for the list of item that must be run before running any setup
254 254 earlyextraloaders = [
255 255 ('configtable', configitems, 'loadconfigtable'),
256 256 ]
257 257 _loadextra(ui, newindex, earlyextraloaders)
258 258
259 259 broken = set()
260 260 for name in _order[newindex:]:
261 261 if not _runuisetup(name, ui):
262 262 broken.add(name)
263 263
264 264 for name in _order[newindex:]:
265 265 if name in broken:
266 266 continue
267 267 if not _runextsetup(name, ui):
268 268 broken.add(name)
269 269
270 270 for name in broken:
271 271 _extensions[name] = None
272 272
273 273 # Call aftercallbacks that were never met.
274 274 for shortname in _aftercallbacks:
275 275 if shortname in _extensions:
276 276 continue
277 277
278 278 for fn in _aftercallbacks[shortname]:
279 279 fn(loaded=False)
280 280
281 281 # loadall() is called multiple times and lingering _aftercallbacks
282 282 # entries could result in double execution. See issue4646.
283 283 _aftercallbacks.clear()
284 284
285 285 # delay importing avoids cyclic dependency (especially commands)
286 286 from . import (
287 287 color,
288 288 commands,
289 289 filemerge,
290 290 fileset,
291 291 revset,
292 292 templatefilters,
293 templatefuncs,
293 294 templatekw,
294 templater,
295 295 )
296 296
297 297 # list of (objname, loadermod, loadername) tuple:
298 298 # - objname is the name of an object in extension module,
299 299 # from which extra information is loaded
300 300 # - loadermod is the module where loader is placed
301 301 # - loadername is the name of the function,
302 302 # which takes (ui, extensionname, extraobj) arguments
303 303 extraloaders = [
304 304 ('cmdtable', commands, 'loadcmdtable'),
305 305 ('colortable', color, 'loadcolortable'),
306 306 ('filesetpredicate', fileset, 'loadpredicate'),
307 307 ('internalmerge', filemerge, 'loadinternalmerge'),
308 308 ('revsetpredicate', revset, 'loadpredicate'),
309 309 ('templatefilter', templatefilters, 'loadfilter'),
310 ('templatefunc', templater, 'loadfunction'),
310 ('templatefunc', templatefuncs, 'loadfunction'),
311 311 ('templatekeyword', templatekw, 'loadkeyword'),
312 312 ]
313 313 _loadextra(ui, newindex, extraloaders)
314 314
315 315 def _loadextra(ui, newindex, extraloaders):
316 316 for name in _order[newindex:]:
317 317 module = _extensions[name]
318 318 if not module:
319 319 continue # loading this module failed
320 320
321 321 for objname, loadermod, loadername in extraloaders:
322 322 extraobj = getattr(module, objname, None)
323 323 if extraobj is not None:
324 324 getattr(loadermod, loadername)(ui, name, extraobj)
325 325
326 326 def afterloaded(extension, callback):
327 327 '''Run the specified function after a named extension is loaded.
328 328
329 329 If the named extension is already loaded, the callback will be called
330 330 immediately.
331 331
332 332 If the named extension never loads, the callback will be called after
333 333 all extensions have been loaded.
334 334
335 335 The callback receives the named argument ``loaded``, which is a boolean
336 336 indicating whether the dependent extension actually loaded.
337 337 '''
338 338
339 339 if extension in _extensions:
340 340 # Report loaded as False if the extension is disabled
341 341 loaded = (_extensions[extension] is not None)
342 342 callback(loaded=loaded)
343 343 else:
344 344 _aftercallbacks.setdefault(extension, []).append(callback)
345 345
346 346 def bind(func, *args):
347 347 '''Partial function application
348 348
349 349 Returns a new function that is the partial application of args and kwargs
350 350 to func. For example,
351 351
352 352 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
353 353 assert callable(func)
354 354 def closure(*a, **kw):
355 355 return func(*(args + a), **kw)
356 356 return closure
357 357
358 358 def _updatewrapper(wrap, origfn, unboundwrapper):
359 359 '''Copy and add some useful attributes to wrapper'''
360 360 try:
361 361 wrap.__name__ = origfn.__name__
362 362 except AttributeError:
363 363 pass
364 364 wrap.__module__ = getattr(origfn, '__module__')
365 365 wrap.__doc__ = getattr(origfn, '__doc__')
366 366 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
367 367 wrap._origfunc = origfn
368 368 wrap._unboundwrapper = unboundwrapper
369 369
370 370 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
371 371 '''Wrap the command named `command' in table
372 372
373 373 Replace command in the command table with wrapper. The wrapped command will
374 374 be inserted into the command table specified by the table argument.
375 375
376 376 The wrapper will be called like
377 377
378 378 wrapper(orig, *args, **kwargs)
379 379
380 380 where orig is the original (wrapped) function, and *args, **kwargs
381 381 are the arguments passed to it.
382 382
383 383 Optionally append to the command synopsis and docstring, used for help.
384 384 For example, if your extension wraps the ``bookmarks`` command to add the
385 385 flags ``--remote`` and ``--all`` you might call this function like so:
386 386
387 387 synopsis = ' [-a] [--remote]'
388 388 docstring = """
389 389
390 390 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
391 391 flags to the bookmarks command. Either flag will show the remote bookmarks
392 392 known to the repository; ``--remote`` will also suppress the output of the
393 393 local bookmarks.
394 394 """
395 395
396 396 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
397 397 synopsis, docstring)
398 398 '''
399 399 assert callable(wrapper)
400 400 aliases, entry = cmdutil.findcmd(command, table)
401 401 for alias, e in table.iteritems():
402 402 if e is entry:
403 403 key = alias
404 404 break
405 405
406 406 origfn = entry[0]
407 407 wrap = functools.partial(util.checksignature(wrapper),
408 408 util.checksignature(origfn))
409 409 _updatewrapper(wrap, origfn, wrapper)
410 410 if docstring is not None:
411 411 wrap.__doc__ += docstring
412 412
413 413 newentry = list(entry)
414 414 newentry[0] = wrap
415 415 if synopsis is not None:
416 416 newentry[2] += synopsis
417 417 table[key] = tuple(newentry)
418 418 return entry
419 419
420 420 def wrapfilecache(cls, propname, wrapper):
421 421 """Wraps a filecache property.
422 422
423 423 These can't be wrapped using the normal wrapfunction.
424 424 """
425 425 propname = pycompat.sysstr(propname)
426 426 assert callable(wrapper)
427 427 for currcls in cls.__mro__:
428 428 if propname in currcls.__dict__:
429 429 origfn = currcls.__dict__[propname].func
430 430 assert callable(origfn)
431 431 def wrap(*args, **kwargs):
432 432 return wrapper(origfn, *args, **kwargs)
433 433 currcls.__dict__[propname].func = wrap
434 434 break
435 435
436 436 if currcls is object:
437 437 raise AttributeError(r"type '%s' has no property '%s'" % (
438 438 cls, propname))
439 439
440 440 class wrappedfunction(object):
441 441 '''context manager for temporarily wrapping a function'''
442 442
443 443 def __init__(self, container, funcname, wrapper):
444 444 assert callable(wrapper)
445 445 self._container = container
446 446 self._funcname = funcname
447 447 self._wrapper = wrapper
448 448
449 449 def __enter__(self):
450 450 wrapfunction(self._container, self._funcname, self._wrapper)
451 451
452 452 def __exit__(self, exctype, excvalue, traceback):
453 453 unwrapfunction(self._container, self._funcname, self._wrapper)
454 454
455 455 def wrapfunction(container, funcname, wrapper):
456 456 '''Wrap the function named funcname in container
457 457
458 458 Replace the funcname member in the given container with the specified
459 459 wrapper. The container is typically a module, class, or instance.
460 460
461 461 The wrapper will be called like
462 462
463 463 wrapper(orig, *args, **kwargs)
464 464
465 465 where orig is the original (wrapped) function, and *args, **kwargs
466 466 are the arguments passed to it.
467 467
468 468 Wrapping methods of the repository object is not recommended since
469 469 it conflicts with extensions that extend the repository by
470 470 subclassing. All extensions that need to extend methods of
471 471 localrepository should use this subclassing trick: namely,
472 472 reposetup() should look like
473 473
474 474 def reposetup(ui, repo):
475 475 class myrepo(repo.__class__):
476 476 def whatever(self, *args, **kwargs):
477 477 [...extension stuff...]
478 478 super(myrepo, self).whatever(*args, **kwargs)
479 479 [...extension stuff...]
480 480
481 481 repo.__class__ = myrepo
482 482
483 483 In general, combining wrapfunction() with subclassing does not
484 484 work. Since you cannot control what other extensions are loaded by
485 485 your end users, you should play nicely with others by using the
486 486 subclass trick.
487 487 '''
488 488 assert callable(wrapper)
489 489
490 490 origfn = getattr(container, funcname)
491 491 assert callable(origfn)
492 492 if inspect.ismodule(container):
493 493 # origfn is not an instance or class method. "partial" can be used.
494 494 # "partial" won't insert a frame in traceback.
495 495 wrap = functools.partial(wrapper, origfn)
496 496 else:
497 497 # "partial" cannot be safely used. Emulate its effect by using "bind".
498 498 # The downside is one more frame in traceback.
499 499 wrap = bind(wrapper, origfn)
500 500 _updatewrapper(wrap, origfn, wrapper)
501 501 setattr(container, funcname, wrap)
502 502 return origfn
503 503
504 504 def unwrapfunction(container, funcname, wrapper=None):
505 505 '''undo wrapfunction
506 506
507 507 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
508 508 from the chain of wrappers.
509 509
510 510 Return the removed wrapper.
511 511 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
512 512 wrapper is not None but is not found in the wrapper chain.
513 513 '''
514 514 chain = getwrapperchain(container, funcname)
515 515 origfn = chain.pop()
516 516 if wrapper is None:
517 517 wrapper = chain[0]
518 518 chain.remove(wrapper)
519 519 setattr(container, funcname, origfn)
520 520 for w in reversed(chain):
521 521 wrapfunction(container, funcname, w)
522 522 return wrapper
523 523
524 524 def getwrapperchain(container, funcname):
525 525 '''get a chain of wrappers of a function
526 526
527 527 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
528 528
529 529 The wrapper functions are the ones passed to wrapfunction, whose first
530 530 argument is origfunc.
531 531 '''
532 532 result = []
533 533 fn = getattr(container, funcname)
534 534 while fn:
535 535 assert callable(fn)
536 536 result.append(getattr(fn, '_unboundwrapper', fn))
537 537 fn = getattr(fn, '_origfunc', None)
538 538 return result
539 539
540 540 def _disabledpaths(strip_init=False):
541 541 '''find paths of disabled extensions. returns a dict of {name: path}
542 542 removes /__init__.py from packages if strip_init is True'''
543 543 import hgext
544 544 extpath = os.path.dirname(
545 545 os.path.abspath(pycompat.fsencode(hgext.__file__)))
546 546 try: # might not be a filesystem path
547 547 files = os.listdir(extpath)
548 548 except OSError:
549 549 return {}
550 550
551 551 exts = {}
552 552 for e in files:
553 553 if e.endswith('.py'):
554 554 name = e.rsplit('.', 1)[0]
555 555 path = os.path.join(extpath, e)
556 556 else:
557 557 name = e
558 558 path = os.path.join(extpath, e, '__init__.py')
559 559 if not os.path.exists(path):
560 560 continue
561 561 if strip_init:
562 562 path = os.path.dirname(path)
563 563 if name in exts or name in _order or name == '__init__':
564 564 continue
565 565 exts[name] = path
566 566 for name, path in _disabledextensions.iteritems():
567 567 # If no path was provided for a disabled extension (e.g. "color=!"),
568 568 # don't replace the path we already found by the scan above.
569 569 if path:
570 570 exts[name] = path
571 571 return exts
572 572
573 573 def _moduledoc(file):
574 574 '''return the top-level python documentation for the given file
575 575
576 576 Loosely inspired by pydoc.source_synopsis(), but rewritten to
577 577 handle triple quotes and to return the whole text instead of just
578 578 the synopsis'''
579 579 result = []
580 580
581 581 line = file.readline()
582 582 while line[:1] == '#' or not line.strip():
583 583 line = file.readline()
584 584 if not line:
585 585 break
586 586
587 587 start = line[:3]
588 588 if start == '"""' or start == "'''":
589 589 line = line[3:]
590 590 while line:
591 591 if line.rstrip().endswith(start):
592 592 line = line.split(start)[0]
593 593 if line:
594 594 result.append(line)
595 595 break
596 596 elif not line:
597 597 return None # unmatched delimiter
598 598 result.append(line)
599 599 line = file.readline()
600 600 else:
601 601 return None
602 602
603 603 return ''.join(result)
604 604
605 605 def _disabledhelp(path):
606 606 '''retrieve help synopsis of a disabled extension (without importing)'''
607 607 try:
608 608 file = open(path)
609 609 except IOError:
610 610 return
611 611 else:
612 612 doc = _moduledoc(file)
613 613 file.close()
614 614
615 615 if doc: # extracting localized synopsis
616 616 return gettext(doc)
617 617 else:
618 618 return _('(no help text available)')
619 619
620 620 def disabled():
621 621 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
622 622 try:
623 623 from hgext import __index__
624 624 return dict((name, gettext(desc))
625 625 for name, desc in __index__.docs.iteritems()
626 626 if name not in _order)
627 627 except (ImportError, AttributeError):
628 628 pass
629 629
630 630 paths = _disabledpaths()
631 631 if not paths:
632 632 return {}
633 633
634 634 exts = {}
635 635 for name, path in paths.iteritems():
636 636 doc = _disabledhelp(path)
637 637 if doc:
638 638 exts[name] = doc.splitlines()[0]
639 639
640 640 return exts
641 641
642 642 def disabledext(name):
643 643 '''find a specific disabled extension from hgext. returns desc'''
644 644 try:
645 645 from hgext import __index__
646 646 if name in _order: # enabled
647 647 return
648 648 else:
649 649 return gettext(__index__.docs.get(name))
650 650 except (ImportError, AttributeError):
651 651 pass
652 652
653 653 paths = _disabledpaths()
654 654 if name in paths:
655 655 return _disabledhelp(paths[name])
656 656
657 657 def disabledcmd(ui, cmd, strict=False):
658 658 '''import disabled extensions until cmd is found.
659 659 returns (cmdname, extname, module)'''
660 660
661 661 paths = _disabledpaths(strip_init=True)
662 662 if not paths:
663 663 raise error.UnknownCommand(cmd)
664 664
665 665 def findcmd(cmd, name, path):
666 666 try:
667 667 mod = loadpath(path, 'hgext.%s' % name)
668 668 except Exception:
669 669 return
670 670 try:
671 671 aliases, entry = cmdutil.findcmd(cmd,
672 672 getattr(mod, 'cmdtable', {}), strict)
673 673 except (error.AmbiguousCommand, error.UnknownCommand):
674 674 return
675 675 except Exception:
676 676 ui.warn(_('warning: error finding commands in %s\n') % path)
677 677 ui.traceback()
678 678 return
679 679 for c in aliases:
680 680 if c.startswith(cmd):
681 681 cmd = c
682 682 break
683 683 else:
684 684 cmd = aliases[0]
685 685 return (cmd, name, mod)
686 686
687 687 ext = None
688 688 # first, search for an extension with the same name as the command
689 689 path = paths.pop(cmd, None)
690 690 if path:
691 691 ext = findcmd(cmd, cmd, path)
692 692 if not ext:
693 693 # otherwise, interrogate each extension until there's a match
694 694 for name, path in paths.iteritems():
695 695 ext = findcmd(cmd, name, path)
696 696 if ext:
697 697 break
698 698 if ext and 'DEPRECATED' not in ext.__doc__:
699 699 return ext
700 700
701 701 raise error.UnknownCommand(cmd)
702 702
703 703 def enabled(shortname=True):
704 704 '''return a dict of {name: desc} of extensions'''
705 705 exts = {}
706 706 for ename, ext in extensions():
707 707 doc = (gettext(ext.__doc__) or _('(no help text available)'))
708 708 if shortname:
709 709 ename = ename.split('.')[-1]
710 710 exts[ename] = doc.splitlines()[0].strip()
711 711
712 712 return exts
713 713
714 714 def notloaded():
715 715 '''return short names of extensions that failed to load'''
716 716 return [name for name, mod in _extensions.iteritems() if mod is None]
717 717
718 718 def moduleversion(module):
719 719 '''return version information from given module as a string'''
720 720 if (util.safehasattr(module, 'getversion')
721 721 and callable(module.getversion)):
722 722 version = module.getversion()
723 723 elif util.safehasattr(module, '__version__'):
724 724 version = module.__version__
725 725 else:
726 726 version = ''
727 727 if isinstance(version, (list, tuple)):
728 728 version = '.'.join(str(o) for o in version)
729 729 return version
730 730
731 731 def ismoduleinternal(module):
732 732 exttestedwith = getattr(module, 'testedwith', None)
733 733 return exttestedwith == "ships-with-hg-core"
@@ -1,685 +1,685
1 1 # help.py - help data for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12 import textwrap
13 13
14 14 from .i18n import (
15 15 _,
16 16 gettext,
17 17 )
18 18 from . import (
19 19 cmdutil,
20 20 encoding,
21 21 error,
22 22 extensions,
23 23 filemerge,
24 24 fileset,
25 25 minirst,
26 26 pycompat,
27 27 revset,
28 28 templatefilters,
29 templatefuncs,
29 30 templatekw,
30 templater,
31 31 util,
32 32 )
33 33 from .hgweb import (
34 34 webcommands,
35 35 )
36 36
37 37 _exclkeywords = {
38 38 "(ADVANCED)",
39 39 "(DEPRECATED)",
40 40 "(EXPERIMENTAL)",
41 41 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
42 42 _("(ADVANCED)"),
43 43 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
44 44 _("(DEPRECATED)"),
45 45 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
46 46 _("(EXPERIMENTAL)"),
47 47 }
48 48
49 49 def listexts(header, exts, indent=1, showdeprecated=False):
50 50 '''return a text listing of the given extensions'''
51 51 rst = []
52 52 if exts:
53 53 for name, desc in sorted(exts.iteritems()):
54 54 if not showdeprecated and any(w in desc for w in _exclkeywords):
55 55 continue
56 56 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
57 57 if rst:
58 58 rst.insert(0, '\n%s\n\n' % header)
59 59 return rst
60 60
61 61 def extshelp(ui):
62 62 rst = loaddoc('extensions')(ui).splitlines(True)
63 63 rst.extend(listexts(
64 64 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
65 65 rst.extend(listexts(_('disabled extensions:'), extensions.disabled(),
66 66 showdeprecated=ui.verbose))
67 67 doc = ''.join(rst)
68 68 return doc
69 69
70 70 def optrst(header, options, verbose):
71 71 data = []
72 72 multioccur = False
73 73 for option in options:
74 74 if len(option) == 5:
75 75 shortopt, longopt, default, desc, optlabel = option
76 76 else:
77 77 shortopt, longopt, default, desc = option
78 78 optlabel = _("VALUE") # default label
79 79
80 80 if not verbose and any(w in desc for w in _exclkeywords):
81 81 continue
82 82
83 83 so = ''
84 84 if shortopt:
85 85 so = '-' + shortopt
86 86 lo = '--' + longopt
87 87 if default:
88 88 # default is of unknown type, and in Python 2 we abused
89 89 # the %s-shows-repr property to handle integers etc. To
90 90 # match that behavior on Python 3, we do str(default) and
91 91 # then convert it to bytes.
92 92 desc += _(" (default: %s)") % pycompat.bytestr(default)
93 93
94 94 if isinstance(default, list):
95 95 lo += " %s [+]" % optlabel
96 96 multioccur = True
97 97 elif (default is not None) and not isinstance(default, bool):
98 98 lo += " %s" % optlabel
99 99
100 100 data.append((so, lo, desc))
101 101
102 102 if multioccur:
103 103 header += (_(" ([+] can be repeated)"))
104 104
105 105 rst = ['\n%s:\n\n' % header]
106 106 rst.extend(minirst.maketable(data, 1))
107 107
108 108 return ''.join(rst)
109 109
110 110 def indicateomitted(rst, omitted, notomitted=None):
111 111 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
112 112 if notomitted:
113 113 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
114 114
115 115 def filtercmd(ui, cmd, kw, doc):
116 116 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
117 117 return True
118 118 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
119 119 return True
120 120 return False
121 121
122 122 def topicmatch(ui, commands, kw):
123 123 """Return help topics matching kw.
124 124
125 125 Returns {'section': [(name, summary), ...], ...} where section is
126 126 one of topics, commands, extensions, or extensioncommands.
127 127 """
128 128 kw = encoding.lower(kw)
129 129 def lowercontains(container):
130 130 return kw in encoding.lower(container) # translated in helptable
131 131 results = {'topics': [],
132 132 'commands': [],
133 133 'extensions': [],
134 134 'extensioncommands': [],
135 135 }
136 136 for names, header, doc in helptable:
137 137 # Old extensions may use a str as doc.
138 138 if (sum(map(lowercontains, names))
139 139 or lowercontains(header)
140 140 or (callable(doc) and lowercontains(doc(ui)))):
141 141 results['topics'].append((names[0], header))
142 142 for cmd, entry in commands.table.iteritems():
143 143 if len(entry) == 3:
144 144 summary = entry[2]
145 145 else:
146 146 summary = ''
147 147 # translate docs *before* searching there
148 148 docs = _(pycompat.getdoc(entry[0])) or ''
149 149 if kw in cmd or lowercontains(summary) or lowercontains(docs):
150 150 doclines = docs.splitlines()
151 151 if doclines:
152 152 summary = doclines[0]
153 153 cmdname = cmdutil.parsealiases(cmd)[0]
154 154 if filtercmd(ui, cmdname, kw, docs):
155 155 continue
156 156 results['commands'].append((cmdname, summary))
157 157 for name, docs in itertools.chain(
158 158 extensions.enabled(False).iteritems(),
159 159 extensions.disabled().iteritems()):
160 160 if not docs:
161 161 continue
162 162 name = name.rpartition('.')[-1]
163 163 if lowercontains(name) or lowercontains(docs):
164 164 # extension docs are already translated
165 165 results['extensions'].append((name, docs.splitlines()[0]))
166 166 try:
167 167 mod = extensions.load(ui, name, '')
168 168 except ImportError:
169 169 # debug message would be printed in extensions.load()
170 170 continue
171 171 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
172 172 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
173 173 cmdname = cmdutil.parsealiases(cmd)[0]
174 174 cmddoc = pycompat.getdoc(entry[0])
175 175 if cmddoc:
176 176 cmddoc = gettext(cmddoc).splitlines()[0]
177 177 else:
178 178 cmddoc = _('(no help text available)')
179 179 if filtercmd(ui, cmdname, kw, cmddoc):
180 180 continue
181 181 results['extensioncommands'].append((cmdname, cmddoc))
182 182 return results
183 183
184 184 def loaddoc(topic, subdir=None):
185 185 """Return a delayed loader for help/topic.txt."""
186 186
187 187 def loader(ui):
188 188 docdir = os.path.join(util.datapath, 'help')
189 189 if subdir:
190 190 docdir = os.path.join(docdir, subdir)
191 191 path = os.path.join(docdir, topic + ".txt")
192 192 doc = gettext(util.readfile(path))
193 193 for rewriter in helphooks.get(topic, []):
194 194 doc = rewriter(ui, topic, doc)
195 195 return doc
196 196
197 197 return loader
198 198
199 199 internalstable = sorted([
200 200 (['bundle2'], _('Bundle2'),
201 201 loaddoc('bundle2', subdir='internals')),
202 202 (['bundles'], _('Bundles'),
203 203 loaddoc('bundles', subdir='internals')),
204 204 (['censor'], _('Censor'),
205 205 loaddoc('censor', subdir='internals')),
206 206 (['changegroups'], _('Changegroups'),
207 207 loaddoc('changegroups', subdir='internals')),
208 208 (['config'], _('Config Registrar'),
209 209 loaddoc('config', subdir='internals')),
210 210 (['requirements'], _('Repository Requirements'),
211 211 loaddoc('requirements', subdir='internals')),
212 212 (['revlogs'], _('Revision Logs'),
213 213 loaddoc('revlogs', subdir='internals')),
214 214 (['wireprotocol'], _('Wire Protocol'),
215 215 loaddoc('wireprotocol', subdir='internals')),
216 216 ])
217 217
218 218 def internalshelp(ui):
219 219 """Generate the index for the "internals" topic."""
220 220 lines = ['To access a subtopic, use "hg help internals.{subtopic-name}"\n',
221 221 '\n']
222 222 for names, header, doc in internalstable:
223 223 lines.append(' :%s: %s\n' % (names[0], header))
224 224
225 225 return ''.join(lines)
226 226
227 227 helptable = sorted([
228 228 (['bundlespec'], _("Bundle File Formats"), loaddoc('bundlespec')),
229 229 (['color'], _("Colorizing Outputs"), loaddoc('color')),
230 230 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
231 231 (["dates"], _("Date Formats"), loaddoc('dates')),
232 232 (["flags"], _("Command-line flags"), loaddoc('flags')),
233 233 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
234 234 (['environment', 'env'], _('Environment Variables'),
235 235 loaddoc('environment')),
236 236 (['revisions', 'revs', 'revsets', 'revset', 'multirevs', 'mrevs'],
237 237 _('Specifying Revisions'), loaddoc('revisions')),
238 238 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
239 239 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
240 240 (['merge-tools', 'mergetools', 'mergetool'], _('Merge Tools'),
241 241 loaddoc('merge-tools')),
242 242 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
243 243 loaddoc('templates')),
244 244 (['urls'], _('URL Paths'), loaddoc('urls')),
245 245 (["extensions"], _("Using Additional Features"), extshelp),
246 246 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
247 247 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
248 248 (["glossary"], _("Glossary"), loaddoc('glossary')),
249 249 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
250 250 loaddoc('hgignore')),
251 251 (["phases"], _("Working with Phases"), loaddoc('phases')),
252 252 (['scripting'], _('Using Mercurial from scripts and automation'),
253 253 loaddoc('scripting')),
254 254 (['internals'], _("Technical implementation topics"),
255 255 internalshelp),
256 256 (['pager'], _("Pager Support"), loaddoc('pager')),
257 257 ])
258 258
259 259 # Maps topics with sub-topics to a list of their sub-topics.
260 260 subtopics = {
261 261 'internals': internalstable,
262 262 }
263 263
264 264 # Map topics to lists of callable taking the current topic help and
265 265 # returning the updated version
266 266 helphooks = {}
267 267
268 268 def addtopichook(topic, rewriter):
269 269 helphooks.setdefault(topic, []).append(rewriter)
270 270
271 271 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
272 272 """Extract docstring from the items key to function mapping, build a
273 273 single documentation block and use it to overwrite the marker in doc.
274 274 """
275 275 entries = []
276 276 for name in sorted(items):
277 277 text = (pycompat.getdoc(items[name]) or '').rstrip()
278 278 if (not text
279 279 or not ui.verbose and any(w in text for w in _exclkeywords)):
280 280 continue
281 281 text = gettext(text)
282 282 if dedent:
283 283 # Abuse latin1 to use textwrap.dedent() on bytes.
284 284 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
285 285 lines = text.splitlines()
286 286 doclines = [(lines[0])]
287 287 for l in lines[1:]:
288 288 # Stop once we find some Python doctest
289 289 if l.strip().startswith('>>>'):
290 290 break
291 291 if dedent:
292 292 doclines.append(l.rstrip())
293 293 else:
294 294 doclines.append(' ' + l.strip())
295 295 entries.append('\n'.join(doclines))
296 296 entries = '\n\n'.join(entries)
297 297 return doc.replace(marker, entries)
298 298
299 299 def addtopicsymbols(topic, marker, symbols, dedent=False):
300 300 def add(ui, topic, doc):
301 301 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
302 302 addtopichook(topic, add)
303 303
304 304 addtopicsymbols('bundlespec', '.. bundlecompressionmarker',
305 305 util.bundlecompressiontopics())
306 306 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
307 307 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
308 308 filemerge.internalsdoc)
309 309 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
310 310 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
311 311 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
312 addtopicsymbols('templates', '.. functionsmarker', templater.funcs)
312 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
313 313 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
314 314 dedent=True)
315 315
316 316 def help_(ui, commands, name, unknowncmd=False, full=True, subtopic=None,
317 317 **opts):
318 318 '''
319 319 Generate the help for 'name' as unformatted restructured text. If
320 320 'name' is None, describe the commands available.
321 321 '''
322 322
323 323 opts = pycompat.byteskwargs(opts)
324 324
325 325 def helpcmd(name, subtopic=None):
326 326 try:
327 327 aliases, entry = cmdutil.findcmd(name, commands.table,
328 328 strict=unknowncmd)
329 329 except error.AmbiguousCommand as inst:
330 330 # py3k fix: except vars can't be used outside the scope of the
331 331 # except block, nor can be used inside a lambda. python issue4617
332 332 prefix = inst.args[0]
333 333 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
334 334 rst = helplist(select)
335 335 return rst
336 336
337 337 rst = []
338 338
339 339 # check if it's an invalid alias and display its error if it is
340 340 if getattr(entry[0], 'badalias', None):
341 341 rst.append(entry[0].badalias + '\n')
342 342 if entry[0].unknowncmd:
343 343 try:
344 344 rst.extend(helpextcmd(entry[0].cmdname))
345 345 except error.UnknownCommand:
346 346 pass
347 347 return rst
348 348
349 349 # synopsis
350 350 if len(entry) > 2:
351 351 if entry[2].startswith('hg'):
352 352 rst.append("%s\n" % entry[2])
353 353 else:
354 354 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
355 355 else:
356 356 rst.append('hg %s\n' % aliases[0])
357 357 # aliases
358 358 if full and not ui.quiet and len(aliases) > 1:
359 359 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
360 360 rst.append('\n')
361 361
362 362 # description
363 363 doc = gettext(pycompat.getdoc(entry[0]))
364 364 if not doc:
365 365 doc = _("(no help text available)")
366 366 if util.safehasattr(entry[0], 'definition'): # aliased command
367 367 source = entry[0].source
368 368 if entry[0].definition.startswith('!'): # shell alias
369 369 doc = (_('shell alias for::\n\n %s\n\ndefined by: %s\n') %
370 370 (entry[0].definition[1:], source))
371 371 else:
372 372 doc = (_('alias for: hg %s\n\n%s\n\ndefined by: %s\n') %
373 373 (entry[0].definition, doc, source))
374 374 doc = doc.splitlines(True)
375 375 if ui.quiet or not full:
376 376 rst.append(doc[0])
377 377 else:
378 378 rst.extend(doc)
379 379 rst.append('\n')
380 380
381 381 # check if this command shadows a non-trivial (multi-line)
382 382 # extension help text
383 383 try:
384 384 mod = extensions.find(name)
385 385 doc = gettext(pycompat.getdoc(mod)) or ''
386 386 if '\n' in doc.strip():
387 387 msg = _("(use 'hg help -e %s' to show help for "
388 388 "the %s extension)") % (name, name)
389 389 rst.append('\n%s\n' % msg)
390 390 except KeyError:
391 391 pass
392 392
393 393 # options
394 394 if not ui.quiet and entry[1]:
395 395 rst.append(optrst(_("options"), entry[1], ui.verbose))
396 396
397 397 if ui.verbose:
398 398 rst.append(optrst(_("global options"),
399 399 commands.globalopts, ui.verbose))
400 400
401 401 if not ui.verbose:
402 402 if not full:
403 403 rst.append(_("\n(use 'hg %s -h' to show more help)\n")
404 404 % name)
405 405 elif not ui.quiet:
406 406 rst.append(_('\n(some details hidden, use --verbose '
407 407 'to show complete help)'))
408 408
409 409 return rst
410 410
411 411
412 412 def helplist(select=None, **opts):
413 413 # list of commands
414 414 if name == "shortlist":
415 415 header = _('basic commands:\n\n')
416 416 elif name == "debug":
417 417 header = _('debug commands (internal and unsupported):\n\n')
418 418 else:
419 419 header = _('list of commands:\n\n')
420 420
421 421 h = {}
422 422 cmds = {}
423 423 for c, e in commands.table.iteritems():
424 424 fs = cmdutil.parsealiases(c)
425 425 f = fs[0]
426 426 p = ''
427 427 if c.startswith("^"):
428 428 p = '^'
429 429 if select and not select(p + f):
430 430 continue
431 431 if (not select and name != 'shortlist' and
432 432 e[0].__module__ != commands.__name__):
433 433 continue
434 434 if name == "shortlist" and not p:
435 435 continue
436 436 doc = pycompat.getdoc(e[0])
437 437 if filtercmd(ui, f, name, doc):
438 438 continue
439 439 doc = gettext(doc)
440 440 if not doc:
441 441 doc = _("(no help text available)")
442 442 h[f] = doc.splitlines()[0].rstrip()
443 443 cmds[f] = '|'.join(fs)
444 444
445 445 rst = []
446 446 if not h:
447 447 if not ui.quiet:
448 448 rst.append(_('no commands defined\n'))
449 449 return rst
450 450
451 451 if not ui.quiet:
452 452 rst.append(header)
453 453 fns = sorted(h)
454 454 for f in fns:
455 455 if ui.verbose:
456 456 commacmds = cmds[f].replace("|",", ")
457 457 rst.append(" :%s: %s\n" % (commacmds, h[f]))
458 458 else:
459 459 rst.append(' :%s: %s\n' % (f, h[f]))
460 460
461 461 ex = opts.get
462 462 anyopts = (ex(r'keyword') or not (ex(r'command') or ex(r'extension')))
463 463 if not name and anyopts:
464 464 exts = listexts(_('enabled extensions:'), extensions.enabled())
465 465 if exts:
466 466 rst.append('\n')
467 467 rst.extend(exts)
468 468
469 469 rst.append(_("\nadditional help topics:\n\n"))
470 470 topics = []
471 471 for names, header, doc in helptable:
472 472 topics.append((names[0], header))
473 473 for t, desc in topics:
474 474 rst.append(" :%s: %s\n" % (t, desc))
475 475
476 476 if ui.quiet:
477 477 pass
478 478 elif ui.verbose:
479 479 rst.append('\n%s\n' % optrst(_("global options"),
480 480 commands.globalopts, ui.verbose))
481 481 if name == 'shortlist':
482 482 rst.append(_("\n(use 'hg help' for the full list "
483 483 "of commands)\n"))
484 484 else:
485 485 if name == 'shortlist':
486 486 rst.append(_("\n(use 'hg help' for the full list of commands "
487 487 "or 'hg -v' for details)\n"))
488 488 elif name and not full:
489 489 rst.append(_("\n(use 'hg help %s' to show the full help "
490 490 "text)\n") % name)
491 491 elif name and cmds and name in cmds.keys():
492 492 rst.append(_("\n(use 'hg help -v -e %s' to show built-in "
493 493 "aliases and global options)\n") % name)
494 494 else:
495 495 rst.append(_("\n(use 'hg help -v%s' to show built-in aliases "
496 496 "and global options)\n")
497 497 % (name and " " + name or ""))
498 498 return rst
499 499
500 500 def helptopic(name, subtopic=None):
501 501 # Look for sub-topic entry first.
502 502 header, doc = None, None
503 503 if subtopic and name in subtopics:
504 504 for names, header, doc in subtopics[name]:
505 505 if subtopic in names:
506 506 break
507 507
508 508 if not header:
509 509 for names, header, doc in helptable:
510 510 if name in names:
511 511 break
512 512 else:
513 513 raise error.UnknownCommand(name)
514 514
515 515 rst = [minirst.section(header)]
516 516
517 517 # description
518 518 if not doc:
519 519 rst.append(" %s\n" % _("(no help text available)"))
520 520 if callable(doc):
521 521 rst += [" %s\n" % l for l in doc(ui).splitlines()]
522 522
523 523 if not ui.verbose:
524 524 omitted = _('(some details hidden, use --verbose'
525 525 ' to show complete help)')
526 526 indicateomitted(rst, omitted)
527 527
528 528 try:
529 529 cmdutil.findcmd(name, commands.table)
530 530 rst.append(_("\nuse 'hg help -c %s' to see help for "
531 531 "the %s command\n") % (name, name))
532 532 except error.UnknownCommand:
533 533 pass
534 534 return rst
535 535
536 536 def helpext(name, subtopic=None):
537 537 try:
538 538 mod = extensions.find(name)
539 539 doc = gettext(pycompat.getdoc(mod)) or _('no help text available')
540 540 except KeyError:
541 541 mod = None
542 542 doc = extensions.disabledext(name)
543 543 if not doc:
544 544 raise error.UnknownCommand(name)
545 545
546 546 if '\n' not in doc:
547 547 head, tail = doc, ""
548 548 else:
549 549 head, tail = doc.split('\n', 1)
550 550 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
551 551 if tail:
552 552 rst.extend(tail.splitlines(True))
553 553 rst.append('\n')
554 554
555 555 if not ui.verbose:
556 556 omitted = _('(some details hidden, use --verbose'
557 557 ' to show complete help)')
558 558 indicateomitted(rst, omitted)
559 559
560 560 if mod:
561 561 try:
562 562 ct = mod.cmdtable
563 563 except AttributeError:
564 564 ct = {}
565 565 modcmds = set([c.partition('|')[0] for c in ct])
566 566 rst.extend(helplist(modcmds.__contains__))
567 567 else:
568 568 rst.append(_("(use 'hg help extensions' for information on enabling"
569 569 " extensions)\n"))
570 570 return rst
571 571
572 572 def helpextcmd(name, subtopic=None):
573 573 cmd, ext, mod = extensions.disabledcmd(ui, name,
574 574 ui.configbool('ui', 'strict'))
575 575 doc = gettext(pycompat.getdoc(mod)).splitlines()[0]
576 576
577 577 rst = listexts(_("'%s' is provided by the following "
578 578 "extension:") % cmd, {ext: doc}, indent=4,
579 579 showdeprecated=True)
580 580 rst.append('\n')
581 581 rst.append(_("(use 'hg help extensions' for information on enabling "
582 582 "extensions)\n"))
583 583 return rst
584 584
585 585
586 586 rst = []
587 587 kw = opts.get('keyword')
588 588 if kw or name is None and any(opts[o] for o in opts):
589 589 matches = topicmatch(ui, commands, name or '')
590 590 helpareas = []
591 591 if opts.get('extension'):
592 592 helpareas += [('extensions', _('Extensions'))]
593 593 if opts.get('command'):
594 594 helpareas += [('commands', _('Commands'))]
595 595 if not helpareas:
596 596 helpareas = [('topics', _('Topics')),
597 597 ('commands', _('Commands')),
598 598 ('extensions', _('Extensions')),
599 599 ('extensioncommands', _('Extension Commands'))]
600 600 for t, title in helpareas:
601 601 if matches[t]:
602 602 rst.append('%s:\n\n' % title)
603 603 rst.extend(minirst.maketable(sorted(matches[t]), 1))
604 604 rst.append('\n')
605 605 if not rst:
606 606 msg = _('no matches')
607 607 hint = _("try 'hg help' for a list of topics")
608 608 raise error.Abort(msg, hint=hint)
609 609 elif name and name != 'shortlist':
610 610 queries = []
611 611 if unknowncmd:
612 612 queries += [helpextcmd]
613 613 if opts.get('extension'):
614 614 queries += [helpext]
615 615 if opts.get('command'):
616 616 queries += [helpcmd]
617 617 if not queries:
618 618 queries = (helptopic, helpcmd, helpext, helpextcmd)
619 619 for f in queries:
620 620 try:
621 621 rst = f(name, subtopic)
622 622 break
623 623 except error.UnknownCommand:
624 624 pass
625 625 else:
626 626 if unknowncmd:
627 627 raise error.UnknownCommand(name)
628 628 else:
629 629 msg = _('no such help topic: %s') % name
630 630 hint = _("try 'hg help --keyword %s'") % name
631 631 raise error.Abort(msg, hint=hint)
632 632 else:
633 633 # program name
634 634 if not ui.quiet:
635 635 rst = [_("Mercurial Distributed SCM\n"), '\n']
636 636 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
637 637
638 638 return ''.join(rst)
639 639
640 640 def formattedhelp(ui, commands, name, keep=None, unknowncmd=False, full=True,
641 641 **opts):
642 642 """get help for a given topic (as a dotted name) as rendered rst
643 643
644 644 Either returns the rendered help text or raises an exception.
645 645 """
646 646 if keep is None:
647 647 keep = []
648 648 else:
649 649 keep = list(keep) # make a copy so we can mutate this later
650 650 fullname = name
651 651 section = None
652 652 subtopic = None
653 653 if name and '.' in name:
654 654 name, remaining = name.split('.', 1)
655 655 remaining = encoding.lower(remaining)
656 656 if '.' in remaining:
657 657 subtopic, section = remaining.split('.', 1)
658 658 else:
659 659 if name in subtopics:
660 660 subtopic = remaining
661 661 else:
662 662 section = remaining
663 663 textwidth = ui.configint('ui', 'textwidth')
664 664 termwidth = ui.termwidth() - 2
665 665 if textwidth <= 0 or termwidth < textwidth:
666 666 textwidth = termwidth
667 667 text = help_(ui, commands, name,
668 668 subtopic=subtopic, unknowncmd=unknowncmd, full=full, **opts)
669 669
670 670 formatted, pruned = minirst.format(text, textwidth, keep=keep,
671 671 section=section)
672 672
673 673 # We could have been given a weird ".foo" section without a name
674 674 # to look for, or we could have simply failed to found "foo.bar"
675 675 # because bar isn't a section of foo
676 676 if section and not (formatted and name):
677 677 raise error.Abort(_("help section not found: %s") % fullname)
678 678
679 679 if 'verbose' in pruned:
680 680 keep.append('omitted')
681 681 else:
682 682 keep.append('notomitted')
683 683 formatted, pruned = minirst.format(text, textwidth, keep=keep,
684 684 section=section)
685 685 return formatted
@@ -1,437 +1,437
1 1 # registrar.py - utilities to register function for specific purpose
2 2 #
3 3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from . import (
11 11 configitems,
12 12 error,
13 13 pycompat,
14 14 util,
15 15 )
16 16
17 17 # unlike the other registered items, config options are neither functions or
18 18 # classes. Registering the option is just small function call.
19 19 #
20 20 # We still add the official API to the registrar module for consistency with
21 21 # the other items extensions want might to register.
22 22 configitem = configitems.getitemregister
23 23
24 24 class _funcregistrarbase(object):
25 25 """Base of decorator to register a function for specific purpose
26 26
27 27 This decorator stores decorated functions into own dict 'table'.
28 28
29 29 The least derived class can be defined by overriding 'formatdoc',
30 30 for example::
31 31
32 32 class keyword(_funcregistrarbase):
33 33 _docformat = ":%s: %s"
34 34
35 35 This should be used as below:
36 36
37 37 keyword = registrar.keyword()
38 38
39 39 @keyword('bar')
40 40 def barfunc(*args, **kwargs):
41 41 '''Explanation of bar keyword ....
42 42 '''
43 43 pass
44 44
45 45 In this case:
46 46
47 47 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
48 48 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
49 49 """
50 50 def __init__(self, table=None):
51 51 if table is None:
52 52 self._table = {}
53 53 else:
54 54 self._table = table
55 55
56 56 def __call__(self, decl, *args, **kwargs):
57 57 return lambda func: self._doregister(func, decl, *args, **kwargs)
58 58
59 59 def _doregister(self, func, decl, *args, **kwargs):
60 60 name = self._getname(decl)
61 61
62 62 if name in self._table:
63 63 msg = 'duplicate registration for name: "%s"' % name
64 64 raise error.ProgrammingError(msg)
65 65
66 66 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
67 67 doc = pycompat.sysbytes(func.__doc__).strip()
68 68 func._origdoc = doc
69 69 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
70 70
71 71 self._table[name] = func
72 72 self._extrasetup(name, func, *args, **kwargs)
73 73
74 74 return func
75 75
76 76 def _parsefuncdecl(self, decl):
77 77 """Parse function declaration and return the name of function in it
78 78 """
79 79 i = decl.find('(')
80 80 if i >= 0:
81 81 return decl[:i]
82 82 else:
83 83 return decl
84 84
85 85 def _getname(self, decl):
86 86 """Return the name of the registered function from decl
87 87
88 88 Derived class should override this, if it allows more
89 89 descriptive 'decl' string than just a name.
90 90 """
91 91 return decl
92 92
93 93 _docformat = None
94 94
95 95 def _formatdoc(self, decl, doc):
96 96 """Return formatted document of the registered function for help
97 97
98 98 'doc' is '__doc__.strip()' of the registered function.
99 99 """
100 100 return self._docformat % (decl, doc)
101 101
102 102 def _extrasetup(self, name, func):
103 103 """Execute exra setup for registered function, if needed
104 104 """
105 105
106 106 class command(_funcregistrarbase):
107 107 """Decorator to register a command function to table
108 108
109 109 This class receives a command table as its argument. The table should
110 110 be a dict.
111 111
112 112 The created object can be used as a decorator for adding commands to
113 113 that command table. This accepts multiple arguments to define a command.
114 114
115 115 The first argument is the command name (as bytes).
116 116
117 117 The `options` keyword argument is an iterable of tuples defining command
118 118 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
119 119 tuple.
120 120
121 121 The `synopsis` argument defines a short, one line summary of how to use the
122 122 command. This shows up in the help output.
123 123
124 124 There are three arguments that control what repository (if any) is found
125 125 and passed to the decorated function: `norepo`, `optionalrepo`, and
126 126 `inferrepo`.
127 127
128 128 The `norepo` argument defines whether the command does not require a
129 129 local repository. Most commands operate against a repository, thus the
130 130 default is False. When True, no repository will be passed.
131 131
132 132 The `optionalrepo` argument defines whether the command optionally requires
133 133 a local repository. If no repository can be found, None will be passed
134 134 to the decorated function.
135 135
136 136 The `inferrepo` argument defines whether to try to find a repository from
137 137 the command line arguments. If True, arguments will be examined for
138 138 potential repository locations. See ``findrepo()``. If a repository is
139 139 found, it will be used and passed to the decorated function.
140 140
141 141 There are three constants in the class which tells what type of the command
142 142 that is. That information will be helpful at various places. It will be also
143 143 be used to decide what level of access the command has on hidden commits.
144 144 The constants are:
145 145
146 146 `unrecoverablewrite` is for those write commands which can't be recovered
147 147 like push.
148 148 `recoverablewrite` is for write commands which can be recovered like commit.
149 149 `readonly` is for commands which are read only.
150 150
151 151 The signature of the decorated function looks like this:
152 152 def cmd(ui[, repo] [, <args>] [, <options>])
153 153
154 154 `repo` is required if `norepo` is False.
155 155 `<args>` are positional args (or `*args`) arguments, of non-option
156 156 arguments from the command line.
157 157 `<options>` are keyword arguments (or `**options`) of option arguments
158 158 from the command line.
159 159
160 160 See the WritingExtensions and MercurialApi documentation for more exhaustive
161 161 descriptions and examples.
162 162 """
163 163
164 164 unrecoverablewrite = "unrecoverable"
165 165 recoverablewrite = "recoverable"
166 166 readonly = "readonly"
167 167
168 168 possiblecmdtypes = {unrecoverablewrite, recoverablewrite, readonly}
169 169
170 170 def _doregister(self, func, name, options=(), synopsis=None,
171 171 norepo=False, optionalrepo=False, inferrepo=False,
172 172 cmdtype=unrecoverablewrite):
173 173
174 174 if cmdtype not in self.possiblecmdtypes:
175 175 raise error.ProgrammingError("unknown cmdtype value '%s' for "
176 176 "'%s' command" % (cmdtype, name))
177 177 func.norepo = norepo
178 178 func.optionalrepo = optionalrepo
179 179 func.inferrepo = inferrepo
180 180 func.cmdtype = cmdtype
181 181 if synopsis:
182 182 self._table[name] = func, list(options), synopsis
183 183 else:
184 184 self._table[name] = func, list(options)
185 185 return func
186 186
187 187 class revsetpredicate(_funcregistrarbase):
188 188 """Decorator to register revset predicate
189 189
190 190 Usage::
191 191
192 192 revsetpredicate = registrar.revsetpredicate()
193 193
194 194 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
195 195 def mypredicatefunc(repo, subset, x):
196 196 '''Explanation of this revset predicate ....
197 197 '''
198 198 pass
199 199
200 200 The first string argument is used also in online help.
201 201
202 202 Optional argument 'safe' indicates whether a predicate is safe for
203 203 DoS attack (False by default).
204 204
205 205 Optional argument 'takeorder' indicates whether a predicate function
206 206 takes ordering policy as the last argument.
207 207
208 208 Optional argument 'weight' indicates the estimated run-time cost, useful
209 209 for static optimization, default is 1. Higher weight means more expensive.
210 210 Usually, revsets that are fast and return only one revision has a weight of
211 211 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
212 212 changelog have weight 10 (ex. author); revsets reading manifest deltas have
213 213 weight 30 (ex. adds); revset reading manifest contents have weight 100
214 214 (ex. contains). Note: those values are flexible. If the revset has a
215 215 same big-O time complexity as 'contains', but with a smaller constant, it
216 216 might have a weight of 90.
217 217
218 218 'revsetpredicate' instance in example above can be used to
219 219 decorate multiple functions.
220 220
221 221 Decorated functions are registered automatically at loading
222 222 extension, if an instance named as 'revsetpredicate' is used for
223 223 decorating in extension.
224 224
225 225 Otherwise, explicit 'revset.loadpredicate()' is needed.
226 226 """
227 227 _getname = _funcregistrarbase._parsefuncdecl
228 228 _docformat = "``%s``\n %s"
229 229
230 230 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
231 231 func._safe = safe
232 232 func._takeorder = takeorder
233 233 func._weight = weight
234 234
235 235 class filesetpredicate(_funcregistrarbase):
236 236 """Decorator to register fileset predicate
237 237
238 238 Usage::
239 239
240 240 filesetpredicate = registrar.filesetpredicate()
241 241
242 242 @filesetpredicate('mypredicate()')
243 243 def mypredicatefunc(mctx, x):
244 244 '''Explanation of this fileset predicate ....
245 245 '''
246 246 pass
247 247
248 248 The first string argument is used also in online help.
249 249
250 250 Optional argument 'callstatus' indicates whether a predicate
251 251 implies 'matchctx.status()' at runtime or not (False, by
252 252 default).
253 253
254 254 Optional argument 'callexisting' indicates whether a predicate
255 255 implies 'matchctx.existing()' at runtime or not (False, by
256 256 default).
257 257
258 258 'filesetpredicate' instance in example above can be used to
259 259 decorate multiple functions.
260 260
261 261 Decorated functions are registered automatically at loading
262 262 extension, if an instance named as 'filesetpredicate' is used for
263 263 decorating in extension.
264 264
265 265 Otherwise, explicit 'fileset.loadpredicate()' is needed.
266 266 """
267 267 _getname = _funcregistrarbase._parsefuncdecl
268 268 _docformat = "``%s``\n %s"
269 269
270 270 def _extrasetup(self, name, func, callstatus=False, callexisting=False):
271 271 func._callstatus = callstatus
272 272 func._callexisting = callexisting
273 273
274 274 class _templateregistrarbase(_funcregistrarbase):
275 275 """Base of decorator to register functions as template specific one
276 276 """
277 277 _docformat = ":%s: %s"
278 278
279 279 class templatekeyword(_templateregistrarbase):
280 280 """Decorator to register template keyword
281 281
282 282 Usage::
283 283
284 284 templatekeyword = registrar.templatekeyword()
285 285
286 286 # new API (since Mercurial 4.6)
287 287 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
288 288 def mykeywordfunc(context, mapping):
289 289 '''Explanation of this template keyword ....
290 290 '''
291 291 pass
292 292
293 293 # old API
294 294 @templatekeyword('mykeyword')
295 295 def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
296 296 '''Explanation of this template keyword ....
297 297 '''
298 298 pass
299 299
300 300 The first string argument is used also in online help.
301 301
302 302 Optional argument 'requires' should be a collection of resource names
303 303 which the template keyword depends on. This also serves as a flag to
304 304 switch to the new API. If 'requires' is unspecified, all template
305 305 keywords and resources are expanded to the function arguments.
306 306
307 307 'templatekeyword' instance in example above can be used to
308 308 decorate multiple functions.
309 309
310 310 Decorated functions are registered automatically at loading
311 311 extension, if an instance named as 'templatekeyword' is used for
312 312 decorating in extension.
313 313
314 314 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
315 315 """
316 316
317 317 def _extrasetup(self, name, func, requires=None):
318 318 func._requires = requires
319 319
320 320 class templatefilter(_templateregistrarbase):
321 321 """Decorator to register template filer
322 322
323 323 Usage::
324 324
325 325 templatefilter = registrar.templatefilter()
326 326
327 327 @templatefilter('myfilter')
328 328 def myfilterfunc(text):
329 329 '''Explanation of this template filter ....
330 330 '''
331 331 pass
332 332
333 333 The first string argument is used also in online help.
334 334
335 335 'templatefilter' instance in example above can be used to
336 336 decorate multiple functions.
337 337
338 338 Decorated functions are registered automatically at loading
339 339 extension, if an instance named as 'templatefilter' is used for
340 340 decorating in extension.
341 341
342 342 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
343 343 """
344 344
345 345 class templatefunc(_templateregistrarbase):
346 346 """Decorator to register template function
347 347
348 348 Usage::
349 349
350 350 templatefunc = registrar.templatefunc()
351 351
352 352 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3')
353 353 def myfuncfunc(context, mapping, args):
354 354 '''Explanation of this template function ....
355 355 '''
356 356 pass
357 357
358 358 The first string argument is used also in online help.
359 359
360 360 If optional 'argspec' is defined, the function will receive 'args' as
361 361 a dict of named arguments. Otherwise 'args' is a list of positional
362 362 arguments.
363 363
364 364 'templatefunc' instance in example above can be used to
365 365 decorate multiple functions.
366 366
367 367 Decorated functions are registered automatically at loading
368 368 extension, if an instance named as 'templatefunc' is used for
369 369 decorating in extension.
370 370
371 Otherwise, explicit 'templater.loadfunction()' is needed.
371 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
372 372 """
373 373 _getname = _funcregistrarbase._parsefuncdecl
374 374
375 375 def _extrasetup(self, name, func, argspec=None):
376 376 func._argspec = argspec
377 377
378 378 class internalmerge(_funcregistrarbase):
379 379 """Decorator to register in-process merge tool
380 380
381 381 Usage::
382 382
383 383 internalmerge = registrar.internalmerge()
384 384
385 385 @internalmerge('mymerge', internalmerge.mergeonly,
386 386 onfailure=None, precheck=None):
387 387 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
388 388 toolconf, files, labels=None):
389 389 '''Explanation of this internal merge tool ....
390 390 '''
391 391 return 1, False # means "conflicted", "no deletion needed"
392 392
393 393 The first string argument is used to compose actual merge tool name,
394 394 ":name" and "internal:name" (the latter is historical one).
395 395
396 396 The second argument is one of merge types below:
397 397
398 398 ========== ======== ======== =========
399 399 merge type precheck premerge fullmerge
400 400 ========== ======== ======== =========
401 401 nomerge x x x
402 402 mergeonly o x o
403 403 fullmerge o o o
404 404 ========== ======== ======== =========
405 405
406 406 Optional argument 'onfailure' is the format of warning message
407 407 to be used at failure of merging (target filename is specified
408 408 at formatting). Or, None or so, if warning message should be
409 409 suppressed.
410 410
411 411 Optional argument 'precheck' is the function to be used
412 412 before actual invocation of internal merge tool itself.
413 413 It takes as same arguments as internal merge tool does, other than
414 414 'files' and 'labels'. If it returns false value, merging is aborted
415 415 immediately (and file is marked as "unresolved").
416 416
417 417 'internalmerge' instance in example above can be used to
418 418 decorate multiple functions.
419 419
420 420 Decorated functions are registered automatically at loading
421 421 extension, if an instance named as 'internalmerge' is used for
422 422 decorating in extension.
423 423
424 424 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
425 425 """
426 426 _docformat = "``:%s``\n %s"
427 427
428 428 # merge type definitions:
429 429 nomerge = None
430 430 mergeonly = 'mergeonly' # just the full merge, no premerge
431 431 fullmerge = 'fullmerge' # both premerge and merge
432 432
433 433 def _extrasetup(self, name, func, mergetype,
434 434 onfailure=None, precheck=None):
435 435 func.mergetype = mergetype
436 436 func.onfailure = onfailure
437 437 func.precheck = precheck
This diff has been collapsed as it changes many lines, (784 lines changed) Show them Hide them
@@ -1,1444 +1,664
1 # templater.py - template expansion for output
1 # templatefuncs.py - common template functions
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import
9 9
10 import os
11 10 import re
12 11
13 12 from .i18n import _
14 13 from . import (
15 14 color,
16 config,
17 15 encoding,
18 16 error,
19 17 minirst,
20 18 obsutil,
21 parser,
22 19 pycompat,
23 20 registrar,
24 21 revset as revsetmod,
25 22 revsetlang,
26 23 scmutil,
27 24 templatefilters,
28 25 templatekw,
29 26 templateutil,
30 27 util,
31 28 )
32 29 from .utils import dateutil
33 30
34 31 evalrawexp = templateutil.evalrawexp
35 32 evalfuncarg = templateutil.evalfuncarg
36 33 evalboolean = templateutil.evalboolean
37 34 evalinteger = templateutil.evalinteger
38 35 evalstring = templateutil.evalstring
39 36 evalstringliteral = templateutil.evalstringliteral
40 37 evalastype = templateutil.evalastype
41 38
42 # template parsing
43
44 elements = {
45 # token-type: binding-strength, primary, prefix, infix, suffix
46 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
47 ".": (18, None, None, (".", 18), None),
48 "%": (15, None, None, ("%", 15), None),
49 "|": (15, None, None, ("|", 15), None),
50 "*": (5, None, None, ("*", 5), None),
51 "/": (5, None, None, ("/", 5), None),
52 "+": (4, None, None, ("+", 4), None),
53 "-": (4, None, ("negate", 19), ("-", 4), None),
54 "=": (3, None, None, ("keyvalue", 3), None),
55 ",": (2, None, None, ("list", 2), None),
56 ")": (0, None, None, None, None),
57 "integer": (0, "integer", None, None, None),
58 "symbol": (0, "symbol", None, None, None),
59 "string": (0, "string", None, None, None),
60 "template": (0, "template", None, None, None),
61 "end": (0, None, None, None, None),
62 }
63
64 def tokenize(program, start, end, term=None):
65 """Parse a template expression into a stream of tokens, which must end
66 with term if specified"""
67 pos = start
68 program = pycompat.bytestr(program)
69 while pos < end:
70 c = program[pos]
71 if c.isspace(): # skip inter-token whitespace
72 pass
73 elif c in "(=,).%|+-*/": # handle simple operators
74 yield (c, None, pos)
75 elif c in '"\'': # handle quoted templates
76 s = pos + 1
77 data, pos = _parsetemplate(program, s, end, c)
78 yield ('template', data, s)
79 pos -= 1
80 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
81 # handle quoted strings
82 c = program[pos + 1]
83 s = pos = pos + 2
84 while pos < end: # find closing quote
85 d = program[pos]
86 if d == '\\': # skip over escaped characters
87 pos += 2
88 continue
89 if d == c:
90 yield ('string', program[s:pos], s)
91 break
92 pos += 1
93 else:
94 raise error.ParseError(_("unterminated string"), s)
95 elif c.isdigit():
96 s = pos
97 while pos < end:
98 d = program[pos]
99 if not d.isdigit():
100 break
101 pos += 1
102 yield ('integer', program[s:pos], s)
103 pos -= 1
104 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
105 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
106 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
107 # where some of nested templates were preprocessed as strings and
108 # then compiled. therefore, \"...\" was allowed. (issue4733)
109 #
110 # processing flow of _evalifliteral() at 5ab28a2e9962:
111 # outer template string -> stringify() -> compiletemplate()
112 # ------------------------ ------------ ------------------
113 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
114 # ~~~~~~~~
115 # escaped quoted string
116 if c == 'r':
117 pos += 1
118 token = 'string'
119 else:
120 token = 'template'
121 quote = program[pos:pos + 2]
122 s = pos = pos + 2
123 while pos < end: # find closing escaped quote
124 if program.startswith('\\\\\\', pos, end):
125 pos += 4 # skip over double escaped characters
126 continue
127 if program.startswith(quote, pos, end):
128 # interpret as if it were a part of an outer string
129 data = parser.unescapestr(program[s:pos])
130 if token == 'template':
131 data = _parsetemplate(data, 0, len(data))[0]
132 yield (token, data, s)
133 pos += 1
134 break
135 pos += 1
136 else:
137 raise error.ParseError(_("unterminated string"), s)
138 elif c.isalnum() or c in '_':
139 s = pos
140 pos += 1
141 while pos < end: # find end of symbol
142 d = program[pos]
143 if not (d.isalnum() or d == "_"):
144 break
145 pos += 1
146 sym = program[s:pos]
147 yield ('symbol', sym, s)
148 pos -= 1
149 elif c == term:
150 yield ('end', None, pos)
151 return
152 else:
153 raise error.ParseError(_("syntax error"), pos)
154 pos += 1
155 if term:
156 raise error.ParseError(_("unterminated template expansion"), start)
157 yield ('end', None, pos)
158
159 def _parsetemplate(tmpl, start, stop, quote=''):
160 r"""
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
162 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
163 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
164 ([('string', 'foo'), ('symbol', 'bar')], 9)
165 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
166 ([('string', 'foo')], 4)
167 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
168 ([('string', 'foo"'), ('string', 'bar')], 9)
169 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
170 ([('string', 'foo\\')], 6)
171 """
172 parsed = []
173 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
174 if typ == 'string':
175 parsed.append((typ, val))
176 elif typ == 'template':
177 parsed.append(val)
178 elif typ == 'end':
179 return parsed, pos
180 else:
181 raise error.ProgrammingError('unexpected type: %s' % typ)
182 raise error.ProgrammingError('unterminated scanning of template')
183
184 def scantemplate(tmpl, raw=False):
185 r"""Scan (type, start, end) positions of outermost elements in template
186
187 If raw=True, a backslash is not taken as an escape character just like
188 r'' string in Python. Note that this is different from r'' literal in
189 template in that no template fragment can appear in r'', e.g. r'{foo}'
190 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
191 'foo'.
192
193 >>> list(scantemplate(b'foo{bar}"baz'))
194 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
195 >>> list(scantemplate(b'outer{"inner"}outer'))
196 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
197 >>> list(scantemplate(b'foo\\{escaped}'))
198 [('string', 0, 5), ('string', 5, 13)]
199 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
200 [('string', 0, 4), ('template', 4, 13)]
201 """
202 last = None
203 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
204 if last:
205 yield last + (pos,)
206 if typ == 'end':
207 return
208 else:
209 last = (typ, pos)
210 raise error.ProgrammingError('unterminated scanning of template')
211
212 def _scantemplate(tmpl, start, stop, quote='', raw=False):
213 """Parse template string into chunks of strings and template expressions"""
214 sepchars = '{' + quote
215 unescape = [parser.unescapestr, pycompat.identity][raw]
216 pos = start
217 p = parser.parser(elements)
218 try:
219 while pos < stop:
220 n = min((tmpl.find(c, pos, stop) for c in sepchars),
221 key=lambda n: (n < 0, n))
222 if n < 0:
223 yield ('string', unescape(tmpl[pos:stop]), pos)
224 pos = stop
225 break
226 c = tmpl[n:n + 1]
227 bs = 0 # count leading backslashes
228 if not raw:
229 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
230 if bs % 2 == 1:
231 # escaped (e.g. '\{', '\\\{', but not '\\{')
232 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
233 pos = n + 1
234 continue
235 if n > pos:
236 yield ('string', unescape(tmpl[pos:n]), pos)
237 if c == quote:
238 yield ('end', None, n + 1)
239 return
240
241 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
242 if not tmpl.startswith('}', pos):
243 raise error.ParseError(_("invalid token"), pos)
244 yield ('template', parseres, n)
245 pos += 1
246
247 if quote:
248 raise error.ParseError(_("unterminated string"), start)
249 except error.ParseError as inst:
250 if len(inst.args) > 1: # has location
251 loc = inst.args[1]
252 # Offset the caret location by the number of newlines before the
253 # location of the error, since we will replace one-char newlines
254 # with the two-char literal r'\n'.
255 offset = tmpl[:loc].count('\n')
256 tmpl = tmpl.replace('\n', br'\n')
257 # We want the caret to point to the place in the template that
258 # failed to parse, but in a hint we get a open paren at the
259 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
260 # to line up the caret with the location of the error.
261 inst.hint = (tmpl + '\n'
262 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
263 raise
264 yield ('end', None, pos)
265
266 def _unnesttemplatelist(tree):
267 """Expand list of templates to node tuple
268
269 >>> def f(tree):
270 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
271 >>> f((b'template', []))
272 (string '')
273 >>> f((b'template', [(b'string', b'foo')]))
274 (string 'foo')
275 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
276 (template
277 (string 'foo')
278 (symbol 'rev'))
279 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
280 (template
281 (symbol 'rev'))
282 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
283 (string 'foo')
284 """
285 if not isinstance(tree, tuple):
286 return tree
287 op = tree[0]
288 if op != 'template':
289 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
290
291 assert len(tree) == 2
292 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
293 if not xs:
294 return ('string', '') # empty template ""
295 elif len(xs) == 1 and xs[0][0] == 'string':
296 return xs[0] # fast path for string with no template fragment "x"
297 else:
298 return (op,) + xs
299
300 def parse(tmpl):
301 """Parse template string into tree"""
302 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
303 assert pos == len(tmpl), 'unquoted template should be consumed'
304 return _unnesttemplatelist(('template', parsed))
305
306 def _parseexpr(expr):
307 """Parse a template expression into tree
308
309 >>> _parseexpr(b'"foo"')
310 ('string', 'foo')
311 >>> _parseexpr(b'foo(bar)')
312 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
313 >>> _parseexpr(b'foo(')
314 Traceback (most recent call last):
315 ...
316 ParseError: ('not a prefix: end', 4)
317 >>> _parseexpr(b'"foo" "bar"')
318 Traceback (most recent call last):
319 ...
320 ParseError: ('invalid token', 7)
321 """
322 p = parser.parser(elements)
323 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
324 if pos != len(expr):
325 raise error.ParseError(_('invalid token'), pos)
326 return _unnesttemplatelist(tree)
327
328 def prettyformat(tree):
329 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
330
331 def compileexp(exp, context, curmethods):
332 """Compile parsed template tree to (func, data) pair"""
333 if not exp:
334 raise error.ParseError(_("missing argument"))
335 t = exp[0]
336 if t in curmethods:
337 return curmethods[t](exp, context)
338 raise error.ParseError(_("unknown method '%s'") % t)
339
340 # template evaluation
341
342 def getsymbol(exp):
343 if exp[0] == 'symbol':
344 return exp[1]
345 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
346
347 def getlist(x):
348 if not x:
349 return []
350 if x[0] == 'list':
351 return getlist(x[1]) + [x[2]]
352 return [x]
353
354 def gettemplate(exp, context):
355 """Compile given template tree or load named template from map file;
356 returns (func, data) pair"""
357 if exp[0] in ('template', 'string'):
358 return compileexp(exp, context, methods)
359 if exp[0] == 'symbol':
360 # unlike runsymbol(), here 'symbol' is always taken as template name
361 # even if it exists in mapping. this allows us to override mapping
362 # by web templates, e.g. 'changelogtag' is redefined in map file.
363 return context._load(exp[1])
364 raise error.ParseError(_("expected template specifier"))
365
366 def _runrecursivesymbol(context, mapping, key):
367 raise error.Abort(_("recursive reference '%s' in template") % key)
368
369 def buildtemplate(exp, context):
370 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
371 return (templateutil.runtemplate, ctmpl)
372
373 def buildfilter(exp, context):
374 n = getsymbol(exp[2])
375 if n in context._filters:
376 filt = context._filters[n]
377 arg = compileexp(exp[1], context, methods)
378 return (templateutil.runfilter, (arg, filt))
379 if n in context._funcs:
380 f = context._funcs[n]
381 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
382 return (f, args)
383 raise error.ParseError(_("unknown function '%s'") % n)
384
385 def buildmap(exp, context):
386 darg = compileexp(exp[1], context, methods)
387 targ = gettemplate(exp[2], context)
388 return (templateutil.runmap, (darg, targ))
389
390 def buildmember(exp, context):
391 darg = compileexp(exp[1], context, methods)
392 memb = getsymbol(exp[2])
393 return (templateutil.runmember, (darg, memb))
394
395 def buildnegate(exp, context):
396 arg = compileexp(exp[1], context, exprmethods)
397 return (templateutil.runnegate, arg)
398
399 def buildarithmetic(exp, context, func):
400 left = compileexp(exp[1], context, exprmethods)
401 right = compileexp(exp[2], context, exprmethods)
402 return (templateutil.runarithmetic, (func, left, right))
403
404 def buildfunc(exp, context):
405 n = getsymbol(exp[1])
406 if n in context._funcs:
407 f = context._funcs[n]
408 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
409 return (f, args)
410 if n in context._filters:
411 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
412 if len(args) != 1:
413 raise error.ParseError(_("filter %s expects one argument") % n)
414 f = context._filters[n]
415 return (templateutil.runfilter, (args[0], f))
416 raise error.ParseError(_("unknown function '%s'") % n)
417
418 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
419 """Compile parsed tree of function arguments into list or dict of
420 (func, data) pairs
421
422 >>> context = engine(lambda t: (runsymbol, t))
423 >>> def fargs(expr, argspec):
424 ... x = _parseexpr(expr)
425 ... n = getsymbol(x[1])
426 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
427 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
428 ['l', 'k']
429 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
430 >>> list(args.keys()), list(args[b'opts'].keys())
431 (['opts'], ['opts', 'k'])
432 """
433 def compiledict(xs):
434 return util.sortdict((k, compileexp(x, context, curmethods))
435 for k, x in xs.iteritems())
436 def compilelist(xs):
437 return [compileexp(x, context, curmethods) for x in xs]
438
439 if not argspec:
440 # filter or function with no argspec: return list of positional args
441 return compilelist(getlist(exp))
442
443 # function with argspec: return dict of named args
444 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
445 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
446 keyvaluenode='keyvalue', keynode='symbol')
447 compargs = util.sortdict()
448 if varkey:
449 compargs[varkey] = compilelist(treeargs.pop(varkey))
450 if optkey:
451 compargs[optkey] = compiledict(treeargs.pop(optkey))
452 compargs.update(compiledict(treeargs))
453 return compargs
454
455 def buildkeyvaluepair(exp, content):
456 raise error.ParseError(_("can't use a key-value pair in this context"))
457
458 39 # dict of template built-in functions
459 40 funcs = {}
460
461 41 templatefunc = registrar.templatefunc(funcs)
462 42
463 43 @templatefunc('date(date[, fmt])')
464 44 def date(context, mapping, args):
465 45 """Format a date. See :hg:`help dates` for formatting
466 46 strings. The default is a Unix date format, including the timezone:
467 47 "Mon Sep 04 15:13:13 2006 0700"."""
468 48 if not (1 <= len(args) <= 2):
469 49 # i18n: "date" is a keyword
470 50 raise error.ParseError(_("date expects one or two arguments"))
471 51
472 52 date = evalfuncarg(context, mapping, args[0])
473 53 fmt = None
474 54 if len(args) == 2:
475 55 fmt = evalstring(context, mapping, args[1])
476 56 try:
477 57 if fmt is None:
478 58 return dateutil.datestr(date)
479 59 else:
480 60 return dateutil.datestr(date, fmt)
481 61 except (TypeError, ValueError):
482 62 # i18n: "date" is a keyword
483 63 raise error.ParseError(_("date expects a date information"))
484 64
485 65 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
486 66 def dict_(context, mapping, args):
487 67 """Construct a dict from key-value pairs. A key may be omitted if
488 68 a value expression can provide an unambiguous name."""
489 69 data = util.sortdict()
490 70
491 71 for v in args['args']:
492 72 k = templateutil.findsymbolicname(v)
493 73 if not k:
494 74 raise error.ParseError(_('dict key cannot be inferred'))
495 75 if k in data or k in args['kwargs']:
496 76 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
497 77 data[k] = evalfuncarg(context, mapping, v)
498 78
499 79 data.update((k, evalfuncarg(context, mapping, v))
500 80 for k, v in args['kwargs'].iteritems())
501 81 return templateutil.hybriddict(data)
502 82
503 83 @templatefunc('diff([includepattern [, excludepattern]])')
504 84 def diff(context, mapping, args):
505 85 """Show a diff, optionally
506 86 specifying files to include or exclude."""
507 87 if len(args) > 2:
508 88 # i18n: "diff" is a keyword
509 89 raise error.ParseError(_("diff expects zero, one, or two arguments"))
510 90
511 91 def getpatterns(i):
512 92 if i < len(args):
513 93 s = evalstring(context, mapping, args[i]).strip()
514 94 if s:
515 95 return [s]
516 96 return []
517 97
518 98 ctx = context.resource(mapping, 'ctx')
519 99 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
520 100
521 101 return ''.join(chunks)
522 102
523 103 @templatefunc('extdata(source)', argspec='source')
524 104 def extdata(context, mapping, args):
525 105 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
526 106 if 'source' not in args:
527 107 # i18n: "extdata" is a keyword
528 108 raise error.ParseError(_('extdata expects one argument'))
529 109
530 110 source = evalstring(context, mapping, args['source'])
531 111 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
532 112 ctx = context.resource(mapping, 'ctx')
533 113 if source in cache:
534 114 data = cache[source]
535 115 else:
536 116 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
537 117 return data.get(ctx.rev(), '')
538 118
539 119 @templatefunc('files(pattern)')
540 120 def files(context, mapping, args):
541 121 """All files of the current changeset matching the pattern. See
542 122 :hg:`help patterns`."""
543 123 if not len(args) == 1:
544 124 # i18n: "files" is a keyword
545 125 raise error.ParseError(_("files expects one argument"))
546 126
547 127 raw = evalstring(context, mapping, args[0])
548 128 ctx = context.resource(mapping, 'ctx')
549 129 m = ctx.match([raw])
550 130 files = list(ctx.matches(m))
551 131 return templateutil.compatlist(context, mapping, "file", files)
552 132
553 133 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
554 134 def fill(context, mapping, args):
555 135 """Fill many
556 136 paragraphs with optional indentation. See the "fill" filter."""
557 137 if not (1 <= len(args) <= 4):
558 138 # i18n: "fill" is a keyword
559 139 raise error.ParseError(_("fill expects one to four arguments"))
560 140
561 141 text = evalstring(context, mapping, args[0])
562 142 width = 76
563 143 initindent = ''
564 144 hangindent = ''
565 145 if 2 <= len(args) <= 4:
566 146 width = evalinteger(context, mapping, args[1],
567 147 # i18n: "fill" is a keyword
568 148 _("fill expects an integer width"))
569 149 try:
570 150 initindent = evalstring(context, mapping, args[2])
571 151 hangindent = evalstring(context, mapping, args[3])
572 152 except IndexError:
573 153 pass
574 154
575 155 return templatefilters.fill(text, width, initindent, hangindent)
576 156
577 157 @templatefunc('formatnode(node)')
578 158 def formatnode(context, mapping, args):
579 159 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
580 160 if len(args) != 1:
581 161 # i18n: "formatnode" is a keyword
582 162 raise error.ParseError(_("formatnode expects one argument"))
583 163
584 164 ui = context.resource(mapping, 'ui')
585 165 node = evalstring(context, mapping, args[0])
586 166 if ui.debugflag:
587 167 return node
588 168 return templatefilters.short(node)
589 169
590 170 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
591 171 argspec='text width fillchar left')
592 172 def pad(context, mapping, args):
593 173 """Pad text with a
594 174 fill character."""
595 175 if 'text' not in args or 'width' not in args:
596 176 # i18n: "pad" is a keyword
597 177 raise error.ParseError(_("pad() expects two to four arguments"))
598 178
599 179 width = evalinteger(context, mapping, args['width'],
600 180 # i18n: "pad" is a keyword
601 181 _("pad() expects an integer width"))
602 182
603 183 text = evalstring(context, mapping, args['text'])
604 184
605 185 left = False
606 186 fillchar = ' '
607 187 if 'fillchar' in args:
608 188 fillchar = evalstring(context, mapping, args['fillchar'])
609 189 if len(color.stripeffects(fillchar)) != 1:
610 190 # i18n: "pad" is a keyword
611 191 raise error.ParseError(_("pad() expects a single fill character"))
612 192 if 'left' in args:
613 193 left = evalboolean(context, mapping, args['left'])
614 194
615 195 fillwidth = width - encoding.colwidth(color.stripeffects(text))
616 196 if fillwidth <= 0:
617 197 return text
618 198 if left:
619 199 return fillchar * fillwidth + text
620 200 else:
621 201 return text + fillchar * fillwidth
622 202
623 203 @templatefunc('indent(text, indentchars[, firstline])')
624 204 def indent(context, mapping, args):
625 205 """Indents all non-empty lines
626 206 with the characters given in the indentchars string. An optional
627 207 third parameter will override the indent for the first line only
628 208 if present."""
629 209 if not (2 <= len(args) <= 3):
630 210 # i18n: "indent" is a keyword
631 211 raise error.ParseError(_("indent() expects two or three arguments"))
632 212
633 213 text = evalstring(context, mapping, args[0])
634 214 indent = evalstring(context, mapping, args[1])
635 215
636 216 if len(args) == 3:
637 217 firstline = evalstring(context, mapping, args[2])
638 218 else:
639 219 firstline = indent
640 220
641 221 # the indent function doesn't indent the first line, so we do it here
642 222 return templatefilters.indent(firstline + text, indent)
643 223
644 224 @templatefunc('get(dict, key)')
645 225 def get(context, mapping, args):
646 226 """Get an attribute/key from an object. Some keywords
647 227 are complex types. This function allows you to obtain the value of an
648 228 attribute on these types."""
649 229 if len(args) != 2:
650 230 # i18n: "get" is a keyword
651 231 raise error.ParseError(_("get() expects two arguments"))
652 232
653 233 dictarg = evalfuncarg(context, mapping, args[0])
654 234 if not util.safehasattr(dictarg, 'get'):
655 235 # i18n: "get" is a keyword
656 236 raise error.ParseError(_("get() expects a dict as first argument"))
657 237
658 238 key = evalfuncarg(context, mapping, args[1])
659 239 return templateutil.getdictitem(dictarg, key)
660 240
661 241 @templatefunc('if(expr, then[, else])')
662 242 def if_(context, mapping, args):
663 243 """Conditionally execute based on the result of
664 244 an expression."""
665 245 if not (2 <= len(args) <= 3):
666 246 # i18n: "if" is a keyword
667 247 raise error.ParseError(_("if expects two or three arguments"))
668 248
669 249 test = evalboolean(context, mapping, args[0])
670 250 if test:
671 251 yield evalrawexp(context, mapping, args[1])
672 252 elif len(args) == 3:
673 253 yield evalrawexp(context, mapping, args[2])
674 254
675 255 @templatefunc('ifcontains(needle, haystack, then[, else])')
676 256 def ifcontains(context, mapping, args):
677 257 """Conditionally execute based
678 258 on whether the item "needle" is in "haystack"."""
679 259 if not (3 <= len(args) <= 4):
680 260 # i18n: "ifcontains" is a keyword
681 261 raise error.ParseError(_("ifcontains expects three or four arguments"))
682 262
683 263 haystack = evalfuncarg(context, mapping, args[1])
684 264 try:
685 265 needle = evalastype(context, mapping, args[0],
686 266 getattr(haystack, 'keytype', None) or bytes)
687 267 found = (needle in haystack)
688 268 except error.ParseError:
689 269 found = False
690 270
691 271 if found:
692 272 yield evalrawexp(context, mapping, args[2])
693 273 elif len(args) == 4:
694 274 yield evalrawexp(context, mapping, args[3])
695 275
696 276 @templatefunc('ifeq(expr1, expr2, then[, else])')
697 277 def ifeq(context, mapping, args):
698 278 """Conditionally execute based on
699 279 whether 2 items are equivalent."""
700 280 if not (3 <= len(args) <= 4):
701 281 # i18n: "ifeq" is a keyword
702 282 raise error.ParseError(_("ifeq expects three or four arguments"))
703 283
704 284 test = evalstring(context, mapping, args[0])
705 285 match = evalstring(context, mapping, args[1])
706 286 if test == match:
707 287 yield evalrawexp(context, mapping, args[2])
708 288 elif len(args) == 4:
709 289 yield evalrawexp(context, mapping, args[3])
710 290
711 291 @templatefunc('join(list, sep)')
712 292 def join(context, mapping, args):
713 293 """Join items in a list with a delimiter."""
714 294 if not (1 <= len(args) <= 2):
715 295 # i18n: "join" is a keyword
716 296 raise error.ParseError(_("join expects one or two arguments"))
717 297
718 298 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
719 299 # abuses generator as a keyword that returns a list of dicts.
720 300 joinset = evalrawexp(context, mapping, args[0])
721 301 joinset = templateutil.unwrapvalue(joinset)
722 302 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
723 303 joiner = " "
724 304 if len(args) > 1:
725 305 joiner = evalstring(context, mapping, args[1])
726 306
727 307 first = True
728 308 for x in pycompat.maybebytestr(joinset):
729 309 if first:
730 310 first = False
731 311 else:
732 312 yield joiner
733 313 yield joinfmt(x)
734 314
735 315 @templatefunc('label(label, expr)')
736 316 def label(context, mapping, args):
737 317 """Apply a label to generated content. Content with
738 318 a label applied can result in additional post-processing, such as
739 319 automatic colorization."""
740 320 if len(args) != 2:
741 321 # i18n: "label" is a keyword
742 322 raise error.ParseError(_("label expects two arguments"))
743 323
744 324 ui = context.resource(mapping, 'ui')
745 325 thing = evalstring(context, mapping, args[1])
746 326 # preserve unknown symbol as literal so effects like 'red', 'bold',
747 327 # etc. don't need to be quoted
748 328 label = evalstringliteral(context, mapping, args[0])
749 329
750 330 return ui.label(thing, label)
751 331
752 332 @templatefunc('latesttag([pattern])')
753 333 def latesttag(context, mapping, args):
754 334 """The global tags matching the given pattern on the
755 335 most recent globally tagged ancestor of this changeset.
756 336 If no such tags exist, the "{tag}" template resolves to
757 337 the string "null"."""
758 338 if len(args) > 1:
759 339 # i18n: "latesttag" is a keyword
760 340 raise error.ParseError(_("latesttag expects at most one argument"))
761 341
762 342 pattern = None
763 343 if len(args) == 1:
764 344 pattern = evalstring(context, mapping, args[0])
765 345 return templatekw.showlatesttags(context, mapping, pattern)
766 346
767 347 @templatefunc('localdate(date[, tz])')
768 348 def localdate(context, mapping, args):
769 349 """Converts a date to the specified timezone.
770 350 The default is local date."""
771 351 if not (1 <= len(args) <= 2):
772 352 # i18n: "localdate" is a keyword
773 353 raise error.ParseError(_("localdate expects one or two arguments"))
774 354
775 355 date = evalfuncarg(context, mapping, args[0])
776 356 try:
777 357 date = dateutil.parsedate(date)
778 358 except AttributeError: # not str nor date tuple
779 359 # i18n: "localdate" is a keyword
780 360 raise error.ParseError(_("localdate expects a date information"))
781 361 if len(args) >= 2:
782 362 tzoffset = None
783 363 tz = evalfuncarg(context, mapping, args[1])
784 364 if isinstance(tz, bytes):
785 365 tzoffset, remainder = dateutil.parsetimezone(tz)
786 366 if remainder:
787 367 tzoffset = None
788 368 if tzoffset is None:
789 369 try:
790 370 tzoffset = int(tz)
791 371 except (TypeError, ValueError):
792 372 # i18n: "localdate" is a keyword
793 373 raise error.ParseError(_("localdate expects a timezone"))
794 374 else:
795 375 tzoffset = dateutil.makedate()[1]
796 376 return (date[0], tzoffset)
797 377
798 378 @templatefunc('max(iterable)')
799 379 def max_(context, mapping, args, **kwargs):
800 380 """Return the max of an iterable"""
801 381 if len(args) != 1:
802 382 # i18n: "max" is a keyword
803 383 raise error.ParseError(_("max expects one argument"))
804 384
805 385 iterable = evalfuncarg(context, mapping, args[0])
806 386 try:
807 387 x = max(pycompat.maybebytestr(iterable))
808 388 except (TypeError, ValueError):
809 389 # i18n: "max" is a keyword
810 390 raise error.ParseError(_("max first argument should be an iterable"))
811 391 return templateutil.wraphybridvalue(iterable, x, x)
812 392
813 393 @templatefunc('min(iterable)')
814 394 def min_(context, mapping, args, **kwargs):
815 395 """Return the min of an iterable"""
816 396 if len(args) != 1:
817 397 # i18n: "min" is a keyword
818 398 raise error.ParseError(_("min expects one argument"))
819 399
820 400 iterable = evalfuncarg(context, mapping, args[0])
821 401 try:
822 402 x = min(pycompat.maybebytestr(iterable))
823 403 except (TypeError, ValueError):
824 404 # i18n: "min" is a keyword
825 405 raise error.ParseError(_("min first argument should be an iterable"))
826 406 return templateutil.wraphybridvalue(iterable, x, x)
827 407
828 408 @templatefunc('mod(a, b)')
829 409 def mod(context, mapping, args):
830 410 """Calculate a mod b such that a / b + a mod b == a"""
831 411 if not len(args) == 2:
832 412 # i18n: "mod" is a keyword
833 413 raise error.ParseError(_("mod expects two arguments"))
834 414
835 415 func = lambda a, b: a % b
836 416 return templateutil.runarithmetic(context, mapping,
837 417 (func, args[0], args[1]))
838 418
839 419 @templatefunc('obsfateoperations(markers)')
840 420 def obsfateoperations(context, mapping, args):
841 421 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
842 422 if len(args) != 1:
843 423 # i18n: "obsfateoperations" is a keyword
844 424 raise error.ParseError(_("obsfateoperations expects one argument"))
845 425
846 426 markers = evalfuncarg(context, mapping, args[0])
847 427
848 428 try:
849 429 data = obsutil.markersoperations(markers)
850 430 return templateutil.hybridlist(data, name='operation')
851 431 except (TypeError, KeyError):
852 432 # i18n: "obsfateoperations" is a keyword
853 433 errmsg = _("obsfateoperations first argument should be an iterable")
854 434 raise error.ParseError(errmsg)
855 435
856 436 @templatefunc('obsfatedate(markers)')
857 437 def obsfatedate(context, mapping, args):
858 438 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
859 439 if len(args) != 1:
860 440 # i18n: "obsfatedate" is a keyword
861 441 raise error.ParseError(_("obsfatedate expects one argument"))
862 442
863 443 markers = evalfuncarg(context, mapping, args[0])
864 444
865 445 try:
866 446 data = obsutil.markersdates(markers)
867 447 return templateutil.hybridlist(data, name='date', fmt='%d %d')
868 448 except (TypeError, KeyError):
869 449 # i18n: "obsfatedate" is a keyword
870 450 errmsg = _("obsfatedate first argument should be an iterable")
871 451 raise error.ParseError(errmsg)
872 452
873 453 @templatefunc('obsfateusers(markers)')
874 454 def obsfateusers(context, mapping, args):
875 455 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
876 456 if len(args) != 1:
877 457 # i18n: "obsfateusers" is a keyword
878 458 raise error.ParseError(_("obsfateusers expects one argument"))
879 459
880 460 markers = evalfuncarg(context, mapping, args[0])
881 461
882 462 try:
883 463 data = obsutil.markersusers(markers)
884 464 return templateutil.hybridlist(data, name='user')
885 465 except (TypeError, KeyError, ValueError):
886 466 # i18n: "obsfateusers" is a keyword
887 467 msg = _("obsfateusers first argument should be an iterable of "
888 468 "obsmakers")
889 469 raise error.ParseError(msg)
890 470
891 471 @templatefunc('obsfateverb(successors, markers)')
892 472 def obsfateverb(context, mapping, args):
893 473 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
894 474 if len(args) != 2:
895 475 # i18n: "obsfateverb" is a keyword
896 476 raise error.ParseError(_("obsfateverb expects two arguments"))
897 477
898 478 successors = evalfuncarg(context, mapping, args[0])
899 479 markers = evalfuncarg(context, mapping, args[1])
900 480
901 481 try:
902 482 return obsutil.obsfateverb(successors, markers)
903 483 except TypeError:
904 484 # i18n: "obsfateverb" is a keyword
905 485 errmsg = _("obsfateverb first argument should be countable")
906 486 raise error.ParseError(errmsg)
907 487
908 488 @templatefunc('relpath(path)')
909 489 def relpath(context, mapping, args):
910 490 """Convert a repository-absolute path into a filesystem path relative to
911 491 the current working directory."""
912 492 if len(args) != 1:
913 493 # i18n: "relpath" is a keyword
914 494 raise error.ParseError(_("relpath expects one argument"))
915 495
916 496 repo = context.resource(mapping, 'ctx').repo()
917 497 path = evalstring(context, mapping, args[0])
918 498 return repo.pathto(path)
919 499
920 500 @templatefunc('revset(query[, formatargs...])')
921 501 def revset(context, mapping, args):
922 502 """Execute a revision set query. See
923 503 :hg:`help revset`."""
924 504 if not len(args) > 0:
925 505 # i18n: "revset" is a keyword
926 506 raise error.ParseError(_("revset expects one or more arguments"))
927 507
928 508 raw = evalstring(context, mapping, args[0])
929 509 ctx = context.resource(mapping, 'ctx')
930 510 repo = ctx.repo()
931 511
932 512 def query(expr):
933 513 m = revsetmod.match(repo.ui, expr, repo=repo)
934 514 return m(repo)
935 515
936 516 if len(args) > 1:
937 517 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
938 518 revs = query(revsetlang.formatspec(raw, *formatargs))
939 519 revs = list(revs)
940 520 else:
941 521 cache = context.resource(mapping, 'cache')
942 522 revsetcache = cache.setdefault("revsetcache", {})
943 523 if raw in revsetcache:
944 524 revs = revsetcache[raw]
945 525 else:
946 526 revs = query(raw)
947 527 revs = list(revs)
948 528 revsetcache[raw] = revs
949 529 return templatekw.showrevslist(context, mapping, "revision", revs)
950 530
951 531 @templatefunc('rstdoc(text, style)')
952 532 def rstdoc(context, mapping, args):
953 533 """Format reStructuredText."""
954 534 if len(args) != 2:
955 535 # i18n: "rstdoc" is a keyword
956 536 raise error.ParseError(_("rstdoc expects two arguments"))
957 537
958 538 text = evalstring(context, mapping, args[0])
959 539 style = evalstring(context, mapping, args[1])
960 540
961 541 return minirst.format(text, style=style, keep=['verbose'])
962 542
963 543 @templatefunc('separate(sep, args)', argspec='sep *args')
964 544 def separate(context, mapping, args):
965 545 """Add a separator between non-empty arguments."""
966 546 if 'sep' not in args:
967 547 # i18n: "separate" is a keyword
968 548 raise error.ParseError(_("separate expects at least one argument"))
969 549
970 550 sep = evalstring(context, mapping, args['sep'])
971 551 first = True
972 552 for arg in args['args']:
973 553 argstr = evalstring(context, mapping, arg)
974 554 if not argstr:
975 555 continue
976 556 if first:
977 557 first = False
978 558 else:
979 559 yield sep
980 560 yield argstr
981 561
982 562 @templatefunc('shortest(node, minlength=4)')
983 563 def shortest(context, mapping, args):
984 564 """Obtain the shortest representation of
985 565 a node."""
986 566 if not (1 <= len(args) <= 2):
987 567 # i18n: "shortest" is a keyword
988 568 raise error.ParseError(_("shortest() expects one or two arguments"))
989 569
990 570 node = evalstring(context, mapping, args[0])
991 571
992 572 minlength = 4
993 573 if len(args) > 1:
994 574 minlength = evalinteger(context, mapping, args[1],
995 575 # i18n: "shortest" is a keyword
996 576 _("shortest() expects an integer minlength"))
997 577
998 578 # _partialmatch() of filtered changelog could take O(len(repo)) time,
999 579 # which would be unacceptably slow. so we look for hash collision in
1000 580 # unfiltered space, which means some hashes may be slightly longer.
1001 581 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1002 582 return cl.shortest(node, minlength)
1003 583
1004 584 @templatefunc('strip(text[, chars])')
1005 585 def strip(context, mapping, args):
1006 586 """Strip characters from a string. By default,
1007 587 strips all leading and trailing whitespace."""
1008 588 if not (1 <= len(args) <= 2):
1009 589 # i18n: "strip" is a keyword
1010 590 raise error.ParseError(_("strip expects one or two arguments"))
1011 591
1012 592 text = evalstring(context, mapping, args[0])
1013 593 if len(args) == 2:
1014 594 chars = evalstring(context, mapping, args[1])
1015 595 return text.strip(chars)
1016 596 return text.strip()
1017 597
1018 598 @templatefunc('sub(pattern, replacement, expression)')
1019 599 def sub(context, mapping, args):
1020 600 """Perform text substitution
1021 601 using regular expressions."""
1022 602 if len(args) != 3:
1023 603 # i18n: "sub" is a keyword
1024 604 raise error.ParseError(_("sub expects three arguments"))
1025 605
1026 606 pat = evalstring(context, mapping, args[0])
1027 607 rpl = evalstring(context, mapping, args[1])
1028 608 src = evalstring(context, mapping, args[2])
1029 609 try:
1030 610 patre = re.compile(pat)
1031 611 except re.error:
1032 612 # i18n: "sub" is a keyword
1033 613 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1034 614 try:
1035 615 yield patre.sub(rpl, src)
1036 616 except re.error:
1037 617 # i18n: "sub" is a keyword
1038 618 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1039 619
1040 620 @templatefunc('startswith(pattern, text)')
1041 621 def startswith(context, mapping, args):
1042 622 """Returns the value from the "text" argument
1043 623 if it begins with the content from the "pattern" argument."""
1044 624 if len(args) != 2:
1045 625 # i18n: "startswith" is a keyword
1046 626 raise error.ParseError(_("startswith expects two arguments"))
1047 627
1048 628 patn = evalstring(context, mapping, args[0])
1049 629 text = evalstring(context, mapping, args[1])
1050 630 if text.startswith(patn):
1051 631 return text
1052 632 return ''
1053 633
1054 634 @templatefunc('word(number, text[, separator])')
1055 635 def word(context, mapping, args):
1056 636 """Return the nth word from a string."""
1057 637 if not (2 <= len(args) <= 3):
1058 638 # i18n: "word" is a keyword
1059 639 raise error.ParseError(_("word expects two or three arguments, got %d")
1060 640 % len(args))
1061 641
1062 642 num = evalinteger(context, mapping, args[0],
1063 643 # i18n: "word" is a keyword
1064 644 _("word expects an integer index"))
1065 645 text = evalstring(context, mapping, args[1])
1066 646 if len(args) == 3:
1067 647 splitter = evalstring(context, mapping, args[2])
1068 648 else:
1069 649 splitter = None
1070 650
1071 651 tokens = text.split(splitter)
1072 652 if num >= len(tokens) or num < -len(tokens):
1073 653 return ''
1074 654 else:
1075 655 return tokens[num]
1076 656
1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1078 exprmethods = {
1079 "integer": lambda e, c: (templateutil.runinteger, e[1]),
1080 "string": lambda e, c: (templateutil.runstring, e[1]),
1081 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
1082 "template": buildtemplate,
1083 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1084 ".": buildmember,
1085 "|": buildfilter,
1086 "%": buildmap,
1087 "func": buildfunc,
1088 "keyvalue": buildkeyvaluepair,
1089 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1090 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1091 "negate": buildnegate,
1092 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1093 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1094 }
1095
1096 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1097 methods = exprmethods.copy()
1098 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1099
1100 class _aliasrules(parser.basealiasrules):
1101 """Parsing and expansion rule set of template aliases"""
1102 _section = _('template alias')
1103 _parse = staticmethod(_parseexpr)
1104
1105 @staticmethod
1106 def _trygetfunc(tree):
1107 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1108 None"""
1109 if tree[0] == 'func' and tree[1][0] == 'symbol':
1110 return tree[1][1], getlist(tree[2])
1111 if tree[0] == '|' and tree[2][0] == 'symbol':
1112 return tree[2][1], [tree[1]]
1113
1114 def expandaliases(tree, aliases):
1115 """Return new tree of aliases are expanded"""
1116 aliasmap = _aliasrules.buildmap(aliases)
1117 return _aliasrules.expand(aliasmap, tree)
1118
1119 # template engine
1120
1121 def _flatten(thing):
1122 '''yield a single stream from a possibly nested set of iterators'''
1123 thing = templateutil.unwraphybrid(thing)
1124 if isinstance(thing, bytes):
1125 yield thing
1126 elif isinstance(thing, str):
1127 # We can only hit this on Python 3, and it's here to guard
1128 # against infinite recursion.
1129 raise error.ProgrammingError('Mercurial IO including templates is done'
1130 ' with bytes, not strings, got %r' % thing)
1131 elif thing is None:
1132 pass
1133 elif not util.safehasattr(thing, '__iter__'):
1134 yield pycompat.bytestr(thing)
1135 else:
1136 for i in thing:
1137 i = templateutil.unwraphybrid(i)
1138 if isinstance(i, bytes):
1139 yield i
1140 elif i is None:
1141 pass
1142 elif not util.safehasattr(i, '__iter__'):
1143 yield pycompat.bytestr(i)
1144 else:
1145 for j in _flatten(i):
1146 yield j
1147
1148 def unquotestring(s):
1149 '''unwrap quotes if any; otherwise returns unmodified string'''
1150 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1151 return s
1152 return s[1:-1]
1153
1154 class engine(object):
1155 '''template expansion engine.
1156
1157 template expansion works like this. a map file contains key=value
1158 pairs. if value is quoted, it is treated as string. otherwise, it
1159 is treated as name of template file.
1160
1161 templater is asked to expand a key in map. it looks up key, and
1162 looks for strings like this: {foo}. it expands {foo} by looking up
1163 foo in map, and substituting it. expansion is recursive: it stops
1164 when there is no more {foo} to replace.
1165
1166 expansion also allows formatting and filtering.
1167
1168 format uses key to expand each item in list. syntax is
1169 {key%format}.
1170
1171 filter uses function to transform value. syntax is
1172 {key|filter1|filter2|...}.'''
1173
1174 def __init__(self, loader, filters=None, defaults=None, resources=None,
1175 aliases=()):
1176 self._loader = loader
1177 if filters is None:
1178 filters = {}
1179 self._filters = filters
1180 self._funcs = funcs # make this a parameter if needed
1181 if defaults is None:
1182 defaults = {}
1183 if resources is None:
1184 resources = {}
1185 self._defaults = defaults
1186 self._resources = resources
1187 self._aliasmap = _aliasrules.buildmap(aliases)
1188 self._cache = {} # key: (func, data)
1189
1190 def symbol(self, mapping, key):
1191 """Resolve symbol to value or function; None if nothing found"""
1192 v = None
1193 if key not in self._resources:
1194 v = mapping.get(key)
1195 if v is None:
1196 v = self._defaults.get(key)
1197 return v
1198
1199 def resource(self, mapping, key):
1200 """Return internal data (e.g. cache) used for keyword/function
1201 evaluation"""
1202 v = None
1203 if key in self._resources:
1204 v = mapping.get(key)
1205 if v is None:
1206 v = self._resources.get(key)
1207 if v is None:
1208 raise templateutil.ResourceUnavailable(
1209 _('template resource not available: %s') % key)
1210 return v
1211
1212 def _load(self, t):
1213 '''load, parse, and cache a template'''
1214 if t not in self._cache:
1215 # put poison to cut recursion while compiling 't'
1216 self._cache[t] = (_runrecursivesymbol, t)
1217 try:
1218 x = parse(self._loader(t))
1219 if self._aliasmap:
1220 x = _aliasrules.expand(self._aliasmap, x)
1221 self._cache[t] = compileexp(x, self, methods)
1222 except: # re-raises
1223 del self._cache[t]
1224 raise
1225 return self._cache[t]
1226
1227 def process(self, t, mapping):
1228 '''Perform expansion. t is name of map element to expand.
1229 mapping contains added elements for use during expansion. Is a
1230 generator.'''
1231 func, data = self._load(t)
1232 return _flatten(func(self, mapping, data))
1233
1234 engines = {'default': engine}
1235
1236 def stylelist():
1237 paths = templatepaths()
1238 if not paths:
1239 return _('no templates found, try `hg debuginstall` for more info')
1240 dirlist = os.listdir(paths[0])
1241 stylelist = []
1242 for file in dirlist:
1243 split = file.split(".")
1244 if split[-1] in ('orig', 'rej'):
1245 continue
1246 if split[0] == "map-cmdline":
1247 stylelist.append(split[1])
1248 return ", ".join(sorted(stylelist))
1249
1250 def _readmapfile(mapfile):
1251 """Load template elements from the given map file"""
1252 if not os.path.exists(mapfile):
1253 raise error.Abort(_("style '%s' not found") % mapfile,
1254 hint=_("available styles: %s") % stylelist())
1255
1256 base = os.path.dirname(mapfile)
1257 conf = config.config(includepaths=templatepaths())
1258 conf.read(mapfile, remap={'': 'templates'})
1259
1260 cache = {}
1261 tmap = {}
1262 aliases = []
1263
1264 val = conf.get('templates', '__base__')
1265 if val and val[0] not in "'\"":
1266 # treat as a pointer to a base class for this style
1267 path = util.normpath(os.path.join(base, val))
1268
1269 # fallback check in template paths
1270 if not os.path.exists(path):
1271 for p in templatepaths():
1272 p2 = util.normpath(os.path.join(p, val))
1273 if os.path.isfile(p2):
1274 path = p2
1275 break
1276 p3 = util.normpath(os.path.join(p2, "map"))
1277 if os.path.isfile(p3):
1278 path = p3
1279 break
1280
1281 cache, tmap, aliases = _readmapfile(path)
1282
1283 for key, val in conf['templates'].items():
1284 if not val:
1285 raise error.ParseError(_('missing value'),
1286 conf.source('templates', key))
1287 if val[0] in "'\"":
1288 if val[0] != val[-1]:
1289 raise error.ParseError(_('unmatched quotes'),
1290 conf.source('templates', key))
1291 cache[key] = unquotestring(val)
1292 elif key != '__base__':
1293 val = 'default', val
1294 if ':' in val[1]:
1295 val = val[1].split(':', 1)
1296 tmap[key] = val[0], os.path.join(base, val[1])
1297 aliases.extend(conf['templatealias'].items())
1298 return cache, tmap, aliases
1299
1300 class templater(object):
1301
1302 def __init__(self, filters=None, defaults=None, resources=None,
1303 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1304 """Create template engine optionally with preloaded template fragments
1305
1306 - ``filters``: a dict of functions to transform a value into another.
1307 - ``defaults``: a dict of symbol values/functions; may be overridden
1308 by a ``mapping`` dict.
1309 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1310 from user template; may be overridden by a ``mapping`` dict.
1311 - ``cache``: a dict of preloaded template fragments.
1312 - ``aliases``: a list of alias (name, replacement) pairs.
1313
1314 self.cache may be updated later to register additional template
1315 fragments.
1316 """
1317 if filters is None:
1318 filters = {}
1319 if defaults is None:
1320 defaults = {}
1321 if resources is None:
1322 resources = {}
1323 if cache is None:
1324 cache = {}
1325 self.cache = cache.copy()
1326 self.map = {}
1327 self.filters = templatefilters.filters.copy()
1328 self.filters.update(filters)
1329 self.defaults = defaults
1330 self._resources = {'templ': self}
1331 self._resources.update(resources)
1332 self._aliases = aliases
1333 self.minchunk, self.maxchunk = minchunk, maxchunk
1334 self.ecache = {}
1335
1336 @classmethod
1337 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1338 cache=None, minchunk=1024, maxchunk=65536):
1339 """Create templater from the specified map file"""
1340 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1341 cache, tmap, aliases = _readmapfile(mapfile)
1342 t.cache.update(cache)
1343 t.map = tmap
1344 t._aliases = aliases
1345 return t
1346
1347 def __contains__(self, key):
1348 return key in self.cache or key in self.map
1349
1350 def load(self, t):
1351 '''Get the template for the given template name. Use a local cache.'''
1352 if t not in self.cache:
1353 try:
1354 self.cache[t] = util.readfile(self.map[t][1])
1355 except KeyError as inst:
1356 raise templateutil.TemplateNotFound(
1357 _('"%s" not in template map') % inst.args[0])
1358 except IOError as inst:
1359 reason = (_('template file %s: %s')
1360 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1361 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1362 return self.cache[t]
1363
1364 def render(self, mapping):
1365 """Render the default unnamed template and return result as string"""
1366 mapping = pycompat.strkwargs(mapping)
1367 return templateutil.stringify(self('', **mapping))
1368
1369 def __call__(self, t, **mapping):
1370 mapping = pycompat.byteskwargs(mapping)
1371 ttype = t in self.map and self.map[t][0] or 'default'
1372 if ttype not in self.ecache:
1373 try:
1374 ecls = engines[ttype]
1375 except KeyError:
1376 raise error.Abort(_('invalid template engine: %s') % ttype)
1377 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1378 self._resources, self._aliases)
1379 proc = self.ecache[ttype]
1380
1381 stream = proc.process(t, mapping)
1382 if self.minchunk:
1383 stream = util.increasingchunks(stream, min=self.minchunk,
1384 max=self.maxchunk)
1385 return stream
1386
1387 def templatepaths():
1388 '''return locations used for template files.'''
1389 pathsrel = ['templates']
1390 paths = [os.path.normpath(os.path.join(util.datapath, f))
1391 for f in pathsrel]
1392 return [p for p in paths if os.path.isdir(p)]
1393
1394 def templatepath(name):
1395 '''return location of template file. returns None if not found.'''
1396 for p in templatepaths():
1397 f = os.path.join(p, name)
1398 if os.path.exists(f):
1399 return f
1400 return None
1401
1402 def stylemap(styles, paths=None):
1403 """Return path to mapfile for a given style.
1404
1405 Searches mapfile in the following locations:
1406 1. templatepath/style/map
1407 2. templatepath/map-style
1408 3. templatepath/map
1409 """
1410
1411 if paths is None:
1412 paths = templatepaths()
1413 elif isinstance(paths, bytes):
1414 paths = [paths]
1415
1416 if isinstance(styles, bytes):
1417 styles = [styles]
1418
1419 for style in styles:
1420 # only plain name is allowed to honor template paths
1421 if (not style
1422 or style in (pycompat.oscurdir, pycompat.ospardir)
1423 or pycompat.ossep in style
1424 or pycompat.osaltsep and pycompat.osaltsep in style):
1425 continue
1426 locations = [os.path.join(style, 'map'), 'map-' + style]
1427 locations.append('map')
1428
1429 for path in paths:
1430 for location in locations:
1431 mapfile = os.path.join(path, location)
1432 if os.path.isfile(mapfile):
1433 return style, mapfile
1434
1435 raise RuntimeError("No hgweb templates found in %r" % paths)
1436
1437 657 def loadfunction(ui, extname, registrarobj):
1438 658 """Load template function from specified registrarobj
1439 659 """
1440 660 for name, func in registrarobj._table.iteritems():
1441 661 funcs[name] = func
1442 662
1443 663 # tell hggettext to extract docstrings from these functions:
1444 664 i18nfunctions = funcs.values()
This diff has been collapsed as it changes many lines, (649 lines changed) Show them Hide them
@@ -1,1444 +1,799
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import os
11 import re
12 11
13 12 from .i18n import _
14 13 from . import (
15 color,
16 14 config,
17 15 encoding,
18 16 error,
19 minirst,
20 obsutil,
21 17 parser,
22 18 pycompat,
23 registrar,
24 revset as revsetmod,
25 revsetlang,
26 scmutil,
27 19 templatefilters,
28 templatekw,
20 templatefuncs,
29 21 templateutil,
30 22 util,
31 23 )
32 from .utils import dateutil
33
34 evalrawexp = templateutil.evalrawexp
35 evalfuncarg = templateutil.evalfuncarg
36 evalboolean = templateutil.evalboolean
37 evalinteger = templateutil.evalinteger
38 evalstring = templateutil.evalstring
39 evalstringliteral = templateutil.evalstringliteral
40 evalastype = templateutil.evalastype
41 24
42 25 # template parsing
43 26
44 27 elements = {
45 28 # token-type: binding-strength, primary, prefix, infix, suffix
46 29 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
47 30 ".": (18, None, None, (".", 18), None),
48 31 "%": (15, None, None, ("%", 15), None),
49 32 "|": (15, None, None, ("|", 15), None),
50 33 "*": (5, None, None, ("*", 5), None),
51 34 "/": (5, None, None, ("/", 5), None),
52 35 "+": (4, None, None, ("+", 4), None),
53 36 "-": (4, None, ("negate", 19), ("-", 4), None),
54 37 "=": (3, None, None, ("keyvalue", 3), None),
55 38 ",": (2, None, None, ("list", 2), None),
56 39 ")": (0, None, None, None, None),
57 40 "integer": (0, "integer", None, None, None),
58 41 "symbol": (0, "symbol", None, None, None),
59 42 "string": (0, "string", None, None, None),
60 43 "template": (0, "template", None, None, None),
61 44 "end": (0, None, None, None, None),
62 45 }
63 46
64 47 def tokenize(program, start, end, term=None):
65 48 """Parse a template expression into a stream of tokens, which must end
66 49 with term if specified"""
67 50 pos = start
68 51 program = pycompat.bytestr(program)
69 52 while pos < end:
70 53 c = program[pos]
71 54 if c.isspace(): # skip inter-token whitespace
72 55 pass
73 56 elif c in "(=,).%|+-*/": # handle simple operators
74 57 yield (c, None, pos)
75 58 elif c in '"\'': # handle quoted templates
76 59 s = pos + 1
77 60 data, pos = _parsetemplate(program, s, end, c)
78 61 yield ('template', data, s)
79 62 pos -= 1
80 63 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
81 64 # handle quoted strings
82 65 c = program[pos + 1]
83 66 s = pos = pos + 2
84 67 while pos < end: # find closing quote
85 68 d = program[pos]
86 69 if d == '\\': # skip over escaped characters
87 70 pos += 2
88 71 continue
89 72 if d == c:
90 73 yield ('string', program[s:pos], s)
91 74 break
92 75 pos += 1
93 76 else:
94 77 raise error.ParseError(_("unterminated string"), s)
95 78 elif c.isdigit():
96 79 s = pos
97 80 while pos < end:
98 81 d = program[pos]
99 82 if not d.isdigit():
100 83 break
101 84 pos += 1
102 85 yield ('integer', program[s:pos], s)
103 86 pos -= 1
104 87 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
105 88 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
106 89 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
107 90 # where some of nested templates were preprocessed as strings and
108 91 # then compiled. therefore, \"...\" was allowed. (issue4733)
109 92 #
110 93 # processing flow of _evalifliteral() at 5ab28a2e9962:
111 94 # outer template string -> stringify() -> compiletemplate()
112 95 # ------------------------ ------------ ------------------
113 96 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
114 97 # ~~~~~~~~
115 98 # escaped quoted string
116 99 if c == 'r':
117 100 pos += 1
118 101 token = 'string'
119 102 else:
120 103 token = 'template'
121 104 quote = program[pos:pos + 2]
122 105 s = pos = pos + 2
123 106 while pos < end: # find closing escaped quote
124 107 if program.startswith('\\\\\\', pos, end):
125 108 pos += 4 # skip over double escaped characters
126 109 continue
127 110 if program.startswith(quote, pos, end):
128 111 # interpret as if it were a part of an outer string
129 112 data = parser.unescapestr(program[s:pos])
130 113 if token == 'template':
131 114 data = _parsetemplate(data, 0, len(data))[0]
132 115 yield (token, data, s)
133 116 pos += 1
134 117 break
135 118 pos += 1
136 119 else:
137 120 raise error.ParseError(_("unterminated string"), s)
138 121 elif c.isalnum() or c in '_':
139 122 s = pos
140 123 pos += 1
141 124 while pos < end: # find end of symbol
142 125 d = program[pos]
143 126 if not (d.isalnum() or d == "_"):
144 127 break
145 128 pos += 1
146 129 sym = program[s:pos]
147 130 yield ('symbol', sym, s)
148 131 pos -= 1
149 132 elif c == term:
150 133 yield ('end', None, pos)
151 134 return
152 135 else:
153 136 raise error.ParseError(_("syntax error"), pos)
154 137 pos += 1
155 138 if term:
156 139 raise error.ParseError(_("unterminated template expansion"), start)
157 140 yield ('end', None, pos)
158 141
159 142 def _parsetemplate(tmpl, start, stop, quote=''):
160 143 r"""
161 144 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
162 145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
163 146 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
164 147 ([('string', 'foo'), ('symbol', 'bar')], 9)
165 148 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
166 149 ([('string', 'foo')], 4)
167 150 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
168 151 ([('string', 'foo"'), ('string', 'bar')], 9)
169 152 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
170 153 ([('string', 'foo\\')], 6)
171 154 """
172 155 parsed = []
173 156 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
174 157 if typ == 'string':
175 158 parsed.append((typ, val))
176 159 elif typ == 'template':
177 160 parsed.append(val)
178 161 elif typ == 'end':
179 162 return parsed, pos
180 163 else:
181 164 raise error.ProgrammingError('unexpected type: %s' % typ)
182 165 raise error.ProgrammingError('unterminated scanning of template')
183 166
184 167 def scantemplate(tmpl, raw=False):
185 168 r"""Scan (type, start, end) positions of outermost elements in template
186 169
187 170 If raw=True, a backslash is not taken as an escape character just like
188 171 r'' string in Python. Note that this is different from r'' literal in
189 172 template in that no template fragment can appear in r'', e.g. r'{foo}'
190 173 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
191 174 'foo'.
192 175
193 176 >>> list(scantemplate(b'foo{bar}"baz'))
194 177 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
195 178 >>> list(scantemplate(b'outer{"inner"}outer'))
196 179 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
197 180 >>> list(scantemplate(b'foo\\{escaped}'))
198 181 [('string', 0, 5), ('string', 5, 13)]
199 182 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
200 183 [('string', 0, 4), ('template', 4, 13)]
201 184 """
202 185 last = None
203 186 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
204 187 if last:
205 188 yield last + (pos,)
206 189 if typ == 'end':
207 190 return
208 191 else:
209 192 last = (typ, pos)
210 193 raise error.ProgrammingError('unterminated scanning of template')
211 194
212 195 def _scantemplate(tmpl, start, stop, quote='', raw=False):
213 196 """Parse template string into chunks of strings and template expressions"""
214 197 sepchars = '{' + quote
215 198 unescape = [parser.unescapestr, pycompat.identity][raw]
216 199 pos = start
217 200 p = parser.parser(elements)
218 201 try:
219 202 while pos < stop:
220 203 n = min((tmpl.find(c, pos, stop) for c in sepchars),
221 204 key=lambda n: (n < 0, n))
222 205 if n < 0:
223 206 yield ('string', unescape(tmpl[pos:stop]), pos)
224 207 pos = stop
225 208 break
226 209 c = tmpl[n:n + 1]
227 210 bs = 0 # count leading backslashes
228 211 if not raw:
229 212 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
230 213 if bs % 2 == 1:
231 214 # escaped (e.g. '\{', '\\\{', but not '\\{')
232 215 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
233 216 pos = n + 1
234 217 continue
235 218 if n > pos:
236 219 yield ('string', unescape(tmpl[pos:n]), pos)
237 220 if c == quote:
238 221 yield ('end', None, n + 1)
239 222 return
240 223
241 224 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
242 225 if not tmpl.startswith('}', pos):
243 226 raise error.ParseError(_("invalid token"), pos)
244 227 yield ('template', parseres, n)
245 228 pos += 1
246 229
247 230 if quote:
248 231 raise error.ParseError(_("unterminated string"), start)
249 232 except error.ParseError as inst:
250 233 if len(inst.args) > 1: # has location
251 234 loc = inst.args[1]
252 235 # Offset the caret location by the number of newlines before the
253 236 # location of the error, since we will replace one-char newlines
254 237 # with the two-char literal r'\n'.
255 238 offset = tmpl[:loc].count('\n')
256 239 tmpl = tmpl.replace('\n', br'\n')
257 240 # We want the caret to point to the place in the template that
258 241 # failed to parse, but in a hint we get a open paren at the
259 242 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
260 243 # to line up the caret with the location of the error.
261 244 inst.hint = (tmpl + '\n'
262 245 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
263 246 raise
264 247 yield ('end', None, pos)
265 248
266 249 def _unnesttemplatelist(tree):
267 250 """Expand list of templates to node tuple
268 251
269 252 >>> def f(tree):
270 253 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
271 254 >>> f((b'template', []))
272 255 (string '')
273 256 >>> f((b'template', [(b'string', b'foo')]))
274 257 (string 'foo')
275 258 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
276 259 (template
277 260 (string 'foo')
278 261 (symbol 'rev'))
279 262 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
280 263 (template
281 264 (symbol 'rev'))
282 265 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
283 266 (string 'foo')
284 267 """
285 268 if not isinstance(tree, tuple):
286 269 return tree
287 270 op = tree[0]
288 271 if op != 'template':
289 272 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
290 273
291 274 assert len(tree) == 2
292 275 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
293 276 if not xs:
294 277 return ('string', '') # empty template ""
295 278 elif len(xs) == 1 and xs[0][0] == 'string':
296 279 return xs[0] # fast path for string with no template fragment "x"
297 280 else:
298 281 return (op,) + xs
299 282
300 283 def parse(tmpl):
301 284 """Parse template string into tree"""
302 285 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
303 286 assert pos == len(tmpl), 'unquoted template should be consumed'
304 287 return _unnesttemplatelist(('template', parsed))
305 288
306 289 def _parseexpr(expr):
307 290 """Parse a template expression into tree
308 291
309 292 >>> _parseexpr(b'"foo"')
310 293 ('string', 'foo')
311 294 >>> _parseexpr(b'foo(bar)')
312 295 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
313 296 >>> _parseexpr(b'foo(')
314 297 Traceback (most recent call last):
315 298 ...
316 299 ParseError: ('not a prefix: end', 4)
317 300 >>> _parseexpr(b'"foo" "bar"')
318 301 Traceback (most recent call last):
319 302 ...
320 303 ParseError: ('invalid token', 7)
321 304 """
322 305 p = parser.parser(elements)
323 306 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
324 307 if pos != len(expr):
325 308 raise error.ParseError(_('invalid token'), pos)
326 309 return _unnesttemplatelist(tree)
327 310
328 311 def prettyformat(tree):
329 312 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
330 313
331 314 def compileexp(exp, context, curmethods):
332 315 """Compile parsed template tree to (func, data) pair"""
333 316 if not exp:
334 317 raise error.ParseError(_("missing argument"))
335 318 t = exp[0]
336 319 if t in curmethods:
337 320 return curmethods[t](exp, context)
338 321 raise error.ParseError(_("unknown method '%s'") % t)
339 322
340 323 # template evaluation
341 324
342 325 def getsymbol(exp):
343 326 if exp[0] == 'symbol':
344 327 return exp[1]
345 328 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
346 329
347 330 def getlist(x):
348 331 if not x:
349 332 return []
350 333 if x[0] == 'list':
351 334 return getlist(x[1]) + [x[2]]
352 335 return [x]
353 336
354 337 def gettemplate(exp, context):
355 338 """Compile given template tree or load named template from map file;
356 339 returns (func, data) pair"""
357 340 if exp[0] in ('template', 'string'):
358 341 return compileexp(exp, context, methods)
359 342 if exp[0] == 'symbol':
360 343 # unlike runsymbol(), here 'symbol' is always taken as template name
361 344 # even if it exists in mapping. this allows us to override mapping
362 345 # by web templates, e.g. 'changelogtag' is redefined in map file.
363 346 return context._load(exp[1])
364 347 raise error.ParseError(_("expected template specifier"))
365 348
366 349 def _runrecursivesymbol(context, mapping, key):
367 350 raise error.Abort(_("recursive reference '%s' in template") % key)
368 351
369 352 def buildtemplate(exp, context):
370 353 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
371 354 return (templateutil.runtemplate, ctmpl)
372 355
373 356 def buildfilter(exp, context):
374 357 n = getsymbol(exp[2])
375 358 if n in context._filters:
376 359 filt = context._filters[n]
377 360 arg = compileexp(exp[1], context, methods)
378 361 return (templateutil.runfilter, (arg, filt))
379 362 if n in context._funcs:
380 363 f = context._funcs[n]
381 364 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
382 365 return (f, args)
383 366 raise error.ParseError(_("unknown function '%s'") % n)
384 367
385 368 def buildmap(exp, context):
386 369 darg = compileexp(exp[1], context, methods)
387 370 targ = gettemplate(exp[2], context)
388 371 return (templateutil.runmap, (darg, targ))
389 372
390 373 def buildmember(exp, context):
391 374 darg = compileexp(exp[1], context, methods)
392 375 memb = getsymbol(exp[2])
393 376 return (templateutil.runmember, (darg, memb))
394 377
395 378 def buildnegate(exp, context):
396 379 arg = compileexp(exp[1], context, exprmethods)
397 380 return (templateutil.runnegate, arg)
398 381
399 382 def buildarithmetic(exp, context, func):
400 383 left = compileexp(exp[1], context, exprmethods)
401 384 right = compileexp(exp[2], context, exprmethods)
402 385 return (templateutil.runarithmetic, (func, left, right))
403 386
404 387 def buildfunc(exp, context):
405 388 n = getsymbol(exp[1])
406 389 if n in context._funcs:
407 390 f = context._funcs[n]
408 391 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
409 392 return (f, args)
410 393 if n in context._filters:
411 394 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
412 395 if len(args) != 1:
413 396 raise error.ParseError(_("filter %s expects one argument") % n)
414 397 f = context._filters[n]
415 398 return (templateutil.runfilter, (args[0], f))
416 399 raise error.ParseError(_("unknown function '%s'") % n)
417 400
418 401 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
419 402 """Compile parsed tree of function arguments into list or dict of
420 403 (func, data) pairs
421 404
422 405 >>> context = engine(lambda t: (runsymbol, t))
423 406 >>> def fargs(expr, argspec):
424 407 ... x = _parseexpr(expr)
425 408 ... n = getsymbol(x[1])
426 409 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
427 410 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
428 411 ['l', 'k']
429 412 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
430 413 >>> list(args.keys()), list(args[b'opts'].keys())
431 414 (['opts'], ['opts', 'k'])
432 415 """
433 416 def compiledict(xs):
434 417 return util.sortdict((k, compileexp(x, context, curmethods))
435 418 for k, x in xs.iteritems())
436 419 def compilelist(xs):
437 420 return [compileexp(x, context, curmethods) for x in xs]
438 421
439 422 if not argspec:
440 423 # filter or function with no argspec: return list of positional args
441 424 return compilelist(getlist(exp))
442 425
443 426 # function with argspec: return dict of named args
444 427 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
445 428 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
446 429 keyvaluenode='keyvalue', keynode='symbol')
447 430 compargs = util.sortdict()
448 431 if varkey:
449 432 compargs[varkey] = compilelist(treeargs.pop(varkey))
450 433 if optkey:
451 434 compargs[optkey] = compiledict(treeargs.pop(optkey))
452 435 compargs.update(compiledict(treeargs))
453 436 return compargs
454 437
455 438 def buildkeyvaluepair(exp, content):
456 439 raise error.ParseError(_("can't use a key-value pair in this context"))
457 440
458 # dict of template built-in functions
459 funcs = {}
460
461 templatefunc = registrar.templatefunc(funcs)
462
463 @templatefunc('date(date[, fmt])')
464 def date(context, mapping, args):
465 """Format a date. See :hg:`help dates` for formatting
466 strings. The default is a Unix date format, including the timezone:
467 "Mon Sep 04 15:13:13 2006 0700"."""
468 if not (1 <= len(args) <= 2):
469 # i18n: "date" is a keyword
470 raise error.ParseError(_("date expects one or two arguments"))
471
472 date = evalfuncarg(context, mapping, args[0])
473 fmt = None
474 if len(args) == 2:
475 fmt = evalstring(context, mapping, args[1])
476 try:
477 if fmt is None:
478 return dateutil.datestr(date)
479 else:
480 return dateutil.datestr(date, fmt)
481 except (TypeError, ValueError):
482 # i18n: "date" is a keyword
483 raise error.ParseError(_("date expects a date information"))
484
485 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
486 def dict_(context, mapping, args):
487 """Construct a dict from key-value pairs. A key may be omitted if
488 a value expression can provide an unambiguous name."""
489 data = util.sortdict()
490
491 for v in args['args']:
492 k = templateutil.findsymbolicname(v)
493 if not k:
494 raise error.ParseError(_('dict key cannot be inferred'))
495 if k in data or k in args['kwargs']:
496 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
497 data[k] = evalfuncarg(context, mapping, v)
498
499 data.update((k, evalfuncarg(context, mapping, v))
500 for k, v in args['kwargs'].iteritems())
501 return templateutil.hybriddict(data)
502
503 @templatefunc('diff([includepattern [, excludepattern]])')
504 def diff(context, mapping, args):
505 """Show a diff, optionally
506 specifying files to include or exclude."""
507 if len(args) > 2:
508 # i18n: "diff" is a keyword
509 raise error.ParseError(_("diff expects zero, one, or two arguments"))
510
511 def getpatterns(i):
512 if i < len(args):
513 s = evalstring(context, mapping, args[i]).strip()
514 if s:
515 return [s]
516 return []
517
518 ctx = context.resource(mapping, 'ctx')
519 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
520
521 return ''.join(chunks)
522
523 @templatefunc('extdata(source)', argspec='source')
524 def extdata(context, mapping, args):
525 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
526 if 'source' not in args:
527 # i18n: "extdata" is a keyword
528 raise error.ParseError(_('extdata expects one argument'))
529
530 source = evalstring(context, mapping, args['source'])
531 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
532 ctx = context.resource(mapping, 'ctx')
533 if source in cache:
534 data = cache[source]
535 else:
536 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
537 return data.get(ctx.rev(), '')
538
539 @templatefunc('files(pattern)')
540 def files(context, mapping, args):
541 """All files of the current changeset matching the pattern. See
542 :hg:`help patterns`."""
543 if not len(args) == 1:
544 # i18n: "files" is a keyword
545 raise error.ParseError(_("files expects one argument"))
546
547 raw = evalstring(context, mapping, args[0])
548 ctx = context.resource(mapping, 'ctx')
549 m = ctx.match([raw])
550 files = list(ctx.matches(m))
551 return templateutil.compatlist(context, mapping, "file", files)
552
553 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
554 def fill(context, mapping, args):
555 """Fill many
556 paragraphs with optional indentation. See the "fill" filter."""
557 if not (1 <= len(args) <= 4):
558 # i18n: "fill" is a keyword
559 raise error.ParseError(_("fill expects one to four arguments"))
560
561 text = evalstring(context, mapping, args[0])
562 width = 76
563 initindent = ''
564 hangindent = ''
565 if 2 <= len(args) <= 4:
566 width = evalinteger(context, mapping, args[1],
567 # i18n: "fill" is a keyword
568 _("fill expects an integer width"))
569 try:
570 initindent = evalstring(context, mapping, args[2])
571 hangindent = evalstring(context, mapping, args[3])
572 except IndexError:
573 pass
574
575 return templatefilters.fill(text, width, initindent, hangindent)
576
577 @templatefunc('formatnode(node)')
578 def formatnode(context, mapping, args):
579 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
580 if len(args) != 1:
581 # i18n: "formatnode" is a keyword
582 raise error.ParseError(_("formatnode expects one argument"))
583
584 ui = context.resource(mapping, 'ui')
585 node = evalstring(context, mapping, args[0])
586 if ui.debugflag:
587 return node
588 return templatefilters.short(node)
589
590 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
591 argspec='text width fillchar left')
592 def pad(context, mapping, args):
593 """Pad text with a
594 fill character."""
595 if 'text' not in args or 'width' not in args:
596 # i18n: "pad" is a keyword
597 raise error.ParseError(_("pad() expects two to four arguments"))
598
599 width = evalinteger(context, mapping, args['width'],
600 # i18n: "pad" is a keyword
601 _("pad() expects an integer width"))
602
603 text = evalstring(context, mapping, args['text'])
604
605 left = False
606 fillchar = ' '
607 if 'fillchar' in args:
608 fillchar = evalstring(context, mapping, args['fillchar'])
609 if len(color.stripeffects(fillchar)) != 1:
610 # i18n: "pad" is a keyword
611 raise error.ParseError(_("pad() expects a single fill character"))
612 if 'left' in args:
613 left = evalboolean(context, mapping, args['left'])
614
615 fillwidth = width - encoding.colwidth(color.stripeffects(text))
616 if fillwidth <= 0:
617 return text
618 if left:
619 return fillchar * fillwidth + text
620 else:
621 return text + fillchar * fillwidth
622
623 @templatefunc('indent(text, indentchars[, firstline])')
624 def indent(context, mapping, args):
625 """Indents all non-empty lines
626 with the characters given in the indentchars string. An optional
627 third parameter will override the indent for the first line only
628 if present."""
629 if not (2 <= len(args) <= 3):
630 # i18n: "indent" is a keyword
631 raise error.ParseError(_("indent() expects two or three arguments"))
632
633 text = evalstring(context, mapping, args[0])
634 indent = evalstring(context, mapping, args[1])
635
636 if len(args) == 3:
637 firstline = evalstring(context, mapping, args[2])
638 else:
639 firstline = indent
640
641 # the indent function doesn't indent the first line, so we do it here
642 return templatefilters.indent(firstline + text, indent)
643
644 @templatefunc('get(dict, key)')
645 def get(context, mapping, args):
646 """Get an attribute/key from an object. Some keywords
647 are complex types. This function allows you to obtain the value of an
648 attribute on these types."""
649 if len(args) != 2:
650 # i18n: "get" is a keyword
651 raise error.ParseError(_("get() expects two arguments"))
652
653 dictarg = evalfuncarg(context, mapping, args[0])
654 if not util.safehasattr(dictarg, 'get'):
655 # i18n: "get" is a keyword
656 raise error.ParseError(_("get() expects a dict as first argument"))
657
658 key = evalfuncarg(context, mapping, args[1])
659 return templateutil.getdictitem(dictarg, key)
660
661 @templatefunc('if(expr, then[, else])')
662 def if_(context, mapping, args):
663 """Conditionally execute based on the result of
664 an expression."""
665 if not (2 <= len(args) <= 3):
666 # i18n: "if" is a keyword
667 raise error.ParseError(_("if expects two or three arguments"))
668
669 test = evalboolean(context, mapping, args[0])
670 if test:
671 yield evalrawexp(context, mapping, args[1])
672 elif len(args) == 3:
673 yield evalrawexp(context, mapping, args[2])
674
675 @templatefunc('ifcontains(needle, haystack, then[, else])')
676 def ifcontains(context, mapping, args):
677 """Conditionally execute based
678 on whether the item "needle" is in "haystack"."""
679 if not (3 <= len(args) <= 4):
680 # i18n: "ifcontains" is a keyword
681 raise error.ParseError(_("ifcontains expects three or four arguments"))
682
683 haystack = evalfuncarg(context, mapping, args[1])
684 try:
685 needle = evalastype(context, mapping, args[0],
686 getattr(haystack, 'keytype', None) or bytes)
687 found = (needle in haystack)
688 except error.ParseError:
689 found = False
690
691 if found:
692 yield evalrawexp(context, mapping, args[2])
693 elif len(args) == 4:
694 yield evalrawexp(context, mapping, args[3])
695
696 @templatefunc('ifeq(expr1, expr2, then[, else])')
697 def ifeq(context, mapping, args):
698 """Conditionally execute based on
699 whether 2 items are equivalent."""
700 if not (3 <= len(args) <= 4):
701 # i18n: "ifeq" is a keyword
702 raise error.ParseError(_("ifeq expects three or four arguments"))
703
704 test = evalstring(context, mapping, args[0])
705 match = evalstring(context, mapping, args[1])
706 if test == match:
707 yield evalrawexp(context, mapping, args[2])
708 elif len(args) == 4:
709 yield evalrawexp(context, mapping, args[3])
710
711 @templatefunc('join(list, sep)')
712 def join(context, mapping, args):
713 """Join items in a list with a delimiter."""
714 if not (1 <= len(args) <= 2):
715 # i18n: "join" is a keyword
716 raise error.ParseError(_("join expects one or two arguments"))
717
718 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
719 # abuses generator as a keyword that returns a list of dicts.
720 joinset = evalrawexp(context, mapping, args[0])
721 joinset = templateutil.unwrapvalue(joinset)
722 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
723 joiner = " "
724 if len(args) > 1:
725 joiner = evalstring(context, mapping, args[1])
726
727 first = True
728 for x in pycompat.maybebytestr(joinset):
729 if first:
730 first = False
731 else:
732 yield joiner
733 yield joinfmt(x)
734
735 @templatefunc('label(label, expr)')
736 def label(context, mapping, args):
737 """Apply a label to generated content. Content with
738 a label applied can result in additional post-processing, such as
739 automatic colorization."""
740 if len(args) != 2:
741 # i18n: "label" is a keyword
742 raise error.ParseError(_("label expects two arguments"))
743
744 ui = context.resource(mapping, 'ui')
745 thing = evalstring(context, mapping, args[1])
746 # preserve unknown symbol as literal so effects like 'red', 'bold',
747 # etc. don't need to be quoted
748 label = evalstringliteral(context, mapping, args[0])
749
750 return ui.label(thing, label)
751
752 @templatefunc('latesttag([pattern])')
753 def latesttag(context, mapping, args):
754 """The global tags matching the given pattern on the
755 most recent globally tagged ancestor of this changeset.
756 If no such tags exist, the "{tag}" template resolves to
757 the string "null"."""
758 if len(args) > 1:
759 # i18n: "latesttag" is a keyword
760 raise error.ParseError(_("latesttag expects at most one argument"))
761
762 pattern = None
763 if len(args) == 1:
764 pattern = evalstring(context, mapping, args[0])
765 return templatekw.showlatesttags(context, mapping, pattern)
766
767 @templatefunc('localdate(date[, tz])')
768 def localdate(context, mapping, args):
769 """Converts a date to the specified timezone.
770 The default is local date."""
771 if not (1 <= len(args) <= 2):
772 # i18n: "localdate" is a keyword
773 raise error.ParseError(_("localdate expects one or two arguments"))
774
775 date = evalfuncarg(context, mapping, args[0])
776 try:
777 date = dateutil.parsedate(date)
778 except AttributeError: # not str nor date tuple
779 # i18n: "localdate" is a keyword
780 raise error.ParseError(_("localdate expects a date information"))
781 if len(args) >= 2:
782 tzoffset = None
783 tz = evalfuncarg(context, mapping, args[1])
784 if isinstance(tz, bytes):
785 tzoffset, remainder = dateutil.parsetimezone(tz)
786 if remainder:
787 tzoffset = None
788 if tzoffset is None:
789 try:
790 tzoffset = int(tz)
791 except (TypeError, ValueError):
792 # i18n: "localdate" is a keyword
793 raise error.ParseError(_("localdate expects a timezone"))
794 else:
795 tzoffset = dateutil.makedate()[1]
796 return (date[0], tzoffset)
797
798 @templatefunc('max(iterable)')
799 def max_(context, mapping, args, **kwargs):
800 """Return the max of an iterable"""
801 if len(args) != 1:
802 # i18n: "max" is a keyword
803 raise error.ParseError(_("max expects one argument"))
804
805 iterable = evalfuncarg(context, mapping, args[0])
806 try:
807 x = max(pycompat.maybebytestr(iterable))
808 except (TypeError, ValueError):
809 # i18n: "max" is a keyword
810 raise error.ParseError(_("max first argument should be an iterable"))
811 return templateutil.wraphybridvalue(iterable, x, x)
812
813 @templatefunc('min(iterable)')
814 def min_(context, mapping, args, **kwargs):
815 """Return the min of an iterable"""
816 if len(args) != 1:
817 # i18n: "min" is a keyword
818 raise error.ParseError(_("min expects one argument"))
819
820 iterable = evalfuncarg(context, mapping, args[0])
821 try:
822 x = min(pycompat.maybebytestr(iterable))
823 except (TypeError, ValueError):
824 # i18n: "min" is a keyword
825 raise error.ParseError(_("min first argument should be an iterable"))
826 return templateutil.wraphybridvalue(iterable, x, x)
827
828 @templatefunc('mod(a, b)')
829 def mod(context, mapping, args):
830 """Calculate a mod b such that a / b + a mod b == a"""
831 if not len(args) == 2:
832 # i18n: "mod" is a keyword
833 raise error.ParseError(_("mod expects two arguments"))
834
835 func = lambda a, b: a % b
836 return templateutil.runarithmetic(context, mapping,
837 (func, args[0], args[1]))
838
839 @templatefunc('obsfateoperations(markers)')
840 def obsfateoperations(context, mapping, args):
841 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
842 if len(args) != 1:
843 # i18n: "obsfateoperations" is a keyword
844 raise error.ParseError(_("obsfateoperations expects one argument"))
845
846 markers = evalfuncarg(context, mapping, args[0])
847
848 try:
849 data = obsutil.markersoperations(markers)
850 return templateutil.hybridlist(data, name='operation')
851 except (TypeError, KeyError):
852 # i18n: "obsfateoperations" is a keyword
853 errmsg = _("obsfateoperations first argument should be an iterable")
854 raise error.ParseError(errmsg)
855
856 @templatefunc('obsfatedate(markers)')
857 def obsfatedate(context, mapping, args):
858 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
859 if len(args) != 1:
860 # i18n: "obsfatedate" is a keyword
861 raise error.ParseError(_("obsfatedate expects one argument"))
862
863 markers = evalfuncarg(context, mapping, args[0])
864
865 try:
866 data = obsutil.markersdates(markers)
867 return templateutil.hybridlist(data, name='date', fmt='%d %d')
868 except (TypeError, KeyError):
869 # i18n: "obsfatedate" is a keyword
870 errmsg = _("obsfatedate first argument should be an iterable")
871 raise error.ParseError(errmsg)
872
873 @templatefunc('obsfateusers(markers)')
874 def obsfateusers(context, mapping, args):
875 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
876 if len(args) != 1:
877 # i18n: "obsfateusers" is a keyword
878 raise error.ParseError(_("obsfateusers expects one argument"))
879
880 markers = evalfuncarg(context, mapping, args[0])
881
882 try:
883 data = obsutil.markersusers(markers)
884 return templateutil.hybridlist(data, name='user')
885 except (TypeError, KeyError, ValueError):
886 # i18n: "obsfateusers" is a keyword
887 msg = _("obsfateusers first argument should be an iterable of "
888 "obsmakers")
889 raise error.ParseError(msg)
890
891 @templatefunc('obsfateverb(successors, markers)')
892 def obsfateverb(context, mapping, args):
893 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
894 if len(args) != 2:
895 # i18n: "obsfateverb" is a keyword
896 raise error.ParseError(_("obsfateverb expects two arguments"))
897
898 successors = evalfuncarg(context, mapping, args[0])
899 markers = evalfuncarg(context, mapping, args[1])
900
901 try:
902 return obsutil.obsfateverb(successors, markers)
903 except TypeError:
904 # i18n: "obsfateverb" is a keyword
905 errmsg = _("obsfateverb first argument should be countable")
906 raise error.ParseError(errmsg)
907
908 @templatefunc('relpath(path)')
909 def relpath(context, mapping, args):
910 """Convert a repository-absolute path into a filesystem path relative to
911 the current working directory."""
912 if len(args) != 1:
913 # i18n: "relpath" is a keyword
914 raise error.ParseError(_("relpath expects one argument"))
915
916 repo = context.resource(mapping, 'ctx').repo()
917 path = evalstring(context, mapping, args[0])
918 return repo.pathto(path)
919
920 @templatefunc('revset(query[, formatargs...])')
921 def revset(context, mapping, args):
922 """Execute a revision set query. See
923 :hg:`help revset`."""
924 if not len(args) > 0:
925 # i18n: "revset" is a keyword
926 raise error.ParseError(_("revset expects one or more arguments"))
927
928 raw = evalstring(context, mapping, args[0])
929 ctx = context.resource(mapping, 'ctx')
930 repo = ctx.repo()
931
932 def query(expr):
933 m = revsetmod.match(repo.ui, expr, repo=repo)
934 return m(repo)
935
936 if len(args) > 1:
937 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
938 revs = query(revsetlang.formatspec(raw, *formatargs))
939 revs = list(revs)
940 else:
941 cache = context.resource(mapping, 'cache')
942 revsetcache = cache.setdefault("revsetcache", {})
943 if raw in revsetcache:
944 revs = revsetcache[raw]
945 else:
946 revs = query(raw)
947 revs = list(revs)
948 revsetcache[raw] = revs
949 return templatekw.showrevslist(context, mapping, "revision", revs)
950
951 @templatefunc('rstdoc(text, style)')
952 def rstdoc(context, mapping, args):
953 """Format reStructuredText."""
954 if len(args) != 2:
955 # i18n: "rstdoc" is a keyword
956 raise error.ParseError(_("rstdoc expects two arguments"))
957
958 text = evalstring(context, mapping, args[0])
959 style = evalstring(context, mapping, args[1])
960
961 return minirst.format(text, style=style, keep=['verbose'])
962
963 @templatefunc('separate(sep, args)', argspec='sep *args')
964 def separate(context, mapping, args):
965 """Add a separator between non-empty arguments."""
966 if 'sep' not in args:
967 # i18n: "separate" is a keyword
968 raise error.ParseError(_("separate expects at least one argument"))
969
970 sep = evalstring(context, mapping, args['sep'])
971 first = True
972 for arg in args['args']:
973 argstr = evalstring(context, mapping, arg)
974 if not argstr:
975 continue
976 if first:
977 first = False
978 else:
979 yield sep
980 yield argstr
981
982 @templatefunc('shortest(node, minlength=4)')
983 def shortest(context, mapping, args):
984 """Obtain the shortest representation of
985 a node."""
986 if not (1 <= len(args) <= 2):
987 # i18n: "shortest" is a keyword
988 raise error.ParseError(_("shortest() expects one or two arguments"))
989
990 node = evalstring(context, mapping, args[0])
991
992 minlength = 4
993 if len(args) > 1:
994 minlength = evalinteger(context, mapping, args[1],
995 # i18n: "shortest" is a keyword
996 _("shortest() expects an integer minlength"))
997
998 # _partialmatch() of filtered changelog could take O(len(repo)) time,
999 # which would be unacceptably slow. so we look for hash collision in
1000 # unfiltered space, which means some hashes may be slightly longer.
1001 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1002 return cl.shortest(node, minlength)
1003
1004 @templatefunc('strip(text[, chars])')
1005 def strip(context, mapping, args):
1006 """Strip characters from a string. By default,
1007 strips all leading and trailing whitespace."""
1008 if not (1 <= len(args) <= 2):
1009 # i18n: "strip" is a keyword
1010 raise error.ParseError(_("strip expects one or two arguments"))
1011
1012 text = evalstring(context, mapping, args[0])
1013 if len(args) == 2:
1014 chars = evalstring(context, mapping, args[1])
1015 return text.strip(chars)
1016 return text.strip()
1017
1018 @templatefunc('sub(pattern, replacement, expression)')
1019 def sub(context, mapping, args):
1020 """Perform text substitution
1021 using regular expressions."""
1022 if len(args) != 3:
1023 # i18n: "sub" is a keyword
1024 raise error.ParseError(_("sub expects three arguments"))
1025
1026 pat = evalstring(context, mapping, args[0])
1027 rpl = evalstring(context, mapping, args[1])
1028 src = evalstring(context, mapping, args[2])
1029 try:
1030 patre = re.compile(pat)
1031 except re.error:
1032 # i18n: "sub" is a keyword
1033 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1034 try:
1035 yield patre.sub(rpl, src)
1036 except re.error:
1037 # i18n: "sub" is a keyword
1038 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1039
1040 @templatefunc('startswith(pattern, text)')
1041 def startswith(context, mapping, args):
1042 """Returns the value from the "text" argument
1043 if it begins with the content from the "pattern" argument."""
1044 if len(args) != 2:
1045 # i18n: "startswith" is a keyword
1046 raise error.ParseError(_("startswith expects two arguments"))
1047
1048 patn = evalstring(context, mapping, args[0])
1049 text = evalstring(context, mapping, args[1])
1050 if text.startswith(patn):
1051 return text
1052 return ''
1053
1054 @templatefunc('word(number, text[, separator])')
1055 def word(context, mapping, args):
1056 """Return the nth word from a string."""
1057 if not (2 <= len(args) <= 3):
1058 # i18n: "word" is a keyword
1059 raise error.ParseError(_("word expects two or three arguments, got %d")
1060 % len(args))
1061
1062 num = evalinteger(context, mapping, args[0],
1063 # i18n: "word" is a keyword
1064 _("word expects an integer index"))
1065 text = evalstring(context, mapping, args[1])
1066 if len(args) == 3:
1067 splitter = evalstring(context, mapping, args[2])
1068 else:
1069 splitter = None
1070
1071 tokens = text.split(splitter)
1072 if num >= len(tokens) or num < -len(tokens):
1073 return ''
1074 else:
1075 return tokens[num]
1076
1077 441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1078 442 exprmethods = {
1079 443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
1080 444 "string": lambda e, c: (templateutil.runstring, e[1]),
1081 445 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
1082 446 "template": buildtemplate,
1083 447 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1084 448 ".": buildmember,
1085 449 "|": buildfilter,
1086 450 "%": buildmap,
1087 451 "func": buildfunc,
1088 452 "keyvalue": buildkeyvaluepair,
1089 453 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1090 454 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1091 455 "negate": buildnegate,
1092 456 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1093 457 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1094 458 }
1095 459
1096 460 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1097 461 methods = exprmethods.copy()
1098 462 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1099 463
1100 464 class _aliasrules(parser.basealiasrules):
1101 465 """Parsing and expansion rule set of template aliases"""
1102 466 _section = _('template alias')
1103 467 _parse = staticmethod(_parseexpr)
1104 468
1105 469 @staticmethod
1106 470 def _trygetfunc(tree):
1107 471 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1108 472 None"""
1109 473 if tree[0] == 'func' and tree[1][0] == 'symbol':
1110 474 return tree[1][1], getlist(tree[2])
1111 475 if tree[0] == '|' and tree[2][0] == 'symbol':
1112 476 return tree[2][1], [tree[1]]
1113 477
1114 478 def expandaliases(tree, aliases):
1115 479 """Return new tree of aliases are expanded"""
1116 480 aliasmap = _aliasrules.buildmap(aliases)
1117 481 return _aliasrules.expand(aliasmap, tree)
1118 482
1119 483 # template engine
1120 484
1121 485 def _flatten(thing):
1122 486 '''yield a single stream from a possibly nested set of iterators'''
1123 487 thing = templateutil.unwraphybrid(thing)
1124 488 if isinstance(thing, bytes):
1125 489 yield thing
1126 490 elif isinstance(thing, str):
1127 491 # We can only hit this on Python 3, and it's here to guard
1128 492 # against infinite recursion.
1129 493 raise error.ProgrammingError('Mercurial IO including templates is done'
1130 494 ' with bytes, not strings, got %r' % thing)
1131 495 elif thing is None:
1132 496 pass
1133 497 elif not util.safehasattr(thing, '__iter__'):
1134 498 yield pycompat.bytestr(thing)
1135 499 else:
1136 500 for i in thing:
1137 501 i = templateutil.unwraphybrid(i)
1138 502 if isinstance(i, bytes):
1139 503 yield i
1140 504 elif i is None:
1141 505 pass
1142 506 elif not util.safehasattr(i, '__iter__'):
1143 507 yield pycompat.bytestr(i)
1144 508 else:
1145 509 for j in _flatten(i):
1146 510 yield j
1147 511
1148 512 def unquotestring(s):
1149 513 '''unwrap quotes if any; otherwise returns unmodified string'''
1150 514 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1151 515 return s
1152 516 return s[1:-1]
1153 517
1154 518 class engine(object):
1155 519 '''template expansion engine.
1156 520
1157 521 template expansion works like this. a map file contains key=value
1158 522 pairs. if value is quoted, it is treated as string. otherwise, it
1159 523 is treated as name of template file.
1160 524
1161 525 templater is asked to expand a key in map. it looks up key, and
1162 526 looks for strings like this: {foo}. it expands {foo} by looking up
1163 527 foo in map, and substituting it. expansion is recursive: it stops
1164 528 when there is no more {foo} to replace.
1165 529
1166 530 expansion also allows formatting and filtering.
1167 531
1168 532 format uses key to expand each item in list. syntax is
1169 533 {key%format}.
1170 534
1171 535 filter uses function to transform value. syntax is
1172 536 {key|filter1|filter2|...}.'''
1173 537
1174 538 def __init__(self, loader, filters=None, defaults=None, resources=None,
1175 539 aliases=()):
1176 540 self._loader = loader
1177 541 if filters is None:
1178 542 filters = {}
1179 543 self._filters = filters
1180 self._funcs = funcs # make this a parameter if needed
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
1181 545 if defaults is None:
1182 546 defaults = {}
1183 547 if resources is None:
1184 548 resources = {}
1185 549 self._defaults = defaults
1186 550 self._resources = resources
1187 551 self._aliasmap = _aliasrules.buildmap(aliases)
1188 552 self._cache = {} # key: (func, data)
1189 553
1190 554 def symbol(self, mapping, key):
1191 555 """Resolve symbol to value or function; None if nothing found"""
1192 556 v = None
1193 557 if key not in self._resources:
1194 558 v = mapping.get(key)
1195 559 if v is None:
1196 560 v = self._defaults.get(key)
1197 561 return v
1198 562
1199 563 def resource(self, mapping, key):
1200 564 """Return internal data (e.g. cache) used for keyword/function
1201 565 evaluation"""
1202 566 v = None
1203 567 if key in self._resources:
1204 568 v = mapping.get(key)
1205 569 if v is None:
1206 570 v = self._resources.get(key)
1207 571 if v is None:
1208 572 raise templateutil.ResourceUnavailable(
1209 573 _('template resource not available: %s') % key)
1210 574 return v
1211 575
1212 576 def _load(self, t):
1213 577 '''load, parse, and cache a template'''
1214 578 if t not in self._cache:
1215 579 # put poison to cut recursion while compiling 't'
1216 580 self._cache[t] = (_runrecursivesymbol, t)
1217 581 try:
1218 582 x = parse(self._loader(t))
1219 583 if self._aliasmap:
1220 584 x = _aliasrules.expand(self._aliasmap, x)
1221 585 self._cache[t] = compileexp(x, self, methods)
1222 586 except: # re-raises
1223 587 del self._cache[t]
1224 588 raise
1225 589 return self._cache[t]
1226 590
1227 591 def process(self, t, mapping):
1228 592 '''Perform expansion. t is name of map element to expand.
1229 593 mapping contains added elements for use during expansion. Is a
1230 594 generator.'''
1231 595 func, data = self._load(t)
1232 596 return _flatten(func(self, mapping, data))
1233 597
1234 598 engines = {'default': engine}
1235 599
1236 600 def stylelist():
1237 601 paths = templatepaths()
1238 602 if not paths:
1239 603 return _('no templates found, try `hg debuginstall` for more info')
1240 604 dirlist = os.listdir(paths[0])
1241 605 stylelist = []
1242 606 for file in dirlist:
1243 607 split = file.split(".")
1244 608 if split[-1] in ('orig', 'rej'):
1245 609 continue
1246 610 if split[0] == "map-cmdline":
1247 611 stylelist.append(split[1])
1248 612 return ", ".join(sorted(stylelist))
1249 613
1250 614 def _readmapfile(mapfile):
1251 615 """Load template elements from the given map file"""
1252 616 if not os.path.exists(mapfile):
1253 617 raise error.Abort(_("style '%s' not found") % mapfile,
1254 618 hint=_("available styles: %s") % stylelist())
1255 619
1256 620 base = os.path.dirname(mapfile)
1257 621 conf = config.config(includepaths=templatepaths())
1258 622 conf.read(mapfile, remap={'': 'templates'})
1259 623
1260 624 cache = {}
1261 625 tmap = {}
1262 626 aliases = []
1263 627
1264 628 val = conf.get('templates', '__base__')
1265 629 if val and val[0] not in "'\"":
1266 630 # treat as a pointer to a base class for this style
1267 631 path = util.normpath(os.path.join(base, val))
1268 632
1269 633 # fallback check in template paths
1270 634 if not os.path.exists(path):
1271 635 for p in templatepaths():
1272 636 p2 = util.normpath(os.path.join(p, val))
1273 637 if os.path.isfile(p2):
1274 638 path = p2
1275 639 break
1276 640 p3 = util.normpath(os.path.join(p2, "map"))
1277 641 if os.path.isfile(p3):
1278 642 path = p3
1279 643 break
1280 644
1281 645 cache, tmap, aliases = _readmapfile(path)
1282 646
1283 647 for key, val in conf['templates'].items():
1284 648 if not val:
1285 649 raise error.ParseError(_('missing value'),
1286 650 conf.source('templates', key))
1287 651 if val[0] in "'\"":
1288 652 if val[0] != val[-1]:
1289 653 raise error.ParseError(_('unmatched quotes'),
1290 654 conf.source('templates', key))
1291 655 cache[key] = unquotestring(val)
1292 656 elif key != '__base__':
1293 657 val = 'default', val
1294 658 if ':' in val[1]:
1295 659 val = val[1].split(':', 1)
1296 660 tmap[key] = val[0], os.path.join(base, val[1])
1297 661 aliases.extend(conf['templatealias'].items())
1298 662 return cache, tmap, aliases
1299 663
1300 664 class templater(object):
1301 665
1302 666 def __init__(self, filters=None, defaults=None, resources=None,
1303 667 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1304 668 """Create template engine optionally with preloaded template fragments
1305 669
1306 670 - ``filters``: a dict of functions to transform a value into another.
1307 671 - ``defaults``: a dict of symbol values/functions; may be overridden
1308 672 by a ``mapping`` dict.
1309 673 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1310 674 from user template; may be overridden by a ``mapping`` dict.
1311 675 - ``cache``: a dict of preloaded template fragments.
1312 676 - ``aliases``: a list of alias (name, replacement) pairs.
1313 677
1314 678 self.cache may be updated later to register additional template
1315 679 fragments.
1316 680 """
1317 681 if filters is None:
1318 682 filters = {}
1319 683 if defaults is None:
1320 684 defaults = {}
1321 685 if resources is None:
1322 686 resources = {}
1323 687 if cache is None:
1324 688 cache = {}
1325 689 self.cache = cache.copy()
1326 690 self.map = {}
1327 691 self.filters = templatefilters.filters.copy()
1328 692 self.filters.update(filters)
1329 693 self.defaults = defaults
1330 694 self._resources = {'templ': self}
1331 695 self._resources.update(resources)
1332 696 self._aliases = aliases
1333 697 self.minchunk, self.maxchunk = minchunk, maxchunk
1334 698 self.ecache = {}
1335 699
1336 700 @classmethod
1337 701 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1338 702 cache=None, minchunk=1024, maxchunk=65536):
1339 703 """Create templater from the specified map file"""
1340 704 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1341 705 cache, tmap, aliases = _readmapfile(mapfile)
1342 706 t.cache.update(cache)
1343 707 t.map = tmap
1344 708 t._aliases = aliases
1345 709 return t
1346 710
1347 711 def __contains__(self, key):
1348 712 return key in self.cache or key in self.map
1349 713
1350 714 def load(self, t):
1351 715 '''Get the template for the given template name. Use a local cache.'''
1352 716 if t not in self.cache:
1353 717 try:
1354 718 self.cache[t] = util.readfile(self.map[t][1])
1355 719 except KeyError as inst:
1356 720 raise templateutil.TemplateNotFound(
1357 721 _('"%s" not in template map') % inst.args[0])
1358 722 except IOError as inst:
1359 723 reason = (_('template file %s: %s')
1360 724 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1361 725 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1362 726 return self.cache[t]
1363 727
1364 728 def render(self, mapping):
1365 729 """Render the default unnamed template and return result as string"""
1366 730 mapping = pycompat.strkwargs(mapping)
1367 731 return templateutil.stringify(self('', **mapping))
1368 732
1369 733 def __call__(self, t, **mapping):
1370 734 mapping = pycompat.byteskwargs(mapping)
1371 735 ttype = t in self.map and self.map[t][0] or 'default'
1372 736 if ttype not in self.ecache:
1373 737 try:
1374 738 ecls = engines[ttype]
1375 739 except KeyError:
1376 740 raise error.Abort(_('invalid template engine: %s') % ttype)
1377 741 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1378 742 self._resources, self._aliases)
1379 743 proc = self.ecache[ttype]
1380 744
1381 745 stream = proc.process(t, mapping)
1382 746 if self.minchunk:
1383 747 stream = util.increasingchunks(stream, min=self.minchunk,
1384 748 max=self.maxchunk)
1385 749 return stream
1386 750
1387 751 def templatepaths():
1388 752 '''return locations used for template files.'''
1389 753 pathsrel = ['templates']
1390 754 paths = [os.path.normpath(os.path.join(util.datapath, f))
1391 755 for f in pathsrel]
1392 756 return [p for p in paths if os.path.isdir(p)]
1393 757
1394 758 def templatepath(name):
1395 759 '''return location of template file. returns None if not found.'''
1396 760 for p in templatepaths():
1397 761 f = os.path.join(p, name)
1398 762 if os.path.exists(f):
1399 763 return f
1400 764 return None
1401 765
1402 766 def stylemap(styles, paths=None):
1403 767 """Return path to mapfile for a given style.
1404 768
1405 769 Searches mapfile in the following locations:
1406 770 1. templatepath/style/map
1407 771 2. templatepath/map-style
1408 772 3. templatepath/map
1409 773 """
1410 774
1411 775 if paths is None:
1412 776 paths = templatepaths()
1413 777 elif isinstance(paths, bytes):
1414 778 paths = [paths]
1415 779
1416 780 if isinstance(styles, bytes):
1417 781 styles = [styles]
1418 782
1419 783 for style in styles:
1420 784 # only plain name is allowed to honor template paths
1421 785 if (not style
1422 786 or style in (pycompat.oscurdir, pycompat.ospardir)
1423 787 or pycompat.ossep in style
1424 788 or pycompat.osaltsep and pycompat.osaltsep in style):
1425 789 continue
1426 790 locations = [os.path.join(style, 'map'), 'map-' + style]
1427 791 locations.append('map')
1428 792
1429 793 for path in paths:
1430 794 for location in locations:
1431 795 mapfile = os.path.join(path, location)
1432 796 if os.path.isfile(mapfile):
1433 797 return style, mapfile
1434 798
1435 799 raise RuntimeError("No hgweb templates found in %r" % paths)
1436
1437 def loadfunction(ui, extname, registrarobj):
1438 """Load template function from specified registrarobj
1439 """
1440 for name, func in registrarobj._table.iteritems():
1441 funcs[name] = func
1442
1443 # tell hggettext to extract docstrings from these functions:
1444 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now