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