##// END OF EJS Templates
merge default into stable for 4.0 code freeze
Kevin Bullock -
r30215:438173c4 merge 4.0-rc stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,13 b''
1 Our full contribution guidelines are in our wiki, please see:
2
3 https://www.mercurial-scm.org/wiki/ContributingChanges
4
5 If you just want a checklist to follow, you can go straight to
6
7 https://www.mercurial-scm.org/wiki/ContributingChanges#Submission_checklist
8
9 If you can't run the entire testsuite for some reason (it can be
10 difficult on Windows), please at least run `contrib/check-code.py` on
11 any files you've modified and run `python contrib/check-commit` on any
12 commits you've made (for example, `python contrib/check-commit
13 273ce12ad8f1` will report some style violations on a very old commit).
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,264 +1,266 b''
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
17
18 # Set this to e.g. "mingw32" to use a non-default compiler.
18 # Set this to e.g. "mingw32" to use a non-default compiler.
19 COMPILER=
19 COMPILER=
20
20
21 COMPILERFLAG_tmp_ =
21 COMPILERFLAG_tmp_ =
22 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
22 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
23 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
23 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
24
24
25 help:
25 help:
26 @echo 'Commonly used make targets:'
26 @echo 'Commonly used make targets:'
27 @echo ' all - build program and documentation'
27 @echo ' all - build program and documentation'
28 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
28 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
29 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
29 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
30 @echo ' local - build for inplace usage'
30 @echo ' local - build for inplace usage'
31 @echo ' tests - run all tests in the automatic test suite'
31 @echo ' tests - run all tests in the automatic test suite'
32 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
32 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
33 @echo ' dist - run all tests and create a source tarball in dist/'
33 @echo ' dist - run all tests and create a source tarball in dist/'
34 @echo ' clean - remove files created by other targets'
34 @echo ' clean - remove files created by other targets'
35 @echo ' (except installed files or dist source tarball)'
35 @echo ' (except installed files or dist source tarball)'
36 @echo ' update-pot - update i18n/hg.pot'
36 @echo ' update-pot - update i18n/hg.pot'
37 @echo
37 @echo
38 @echo 'Example for a system-wide installation under /usr/local:'
38 @echo 'Example for a system-wide installation under /usr/local:'
39 @echo ' make all && su -c "make install" && hg version'
39 @echo ' make all && su -c "make install" && hg version'
40 @echo
40 @echo
41 @echo 'Example for a local installation (usable in this directory):'
41 @echo 'Example for a local installation (usable in this directory):'
42 @echo ' make local && ./hg version'
42 @echo ' make local && ./hg version'
43
43
44 all: build doc
44 all: build doc
45
45
46 local:
46 local:
47 $(PYTHON) setup.py $(PURE) \
47 $(PYTHON) setup.py $(PURE) \
48 build_py -c -d . \
48 build_py -c -d . \
49 build_ext $(COMPILERFLAG) -i \
49 build_ext $(COMPILERFLAG) -i \
50 build_hgexe $(COMPILERFLAG) -i \
50 build_hgexe $(COMPILERFLAG) -i \
51 build_mo
51 build_mo
52 env HGRCPATH= $(PYTHON) hg version
52 env HGRCPATH= $(PYTHON) hg version
53
53
54 build:
54 build:
55 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
55 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
56
56
57 wheel:
57 wheel:
58 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
58 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
59
59
60 doc:
60 doc:
61 $(MAKE) -C doc
61 $(MAKE) -C doc
62
62
63 cleanbutpackages:
63 cleanbutpackages:
64 -$(PYTHON) setup.py clean --all # ignore errors from this command
64 -$(PYTHON) setup.py clean --all # ignore errors from this command
65 find contrib doc hgext hgext3rd i18n mercurial tests \
65 find contrib doc hgext hgext3rd i18n mercurial tests \
66 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
66 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
67 rm -f $(addprefix mercurial/,$(notdir $(wildcard mercurial/pure/[a-z]*.py)))
67 rm -f $(addprefix mercurial/,$(notdir $(wildcard mercurial/pure/[a-z]*.py)))
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 update-pot: i18n/hg.pot
125 update-pot: i18n/hg.pot
126
126
127 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
127 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
128 $(PYTHON) i18n/hggettext mercurial/commands.py \
128 $(PYTHON) i18n/hggettext mercurial/commands.py \
129 hgext/*.py hgext/*/__init__.py \
129 hgext/*.py hgext/*/__init__.py \
130 mercurial/fileset.py mercurial/revset.py \
130 mercurial/fileset.py mercurial/revset.py \
131 mercurial/templatefilters.py mercurial/templatekw.py \
131 mercurial/templatefilters.py mercurial/templatekw.py \
132 mercurial/templater.py \
132 mercurial/templater.py \
133 mercurial/filemerge.py \
133 mercurial/filemerge.py \
134 mercurial/hgweb/webcommands.py \
134 mercurial/hgweb/webcommands.py \
135 $(DOCFILES) > i18n/hg.pot.tmp
135 $(DOCFILES) > i18n/hg.pot.tmp
136 # All strings marked for translation in Mercurial contain
136 # All strings marked for translation in Mercurial contain
137 # ASCII characters only. But some files contain string
137 # ASCII characters only. But some files contain string
138 # literals like this '\037\213'. xgettext thinks it has to
138 # literals like this '\037\213'. xgettext thinks it has to
139 # parse them even though they are not marked for translation.
139 # parse them even though they are not marked for translation.
140 # Extracting with an explicit encoding of ISO-8859-1 will make
140 # Extracting with an explicit encoding of ISO-8859-1 will make
141 # xgettext "parse" and ignore them.
141 # xgettext "parse" and ignore them.
142 echo $(PYFILES) | xargs \
142 echo $(PYFILES) | xargs \
143 xgettext --package-name "Mercurial" \
143 xgettext --package-name "Mercurial" \
144 --msgid-bugs-address "<mercurial-devel@selenic.com>" \
144 --msgid-bugs-address "<mercurial-devel@selenic.com>" \
145 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
145 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
146 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
146 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
147 -d hg -p i18n -o hg.pot.tmp
147 -d hg -p i18n -o hg.pot.tmp
148 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
148 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
149 # The target file is not created before the last step. So it never is in
149 # The target file is not created before the last step. So it never is in
150 # an intermediate state.
150 # an intermediate state.
151 mv -f i18n/hg.pot.tmp i18n/hg.pot
151 mv -f i18n/hg.pot.tmp i18n/hg.pot
152
152
153 %.po: i18n/hg.pot
153 %.po: i18n/hg.pot
154 # work on a temporary copy for never having a half completed target
154 # work on a temporary copy for never having a half completed target
155 cp $@ $@.tmp
155 cp $@ $@.tmp
156 msgmerge --no-location --update $@.tmp $^
156 msgmerge --no-location --update $@.tmp $^
157 mv -f $@.tmp $@
157 mv -f $@.tmp $@
158
158
159 # Packaging targets
159 # Packaging targets
160
160
161 osx:
161 osx:
162 /usr/bin/python2.7 setup.py install --optimize=1 \
162 /usr/bin/python2.7 setup.py install --optimize=1 \
163 --root=build/mercurial/ --prefix=/usr/local/ \
163 --root=build/mercurial/ --prefix=/usr/local/ \
164 --install-lib=/Library/Python/2.7/site-packages/
164 --install-lib=/Library/Python/2.7/site-packages/
165 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
165 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
166 mkdir -p $${OUTPUTDIR:-dist}
166 mkdir -p $${OUTPUTDIR:-dist}
167 pkgbuild --root build/mercurial/ --identifier org.mercurial-scm.mercurial \
168 build/mercurial.pkg
169 HGVER=$$((cat build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py; echo 'print(version)') | python) && \
167 HGVER=$$((cat build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py; echo 'print(version)') | python) && \
170 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
168 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
169 pkgbuild --root build/mercurial/ \
170 --identifier org.mercurial-scm.mercurial \
171 --version "$${HGVER}" \
172 build/mercurial.pkg && \
171 productbuild --distribution contrib/macosx/distribution.xml \
173 productbuild --distribution contrib/macosx/distribution.xml \
172 --package-path build/ \
174 --package-path build/ \
173 --version "$${HGVER}" \
175 --version "$${HGVER}" \
174 --resources contrib/macosx/ \
176 --resources contrib/macosx/ \
175 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
177 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
176
178
177 deb:
179 deb:
178 contrib/builddeb
180 contrib/builddeb
179
181
180 ppa:
182 ppa:
181 contrib/builddeb --source-only
183 contrib/builddeb --source-only
182
184
183 docker-debian-jessie:
185 docker-debian-jessie:
184 mkdir -p packages/debian-jessie
186 mkdir -p packages/debian-jessie
185 contrib/dockerdeb debian jessie
187 contrib/dockerdeb debian jessie
186
188
187 contrib/docker/ubuntu-%: contrib/docker/ubuntu.template
189 contrib/docker/ubuntu-%: contrib/docker/ubuntu.template
188 sed "s/__CODENAME__/$*/" $< > $@
190 sed "s/__CODENAME__/$*/" $< > $@
189
191
190 docker-ubuntu-trusty: contrib/docker/ubuntu-trusty
192 docker-ubuntu-trusty: contrib/docker/ubuntu-trusty
191 contrib/dockerdeb ubuntu trusty
193 contrib/dockerdeb ubuntu trusty
192
194
193 docker-ubuntu-trusty-ppa: contrib/docker/ubuntu-trusty
195 docker-ubuntu-trusty-ppa: contrib/docker/ubuntu-trusty
194 contrib/dockerdeb ubuntu trusty --source-only
196 contrib/dockerdeb ubuntu trusty --source-only
195
197
196 docker-ubuntu-wily: contrib/docker/ubuntu-wily
198 docker-ubuntu-wily: contrib/docker/ubuntu-wily
197 contrib/dockerdeb ubuntu wily
199 contrib/dockerdeb ubuntu wily
198
200
199 docker-ubuntu-wily-ppa: contrib/docker/ubuntu-wily
201 docker-ubuntu-wily-ppa: contrib/docker/ubuntu-wily
200 contrib/dockerdeb ubuntu wily --source-only
202 contrib/dockerdeb ubuntu wily --source-only
201
203
202 docker-ubuntu-xenial: contrib/docker/ubuntu-xenial
204 docker-ubuntu-xenial: contrib/docker/ubuntu-xenial
203 contrib/dockerdeb ubuntu xenial
205 contrib/dockerdeb ubuntu xenial
204
206
205 docker-ubuntu-xenial-ppa: contrib/docker/ubuntu-xenial
207 docker-ubuntu-xenial-ppa: contrib/docker/ubuntu-xenial
206 contrib/dockerdeb ubuntu xenial --source-only
208 contrib/dockerdeb ubuntu xenial --source-only
207
209
208 fedora20:
210 fedora20:
209 mkdir -p packages/fedora20
211 mkdir -p packages/fedora20
210 contrib/buildrpm
212 contrib/buildrpm
211 cp rpmbuild/RPMS/*/* packages/fedora20
213 cp rpmbuild/RPMS/*/* packages/fedora20
212 cp rpmbuild/SRPMS/* packages/fedora20
214 cp rpmbuild/SRPMS/* packages/fedora20
213 rm -rf rpmbuild
215 rm -rf rpmbuild
214
216
215 docker-fedora20:
217 docker-fedora20:
216 mkdir -p packages/fedora20
218 mkdir -p packages/fedora20
217 contrib/dockerrpm fedora20
219 contrib/dockerrpm fedora20
218
220
219 fedora21:
221 fedora21:
220 mkdir -p packages/fedora21
222 mkdir -p packages/fedora21
221 contrib/buildrpm
223 contrib/buildrpm
222 cp rpmbuild/RPMS/*/* packages/fedora21
224 cp rpmbuild/RPMS/*/* packages/fedora21
223 cp rpmbuild/SRPMS/* packages/fedora21
225 cp rpmbuild/SRPMS/* packages/fedora21
224 rm -rf rpmbuild
226 rm -rf rpmbuild
225
227
226 docker-fedora21:
228 docker-fedora21:
227 mkdir -p packages/fedora21
229 mkdir -p packages/fedora21
228 contrib/dockerrpm fedora21
230 contrib/dockerrpm fedora21
229
231
230 centos5:
232 centos5:
231 mkdir -p packages/centos5
233 mkdir -p packages/centos5
232 contrib/buildrpm --withpython
234 contrib/buildrpm --withpython
233 cp rpmbuild/RPMS/*/* packages/centos5
235 cp rpmbuild/RPMS/*/* packages/centos5
234 cp rpmbuild/SRPMS/* packages/centos5
236 cp rpmbuild/SRPMS/* packages/centos5
235
237
236 docker-centos5:
238 docker-centos5:
237 mkdir -p packages/centos5
239 mkdir -p packages/centos5
238 contrib/dockerrpm centos5 --withpython
240 contrib/dockerrpm centos5 --withpython
239
241
240 centos6:
242 centos6:
241 mkdir -p packages/centos6
243 mkdir -p packages/centos6
242 contrib/buildrpm
244 contrib/buildrpm
243 cp rpmbuild/RPMS/*/* packages/centos6
245 cp rpmbuild/RPMS/*/* packages/centos6
244 cp rpmbuild/SRPMS/* packages/centos6
246 cp rpmbuild/SRPMS/* packages/centos6
245
247
246 docker-centos6:
248 docker-centos6:
247 mkdir -p packages/centos6
249 mkdir -p packages/centos6
248 contrib/dockerrpm centos6
250 contrib/dockerrpm centos6
249
251
250 centos7:
252 centos7:
251 mkdir -p packages/centos7
253 mkdir -p packages/centos7
252 contrib/buildrpm
254 contrib/buildrpm
253 cp rpmbuild/RPMS/*/* packages/centos7
255 cp rpmbuild/RPMS/*/* packages/centos7
254 cp rpmbuild/SRPMS/* packages/centos7
256 cp rpmbuild/SRPMS/* packages/centos7
255
257
256 docker-centos7:
258 docker-centos7:
257 mkdir -p packages/centos7
259 mkdir -p packages/centos7
258 contrib/dockerrpm centos7
260 contrib/dockerrpm centos7
259
261
260 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
262 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
261 install-doc install-home install-home-bin install-home-doc \
263 install-doc install-home install-home-bin install-home-doc \
262 dist dist-notests check tests check-code update-pot \
264 dist dist-notests check tests check-code update-pot \
263 osx fedora20 docker-fedora20 fedora21 docker-fedora21 \
265 osx fedora20 docker-fedora20 fedora21 docker-fedora21 \
264 centos5 docker-centos5 centos6 docker-centos6 centos7 docker-centos7
266 centos5 docker-centos5 centos6 docker-centos6 centos7 docker-centos7
@@ -1,642 +1,644 b''
1 # bash completion for the Mercurial distributed SCM -*- sh -*-
1 # bash completion for the Mercurial distributed SCM -*- sh -*-
2
2
3 # Docs:
3 # Docs:
4 #
4 #
5 # If you source this file from your .bashrc, bash should be able to
5 # If you source this file from your .bashrc, bash should be able to
6 # complete a command line that uses hg with all the available commands
6 # complete a command line that uses hg with all the available commands
7 # and options and sometimes even arguments.
7 # and options and sometimes even arguments.
8 #
8 #
9 # Mercurial allows you to define additional commands through extensions.
9 # Mercurial allows you to define additional commands through extensions.
10 # Bash should be able to automatically figure out the name of these new
10 # Bash should be able to automatically figure out the name of these new
11 # commands and their options. See below for how to define _hg_opt_foo
11 # commands and their options. See below for how to define _hg_opt_foo
12 # and _hg_cmd_foo functions to fine-tune the completion for option and
12 # and _hg_cmd_foo functions to fine-tune the completion for option and
13 # non-option arguments, respectively.
13 # non-option arguments, respectively.
14 #
14 #
15 #
15 #
16 # Notes about completion for specific commands:
16 # Notes about completion for specific commands:
17 #
17 #
18 # - the completion function for the email command from the patchbomb
18 # - the completion function for the email command from the patchbomb
19 # extension will try to call _hg_emails to get a list of e-mail
19 # extension will try to call _hg_emails to get a list of e-mail
20 # addresses. It's up to the user to define this function. For
20 # addresses. It's up to the user to define this function. For
21 # example, put the addresses of the lists that you usually patchbomb
21 # example, put the addresses of the lists that you usually patchbomb
22 # in ~/.patchbomb-to and the addresses that you usually use to send
22 # in ~/.patchbomb-to and the addresses that you usually use to send
23 # the patchbombs in ~/.patchbomb-from and use something like this:
23 # the patchbombs in ~/.patchbomb-from and use something like this:
24 #
24 #
25 # _hg_emails()
25 # _hg_emails()
26 # {
26 # {
27 # if [ -r ~/.patchbomb-$1 ]; then
27 # if [ -r ~/.patchbomb-$1 ]; then
28 # cat ~/.patchbomb-$1
28 # cat ~/.patchbomb-$1
29 # fi
29 # fi
30 # }
30 # }
31 #
31 #
32 #
32 #
33 # Writing completion functions for additional commands:
33 # Writing completion functions for additional commands:
34 #
34 #
35 # If it exists, the function _hg_cmd_foo will be called without
35 # If it exists, the function _hg_cmd_foo will be called without
36 # arguments to generate the completion candidates for the hg command
36 # arguments to generate the completion candidates for the hg command
37 # "foo". If the command receives some arguments that aren't options
37 # "foo". If the command receives some arguments that aren't options
38 # even though they start with a "-", you can define a function called
38 # even though they start with a "-", you can define a function called
39 # _hg_opt_foo to generate the completion candidates. If _hg_opt_foo
39 # _hg_opt_foo to generate the completion candidates. If _hg_opt_foo
40 # doesn't return 0, regular completion for options is attempted.
40 # doesn't return 0, regular completion for options is attempted.
41 #
41 #
42 # In addition to the regular completion variables provided by bash,
42 # In addition to the regular completion variables provided by bash,
43 # the following variables are also set:
43 # the following variables are also set:
44 # - $hg - the hg program being used (e.g. /usr/bin/hg)
44 # - $hg - the hg program being used (e.g. /usr/bin/hg)
45 # - $cmd - the name of the hg command being completed
45 # - $cmd - the name of the hg command being completed
46 # - $cmd_index - the index of $cmd in $COMP_WORDS
46 # - $cmd_index - the index of $cmd in $COMP_WORDS
47 # - $cur - the current argument being completed
47 # - $cur - the current argument being completed
48 # - $prev - the argument before $cur
48 # - $prev - the argument before $cur
49 # - $global_args - "|"-separated list of global options that accept
49 # - $global_args - "|"-separated list of global options that accept
50 # an argument (e.g. '--cwd|-R|--repository')
50 # an argument (e.g. '--cwd|-R|--repository')
51 # - $canonical - 1 if we canonicalized $cmd before calling the function
51 # - $canonical - 1 if we canonicalized $cmd before calling the function
52 # 0 otherwise
52 # 0 otherwise
53 #
53 #
54
54
55 shopt -s extglob
55 shopt -s extglob
56
56
57 _hg_cmd()
57 _hg_cmd()
58 {
58 {
59 HGPLAIN=1 "$hg" "$@" 2>/dev/null
59 HGPLAIN=1 "$hg" "$@" 2>/dev/null
60 }
60 }
61
61
62 _hg_commands()
62 _hg_commands()
63 {
63 {
64 local commands
64 local commands
65 commands="$(HGPLAINEXCEPT=alias _hg_cmd debugcomplete "$cur")" || commands=""
65 commands="$(HGPLAINEXCEPT=alias _hg_cmd debugcomplete "$cur")" || commands=""
66 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$commands' -- "$cur"))
66 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$commands' -- "$cur"))
67 }
67 }
68
68
69 _hg_paths()
69 _hg_paths()
70 {
70 {
71 local paths="$(_hg_cmd paths -q)"
71 local paths="$(_hg_cmd paths -q)"
72 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$paths' -- "$cur"))
72 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$paths' -- "$cur"))
73 }
73 }
74
74
75 _hg_repos()
75 _hg_repos()
76 {
76 {
77 local i
77 local i
78 for i in $(compgen -d -- "$cur"); do
78 for i in $(compgen -d -- "$cur"); do
79 test ! -d "$i"/.hg || COMPREPLY=(${COMPREPLY[@]:-} "$i")
79 test ! -d "$i"/.hg || COMPREPLY=(${COMPREPLY[@]:-} "$i")
80 done
80 done
81 }
81 }
82
82
83 _hg_debugpathcomplete()
83 _hg_debugpathcomplete()
84 {
84 {
85 local files="$(_hg_cmd debugpathcomplete $1 "$cur")"
85 local files="$(_hg_cmd debugpathcomplete $1 "$cur")"
86 local IFS=$'\n'
86 local IFS=$'\n'
87 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
87 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
88 }
88 }
89
89
90 _hg_status()
90 _hg_status()
91 {
91 {
92 local files="$(_hg_cmd status -n$1 "glob:$cur**")"
92 if [ -z "$HGCOMPLETE_NOSTATUS" ]; then
93 local IFS=$'\n'
93 local files="$(_hg_cmd status -n$1 "glob:$cur**")"
94 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
94 local IFS=$'\n'
95 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
96 fi
95 }
97 }
96
98
97 _hg_branches()
99 _hg_branches()
98 {
100 {
99 local branches="$(_hg_cmd branches -q)"
101 local branches="$(_hg_cmd branches -q)"
100 local IFS=$'\n'
102 local IFS=$'\n'
101 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$branches' -- "$cur"))
103 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$branches' -- "$cur"))
102 }
104 }
103
105
104 _hg_bookmarks()
106 _hg_bookmarks()
105 {
107 {
106 local bookmarks="$(_hg_cmd bookmarks -q)"
108 local bookmarks="$(_hg_cmd bookmarks -q)"
107 local IFS=$'\n'
109 local IFS=$'\n'
108 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$bookmarks' -- "$cur"))
110 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$bookmarks' -- "$cur"))
109 }
111 }
110
112
111 _hg_labels()
113 _hg_labels()
112 {
114 {
113 local labels="$(_hg_cmd debugnamecomplete "$cur")"
115 local labels="$(_hg_cmd debugnamecomplete "$cur")"
114 local IFS=$'\n'
116 local IFS=$'\n'
115 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$labels' -- "$cur"))
117 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$labels' -- "$cur"))
116 }
118 }
117
119
118 # this is "kind of" ugly...
120 # this is "kind of" ugly...
119 _hg_count_non_option()
121 _hg_count_non_option()
120 {
122 {
121 local i count=0
123 local i count=0
122 local filters="$1"
124 local filters="$1"
123
125
124 for ((i=1; $i<=$COMP_CWORD; i++)); do
126 for ((i=1; $i<=$COMP_CWORD; i++)); do
125 if [[ "${COMP_WORDS[i]}" != -* ]]; then
127 if [[ "${COMP_WORDS[i]}" != -* ]]; then
126 if [[ ${COMP_WORDS[i-1]} == @($filters|$global_args) ]]; then
128 if [[ ${COMP_WORDS[i-1]} == @($filters|$global_args) ]]; then
127 continue
129 continue
128 fi
130 fi
129 count=$(($count + 1))
131 count=$(($count + 1))
130 fi
132 fi
131 done
133 done
132
134
133 echo $(($count - 1))
135 echo $(($count - 1))
134 }
136 }
135
137
136 _hg_fix_wordlist()
138 _hg_fix_wordlist()
137 {
139 {
138 local LASTCHAR=' '
140 local LASTCHAR=' '
139 if [ ${#COMPREPLY[@]} = 1 ]; then
141 if [ ${#COMPREPLY[@]} = 1 ]; then
140 [ -d "$COMPREPLY" ] && LASTCHAR=/
142 [ -d "$COMPREPLY" ] && LASTCHAR=/
141 COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR")
143 COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR")
142 else
144 else
143 for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
145 for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
144 [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/
146 [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/
145 done
147 done
146 fi
148 fi
147 }
149 }
148
150
149 _hg()
151 _hg()
150 {
152 {
151 local cur prev cmd cmd_index opts i aliashg
153 local cur prev cmd cmd_index opts i aliashg
152 # global options that receive an argument
154 # global options that receive an argument
153 local global_args='--cwd|-R|--repository'
155 local global_args='--cwd|-R|--repository'
154 local hg="$1"
156 local hg="$1"
155 local canonical=0
157 local canonical=0
156
158
157 aliashg=$(alias $hg 2>/dev/null)
159 aliashg=$(alias $hg 2>/dev/null)
158 if [[ -n "$aliashg" ]]; then
160 if [[ -n "$aliashg" ]]; then
159 aliashg=${aliashg#"alias $hg='"}
161 aliashg=${aliashg#"alias $hg='"}
160 aliashg=${aliashg%"'"}
162 aliashg=${aliashg%"'"}
161 hg=$aliashg
163 hg=$aliashg
162 fi
164 fi
163
165
164 COMPREPLY=()
166 COMPREPLY=()
165 cur="$2"
167 cur="$2"
166 prev="$3"
168 prev="$3"
167
169
168 # searching for the command
170 # searching for the command
169 # (first non-option argument that doesn't follow a global option that
171 # (first non-option argument that doesn't follow a global option that
170 # receives an argument)
172 # receives an argument)
171 for ((i=1; $i<=$COMP_CWORD; i++)); do
173 for ((i=1; $i<=$COMP_CWORD; i++)); do
172 if [[ ${COMP_WORDS[i]} != -* ]]; then
174 if [[ ${COMP_WORDS[i]} != -* ]]; then
173 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
175 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
174 cmd="${COMP_WORDS[i]}"
176 cmd="${COMP_WORDS[i]}"
175 cmd_index=$i
177 cmd_index=$i
176 break
178 break
177 fi
179 fi
178 fi
180 fi
179 done
181 done
180
182
181 if [[ "$cur" == -* ]]; then
183 if [[ "$cur" == -* ]]; then
182 if [ "$(type -t "_hg_opt_$cmd")" = function ] && "_hg_opt_$cmd"; then
184 if [ "$(type -t "_hg_opt_$cmd")" = function ] && "_hg_opt_$cmd"; then
183 _hg_fix_wordlist
185 _hg_fix_wordlist
184 return
186 return
185 fi
187 fi
186
188
187 opts=$(HGPLAINEXCEPT=alias _hg_cmd debugcomplete --options "$cmd")
189 opts=$(HGPLAINEXCEPT=alias _hg_cmd debugcomplete --options "$cmd")
188
190
189 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur"))
191 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur"))
190 _hg_fix_wordlist
192 _hg_fix_wordlist
191 return
193 return
192 fi
194 fi
193
195
194 # global options
196 # global options
195 case "$prev" in
197 case "$prev" in
196 -R|--repository)
198 -R|--repository)
197 _hg_paths
199 _hg_paths
198 _hg_repos
200 _hg_repos
199 _hg_fix_wordlist
201 _hg_fix_wordlist
200 return
202 return
201 ;;
203 ;;
202 --cwd)
204 --cwd)
203 # Stick with default bash completion
205 # Stick with default bash completion
204 _hg_fix_wordlist
206 _hg_fix_wordlist
205 return
207 return
206 ;;
208 ;;
207 esac
209 esac
208
210
209 if [ -z "$cmd" ] || [ $COMP_CWORD -eq $i ]; then
211 if [ -z "$cmd" ] || [ $COMP_CWORD -eq $i ]; then
210 _hg_commands
212 _hg_commands
211 _hg_fix_wordlist
213 _hg_fix_wordlist
212 return
214 return
213 fi
215 fi
214
216
215 # try to generate completion candidates for whatever command the user typed
217 # try to generate completion candidates for whatever command the user typed
216 local help
218 local help
217 if _hg_command_specific; then
219 if _hg_command_specific; then
218 _hg_fix_wordlist
220 _hg_fix_wordlist
219 return
221 return
220 fi
222 fi
221
223
222 # canonicalize the command name and try again
224 # canonicalize the command name and try again
223 help=$(_hg_cmd help "$cmd")
225 help=$(_hg_cmd help "$cmd")
224 if [ $? -ne 0 ]; then
226 if [ $? -ne 0 ]; then
225 # Probably either the command doesn't exist or it's ambiguous
227 # Probably either the command doesn't exist or it's ambiguous
226 return
228 return
227 fi
229 fi
228 cmd=${help#hg }
230 cmd=${help#hg }
229 cmd=${cmd%%[$' \n']*}
231 cmd=${cmd%%[$' \n']*}
230 canonical=1
232 canonical=1
231 _hg_command_specific
233 _hg_command_specific
232 _hg_fix_wordlist
234 _hg_fix_wordlist
233 }
235 }
234
236
235 _hg_command_specific()
237 _hg_command_specific()
236 {
238 {
237 if [ "$(type -t "_hg_cmd_$cmd")" = function ]; then
239 if [ "$(type -t "_hg_cmd_$cmd")" = function ]; then
238 "_hg_cmd_$cmd"
240 "_hg_cmd_$cmd"
239 return 0
241 return 0
240 fi
242 fi
241
243
242 if [ "$cmd" != status ]; then
244 if [ "$cmd" != status ]; then
243 case "$prev" in
245 case "$prev" in
244 -r|--rev)
246 -r|--rev)
245 if [[ $canonical = 1 || status != "$cmd"* ]]; then
247 if [[ $canonical = 1 || status != "$cmd"* ]]; then
246 _hg_labels
248 _hg_labels
247 return 0
249 return 0
248 fi
250 fi
249 return 1
251 return 1
250 ;;
252 ;;
251 -B|--bookmark)
253 -B|--bookmark)
252 if [[ $canonical = 1 || status != "$cmd"* ]]; then
254 if [[ $canonical = 1 || status != "$cmd"* ]]; then
253 _hg_bookmarks
255 _hg_bookmarks
254 return 0
256 return 0
255 fi
257 fi
256 return 1
258 return 1
257 ;;
259 ;;
258 -b|--branch)
260 -b|--branch)
259 if [[ $canonical = 1 || status != "$cmd"* ]]; then
261 if [[ $canonical = 1 || status != "$cmd"* ]]; then
260 _hg_branches
262 _hg_branches
261 return 0
263 return 0
262 fi
264 fi
263 return 1
265 return 1
264 ;;
266 ;;
265 esac
267 esac
266 fi
268 fi
267
269
268 local aliascmd=$(_hg_cmd showconfig alias.$cmd | awk '{print $1}')
270 local aliascmd=$(_hg_cmd showconfig alias.$cmd | awk '{print $1}')
269 [ -n "$aliascmd" ] && cmd=$aliascmd
271 [ -n "$aliascmd" ] && cmd=$aliascmd
270
272
271 case "$cmd" in
273 case "$cmd" in
272 help)
274 help)
273 _hg_commands
275 _hg_commands
274 ;;
276 ;;
275 export)
277 export)
276 if _hg_ext_mq_patchlist qapplied && [ "${COMPREPLY[*]}" ]; then
278 if _hg_ext_mq_patchlist qapplied && [ "${COMPREPLY[*]}" ]; then
277 return 0
279 return 0
278 fi
280 fi
279 _hg_labels
281 _hg_labels
280 ;;
282 ;;
281 manifest|update|up|checkout|co)
283 manifest|update|up|checkout|co)
282 _hg_labels
284 _hg_labels
283 ;;
285 ;;
284 pull|push|outgoing|incoming)
286 pull|push|outgoing|incoming)
285 _hg_paths
287 _hg_paths
286 _hg_repos
288 _hg_repos
287 ;;
289 ;;
288 paths)
290 paths)
289 _hg_paths
291 _hg_paths
290 ;;
292 ;;
291 add)
293 add)
292 _hg_status "u"
294 _hg_status "u"
293 ;;
295 ;;
294 merge)
296 merge)
295 _hg_labels
297 _hg_labels
296 ;;
298 ;;
297 commit|ci|record)
299 commit|ci|record)
298 _hg_status "mar"
300 _hg_status "mar"
299 ;;
301 ;;
300 remove|rm)
302 remove|rm)
301 _hg_debugpathcomplete -n
303 _hg_debugpathcomplete -n
302 ;;
304 ;;
303 forget)
305 forget)
304 _hg_debugpathcomplete -fa
306 _hg_debugpathcomplete -fa
305 ;;
307 ;;
306 diff)
308 diff)
307 _hg_status "mar"
309 _hg_status "mar"
308 ;;
310 ;;
309 revert)
311 revert)
310 _hg_debugpathcomplete
312 _hg_debugpathcomplete
311 ;;
313 ;;
312 clone)
314 clone)
313 local count=$(_hg_count_non_option)
315 local count=$(_hg_count_non_option)
314 if [ $count = 1 ]; then
316 if [ $count = 1 ]; then
315 _hg_paths
317 _hg_paths
316 fi
318 fi
317 _hg_repos
319 _hg_repos
318 ;;
320 ;;
319 debugindex|debugindexdot)
321 debugindex|debugindexdot)
320 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.i" -- "$cur"))
322 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.i" -- "$cur"))
321 ;;
323 ;;
322 debugdata)
324 debugdata)
323 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.d" -- "$cur"))
325 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.d" -- "$cur"))
324 ;;
326 ;;
325 *)
327 *)
326 return 1
328 return 1
327 ;;
329 ;;
328 esac
330 esac
329
331
330 return 0
332 return 0
331 }
333 }
332
334
333 complete -o bashdefault -o default -o nospace -F _hg hg \
335 complete -o bashdefault -o default -o nospace -F _hg hg \
334 || complete -o default -o nospace -F _hg hg
336 || complete -o default -o nospace -F _hg hg
335
337
336
338
337 # Completion for commands provided by extensions
339 # Completion for commands provided by extensions
338
340
339 # bookmarks
341 # bookmarks
340 _hg_cmd_bookmarks()
342 _hg_cmd_bookmarks()
341 {
343 {
342 _hg_bookmarks
344 _hg_bookmarks
343 return
345 return
344 }
346 }
345
347
346 # mq
348 # mq
347 _hg_ext_mq_patchlist()
349 _hg_ext_mq_patchlist()
348 {
350 {
349 local patches
351 local patches
350 patches=$(_hg_cmd $1)
352 patches=$(_hg_cmd $1)
351 if [ $? -eq 0 ] && [ "$patches" ]; then
353 if [ $? -eq 0 ] && [ "$patches" ]; then
352 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$patches' -- "$cur"))
354 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$patches' -- "$cur"))
353 return 0
355 return 0
354 fi
356 fi
355 return 1
357 return 1
356 }
358 }
357
359
358 _hg_ext_mq_queues()
360 _hg_ext_mq_queues()
359 {
361 {
360 local root=$(_hg_cmd root)
362 local root=$(_hg_cmd root)
361 local n
363 local n
362 for n in $(cd "$root"/.hg && compgen -d -- "$cur"); do
364 for n in $(cd "$root"/.hg && compgen -d -- "$cur"); do
363 # I think we're usually not interested in the regular "patches" queue
365 # I think we're usually not interested in the regular "patches" queue
364 # so just filter it.
366 # so just filter it.
365 if [ "$n" != patches ] && [ -e "$root/.hg/$n/series" ]; then
367 if [ "$n" != patches ] && [ -e "$root/.hg/$n/series" ]; then
366 COMPREPLY=(${COMPREPLY[@]:-} "$n")
368 COMPREPLY=(${COMPREPLY[@]:-} "$n")
367 fi
369 fi
368 done
370 done
369 }
371 }
370
372
371 _hg_cmd_qpop()
373 _hg_cmd_qpop()
372 {
374 {
373 if [[ "$prev" = @(-n|--name) ]]; then
375 if [[ "$prev" = @(-n|--name) ]]; then
374 _hg_ext_mq_queues
376 _hg_ext_mq_queues
375 return
377 return
376 fi
378 fi
377 _hg_ext_mq_patchlist qapplied
379 _hg_ext_mq_patchlist qapplied
378 }
380 }
379
381
380 _hg_cmd_qpush()
382 _hg_cmd_qpush()
381 {
383 {
382 if [[ "$prev" = @(-n|--name) ]]; then
384 if [[ "$prev" = @(-n|--name) ]]; then
383 _hg_ext_mq_queues
385 _hg_ext_mq_queues
384 return
386 return
385 fi
387 fi
386 _hg_ext_mq_patchlist qunapplied
388 _hg_ext_mq_patchlist qunapplied
387 }
389 }
388
390
389 _hg_cmd_qgoto()
391 _hg_cmd_qgoto()
390 {
392 {
391 if [[ "$prev" = @(-n|--name) ]]; then
393 if [[ "$prev" = @(-n|--name) ]]; then
392 _hg_ext_mq_queues
394 _hg_ext_mq_queues
393 return
395 return
394 fi
396 fi
395 _hg_ext_mq_patchlist qseries
397 _hg_ext_mq_patchlist qseries
396 }
398 }
397
399
398 _hg_cmd_qdelete()
400 _hg_cmd_qdelete()
399 {
401 {
400 local qcmd=qunapplied
402 local qcmd=qunapplied
401 if [[ "$prev" = @(-r|--rev) ]]; then
403 if [[ "$prev" = @(-r|--rev) ]]; then
402 qcmd=qapplied
404 qcmd=qapplied
403 fi
405 fi
404 _hg_ext_mq_patchlist $qcmd
406 _hg_ext_mq_patchlist $qcmd
405 }
407 }
406
408
407 _hg_cmd_qfinish()
409 _hg_cmd_qfinish()
408 {
410 {
409 if [[ "$prev" = @(-a|--applied) ]]; then
411 if [[ "$prev" = @(-a|--applied) ]]; then
410 return
412 return
411 fi
413 fi
412 _hg_ext_mq_patchlist qapplied
414 _hg_ext_mq_patchlist qapplied
413 }
415 }
414
416
415 _hg_cmd_qsave()
417 _hg_cmd_qsave()
416 {
418 {
417 if [[ "$prev" = @(-n|--name) ]]; then
419 if [[ "$prev" = @(-n|--name) ]]; then
418 _hg_ext_mq_queues
420 _hg_ext_mq_queues
419 return
421 return
420 fi
422 fi
421 }
423 }
422
424
423 _hg_cmd_rebase() {
425 _hg_cmd_rebase() {
424 if [[ "$prev" = @(-s|--source|-d|--dest|-b|--base|-r|--rev) ]]; then
426 if [[ "$prev" = @(-s|--source|-d|--dest|-b|--base|-r|--rev) ]]; then
425 _hg_labels
427 _hg_labels
426 return
428 return
427 fi
429 fi
428 }
430 }
429
431
430 _hg_cmd_strip()
432 _hg_cmd_strip()
431 {
433 {
432 if [[ "$prev" = @(-B|--bookmark) ]]; then
434 if [[ "$prev" = @(-B|--bookmark) ]]; then
433 _hg_bookmarks
435 _hg_bookmarks
434 return
436 return
435 fi
437 fi
436 _hg_labels
438 _hg_labels
437 }
439 }
438
440
439 _hg_cmd_qcommit()
441 _hg_cmd_qcommit()
440 {
442 {
441 local root=$(_hg_cmd root)
443 local root=$(_hg_cmd root)
442 # this is run in a sub-shell, so we can't use _hg_status
444 # this is run in a sub-shell, so we can't use _hg_status
443 local files=$(cd "$root/.hg/patches" && _hg_cmd status -nmar)
445 local files=$(cd "$root/.hg/patches" && _hg_cmd status -nmar)
444 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
446 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
445 }
447 }
446
448
447 _hg_cmd_qfold()
449 _hg_cmd_qfold()
448 {
450 {
449 _hg_ext_mq_patchlist qunapplied
451 _hg_ext_mq_patchlist qunapplied
450 }
452 }
451
453
452 _hg_cmd_qrename()
454 _hg_cmd_qrename()
453 {
455 {
454 _hg_ext_mq_patchlist qseries
456 _hg_ext_mq_patchlist qseries
455 }
457 }
456
458
457 _hg_cmd_qheader()
459 _hg_cmd_qheader()
458 {
460 {
459 _hg_ext_mq_patchlist qseries
461 _hg_ext_mq_patchlist qseries
460 }
462 }
461
463
462 _hg_cmd_qclone()
464 _hg_cmd_qclone()
463 {
465 {
464 local count=$(_hg_count_non_option)
466 local count=$(_hg_count_non_option)
465 if [ $count = 1 ]; then
467 if [ $count = 1 ]; then
466 _hg_paths
468 _hg_paths
467 fi
469 fi
468 _hg_repos
470 _hg_repos
469 }
471 }
470
472
471 _hg_ext_mq_guards()
473 _hg_ext_mq_guards()
472 {
474 {
473 _hg_cmd qselect --series | sed -e 's/^.//'
475 _hg_cmd qselect --series | sed -e 's/^.//'
474 }
476 }
475
477
476 _hg_cmd_qselect()
478 _hg_cmd_qselect()
477 {
479 {
478 local guards=$(_hg_ext_mq_guards)
480 local guards=$(_hg_ext_mq_guards)
479 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$guards' -- "$cur"))
481 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$guards' -- "$cur"))
480 }
482 }
481
483
482 _hg_cmd_qguard()
484 _hg_cmd_qguard()
483 {
485 {
484 local prefix=''
486 local prefix=''
485
487
486 if [[ "$cur" == +* ]]; then
488 if [[ "$cur" == +* ]]; then
487 prefix=+
489 prefix=+
488 elif [[ "$cur" == -* ]]; then
490 elif [[ "$cur" == -* ]]; then
489 prefix=-
491 prefix=-
490 fi
492 fi
491 local ncur=${cur#[-+]}
493 local ncur=${cur#[-+]}
492
494
493 if ! [ "$prefix" ]; then
495 if ! [ "$prefix" ]; then
494 _hg_ext_mq_patchlist qseries
496 _hg_ext_mq_patchlist qseries
495 return
497 return
496 fi
498 fi
497
499
498 local guards=$(_hg_ext_mq_guards)
500 local guards=$(_hg_ext_mq_guards)
499 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -P $prefix -W '$guards' -- "$ncur"))
501 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -P $prefix -W '$guards' -- "$ncur"))
500 }
502 }
501
503
502 _hg_opt_qguard()
504 _hg_opt_qguard()
503 {
505 {
504 local i
506 local i
505 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
507 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
506 if [[ ${COMP_WORDS[i]} != -* ]]; then
508 if [[ ${COMP_WORDS[i]} != -* ]]; then
507 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
509 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
508 _hg_cmd_qguard
510 _hg_cmd_qguard
509 return 0
511 return 0
510 fi
512 fi
511 elif [ "${COMP_WORDS[i]}" = -- ]; then
513 elif [ "${COMP_WORDS[i]}" = -- ]; then
512 _hg_cmd_qguard
514 _hg_cmd_qguard
513 return 0
515 return 0
514 fi
516 fi
515 done
517 done
516 return 1
518 return 1
517 }
519 }
518
520
519 _hg_cmd_qqueue()
521 _hg_cmd_qqueue()
520 {
522 {
521 local q
523 local q
522 local queues
524 local queues
523 local opts="--list --create --rename --delete --purge"
525 local opts="--list --create --rename --delete --purge"
524
526
525 queues=$( _hg_cmd qqueue --quiet )
527 queues=$( _hg_cmd qqueue --quiet )
526
528
527 COMPREPLY=( $( compgen -W "${opts} ${queues}" "${cur}" ) )
529 COMPREPLY=( $( compgen -W "${opts} ${queues}" "${cur}" ) )
528 }
530 }
529
531
530
532
531 # hbisect
533 # hbisect
532 _hg_cmd_bisect()
534 _hg_cmd_bisect()
533 {
535 {
534 local i subcmd
536 local i subcmd
535
537
536 # find the sub-command
538 # find the sub-command
537 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
539 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
538 if [[ ${COMP_WORDS[i]} != -* ]]; then
540 if [[ ${COMP_WORDS[i]} != -* ]]; then
539 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
541 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
540 subcmd="${COMP_WORDS[i]}"
542 subcmd="${COMP_WORDS[i]}"
541 break
543 break
542 fi
544 fi
543 fi
545 fi
544 done
546 done
545
547
546 if [ -z "$subcmd" ] || [ $COMP_CWORD -eq $i ] || [ "$subcmd" = help ]; then
548 if [ -z "$subcmd" ] || [ $COMP_CWORD -eq $i ] || [ "$subcmd" = help ]; then
547 COMPREPLY=(${COMPREPLY[@]:-}
549 COMPREPLY=(${COMPREPLY[@]:-}
548 $(compgen -W 'bad good help init next reset' -- "$cur"))
550 $(compgen -W 'bad good help init next reset' -- "$cur"))
549 return
551 return
550 fi
552 fi
551
553
552 case "$subcmd" in
554 case "$subcmd" in
553 good|bad)
555 good|bad)
554 _hg_labels
556 _hg_labels
555 ;;
557 ;;
556 esac
558 esac
557
559
558 return
560 return
559 }
561 }
560
562
561
563
562 # patchbomb
564 # patchbomb
563 _hg_cmd_email()
565 _hg_cmd_email()
564 {
566 {
565 case "$prev" in
567 case "$prev" in
566 -c|--cc|-t|--to|-f|--from|--bcc)
568 -c|--cc|-t|--to|-f|--from|--bcc)
567 # we need an e-mail address. let the user provide a function
569 # we need an e-mail address. let the user provide a function
568 # to get them
570 # to get them
569 if [ "$(type -t _hg_emails)" = function ]; then
571 if [ "$(type -t _hg_emails)" = function ]; then
570 local arg=to
572 local arg=to
571 if [[ "$prev" == @(-f|--from) ]]; then
573 if [[ "$prev" == @(-f|--from) ]]; then
572 arg=from
574 arg=from
573 fi
575 fi
574 local addresses=$(_hg_emails $arg)
576 local addresses=$(_hg_emails $arg)
575 COMPREPLY=(${COMPREPLY[@]:-}
577 COMPREPLY=(${COMPREPLY[@]:-}
576 $(compgen -W '$addresses' -- "$cur"))
578 $(compgen -W '$addresses' -- "$cur"))
577 fi
579 fi
578 return
580 return
579 ;;
581 ;;
580 -m|--mbox)
582 -m|--mbox)
581 # fallback to standard filename completion
583 # fallback to standard filename completion
582 return
584 return
583 ;;
585 ;;
584 -s|--subject)
586 -s|--subject)
585 # free form string
587 # free form string
586 return
588 return
587 ;;
589 ;;
588 esac
590 esac
589
591
590 _hg_labels
592 _hg_labels
591 return
593 return
592 }
594 }
593
595
594
596
595 # gpg
597 # gpg
596 _hg_cmd_sign()
598 _hg_cmd_sign()
597 {
599 {
598 _hg_labels
600 _hg_labels
599 }
601 }
600
602
601
603
602 # transplant
604 # transplant
603 _hg_cmd_transplant()
605 _hg_cmd_transplant()
604 {
606 {
605 case "$prev" in
607 case "$prev" in
606 -s|--source)
608 -s|--source)
607 _hg_paths
609 _hg_paths
608 _hg_repos
610 _hg_repos
609 return
611 return
610 ;;
612 ;;
611 --filter)
613 --filter)
612 # standard filename completion
614 # standard filename completion
613 return
615 return
614 ;;
616 ;;
615 esac
617 esac
616
618
617 # all other transplant options values and command parameters are revisions
619 # all other transplant options values and command parameters are revisions
618 _hg_labels
620 _hg_labels
619 return
621 return
620 }
622 }
621
623
622 # shelve
624 # shelve
623 _hg_shelves()
625 _hg_shelves()
624 {
626 {
625 local shelves="$(_hg_cmd shelve -ql)"
627 local shelves="$(_hg_cmd shelve -ql)"
626 local IFS=$'\n'
628 local IFS=$'\n'
627 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$shelves' -- "$cur"))
629 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$shelves' -- "$cur"))
628 }
630 }
629
631
630 _hg_cmd_shelve()
632 _hg_cmd_shelve()
631 {
633 {
632 if [[ "$prev" = @(-d|--delete|-l|--list|-p|--patch|--stat) ]]; then
634 if [[ "$prev" = @(-d|--delete|-l|--list|-p|--patch|--stat) ]]; then
633 _hg_shelves
635 _hg_shelves
634 else
636 else
635 _hg_status "mard"
637 _hg_status "mard"
636 fi
638 fi
637 }
639 }
638
640
639 _hg_cmd_unshelve()
641 _hg_cmd_unshelve()
640 {
642 {
641 _hg_shelves
643 _hg_shelves
642 }
644 }
@@ -1,657 +1,657 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """style and portability checker for Mercurial
10 """style and portability checker for Mercurial
11
11
12 when a rule triggers wrong, do one of the following (prefer one from top):
12 when a rule triggers wrong, do one of the following (prefer one from top):
13 * do the work-around the rule suggests
13 * do the work-around the rule suggests
14 * doublecheck that it is a false match
14 * doublecheck that it is a false match
15 * improve the rule pattern
15 * improve the rule pattern
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 (you can append a short comment and match this, like: #re-raises)
17 (you can append a short comment and match this, like: #re-raises)
18 * change the pattern to a warning and list the exception in test-check-code-hg
18 * change the pattern to a warning and list the exception in test-check-code-hg
19 * ONLY use no--check-code for skipping entire files from external sources
19 * ONLY use no--check-code for skipping entire files from external sources
20 """
20 """
21
21
22 from __future__ import absolute_import, print_function
22 from __future__ import absolute_import, print_function
23 import glob
23 import glob
24 import keyword
24 import keyword
25 import optparse
25 import optparse
26 import os
26 import os
27 import re
27 import re
28 import sys
28 import sys
29 if sys.version_info[0] < 3:
29 if sys.version_info[0] < 3:
30 opentext = open
30 opentext = open
31 else:
31 else:
32 def opentext(f):
32 def opentext(f):
33 return open(f, encoding='ascii')
33 return open(f, encoding='ascii')
34 try:
34 try:
35 xrange
35 xrange
36 except NameError:
36 except NameError:
37 xrange = range
37 xrange = range
38 try:
38 try:
39 import re2
39 import re2
40 except ImportError:
40 except ImportError:
41 re2 = None
41 re2 = None
42
42
43 def compilere(pat, multiline=False):
43 def compilere(pat, multiline=False):
44 if multiline:
44 if multiline:
45 pat = '(?m)' + pat
45 pat = '(?m)' + pat
46 if re2:
46 if re2:
47 try:
47 try:
48 return re2.compile(pat)
48 return re2.compile(pat)
49 except re2.error:
49 except re2.error:
50 pass
50 pass
51 return re.compile(pat)
51 return re.compile(pat)
52
52
53 # check "rules depending on implementation of repquote()" in each
53 # check "rules depending on implementation of repquote()" in each
54 # patterns (especially pypats), before changing around repquote()
54 # patterns (especially pypats), before changing around repquote()
55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
57 def _repquoteencodechr(i):
57 def _repquoteencodechr(i):
58 if i > 255:
58 if i > 255:
59 return 'u'
59 return 'u'
60 c = chr(i)
60 c = chr(i)
61 if c in _repquotefixedmap:
61 if c in _repquotefixedmap:
62 return _repquotefixedmap[c]
62 return _repquotefixedmap[c]
63 if c.isalpha():
63 if c.isalpha():
64 return 'x'
64 return 'x'
65 if c.isdigit():
65 if c.isdigit():
66 return 'n'
66 return 'n'
67 return 'o'
67 return 'o'
68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
69
69
70 def repquote(m):
70 def repquote(m):
71 t = m.group('text')
71 t = m.group('text')
72 t = t.translate(_repquotett)
72 t = t.translate(_repquotett)
73 return m.group('quote') + t + m.group('quote')
73 return m.group('quote') + t + m.group('quote')
74
74
75 def reppython(m):
75 def reppython(m):
76 comment = m.group('comment')
76 comment = m.group('comment')
77 if comment:
77 if comment:
78 l = len(comment.rstrip())
78 l = len(comment.rstrip())
79 return "#" * l + comment[l:]
79 return "#" * l + comment[l:]
80 return repquote(m)
80 return repquote(m)
81
81
82 def repcomment(m):
82 def repcomment(m):
83 return m.group(1) + "#" * len(m.group(2))
83 return m.group(1) + "#" * len(m.group(2))
84
84
85 def repccomment(m):
85 def repccomment(m):
86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
87 return m.group(1) + t + "*/"
87 return m.group(1) + t + "*/"
88
88
89 def repcallspaces(m):
89 def repcallspaces(m):
90 t = re.sub(r"\n\s+", "\n", m.group(2))
90 t = re.sub(r"\n\s+", "\n", m.group(2))
91 return m.group(1) + t
91 return m.group(1) + t
92
92
93 def repinclude(m):
93 def repinclude(m):
94 return m.group(1) + "<foo>"
94 return m.group(1) + "<foo>"
95
95
96 def rephere(m):
96 def rephere(m):
97 t = re.sub(r"\S", "x", m.group(2))
97 t = re.sub(r"\S", "x", m.group(2))
98 return m.group(1) + t
98 return m.group(1) + t
99
99
100
100
101 testpats = [
101 testpats = [
102 [
102 [
103 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
103 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
105 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
105 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
106 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
106 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
107 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
107 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
109 (r'echo -n', "don't use 'echo -n', use printf"),
109 (r'echo -n', "don't use 'echo -n', use printf"),
110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
111 (r'head -c', "don't use 'head -c', use 'dd'"),
111 (r'head -c', "don't use 'head -c', use 'dd'"),
112 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
112 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
114 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
114 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
118 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
118 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
119 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
119 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
120 "use egrep for extended grep syntax"),
120 "use egrep for extended grep syntax"),
121 (r'/bin/', "don't use explicit paths for tools"),
121 (r'/bin/', "don't use explicit paths for tools"),
122 (r'[^\n]\Z', "no trailing newline"),
122 (r'[^\n]\Z', "no trailing newline"),
123 (r'export .*=', "don't export and assign at once"),
123 (r'export .*=', "don't export and assign at once"),
124 (r'^source\b', "don't use 'source', use '.'"),
124 (r'^source\b', "don't use 'source', use '.'"),
125 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
125 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
126 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
126 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
127 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
127 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
128 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
128 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
129 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
129 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
130 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
130 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
131 (r'^alias\b.*=', "don't use alias, use a function"),
131 (r'^alias\b.*=', "don't use alias, use a function"),
132 (r'if\s*!', "don't use '!' to negate exit status"),
132 (r'if\s*!', "don't use '!' to negate exit status"),
133 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
133 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
134 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
134 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
135 (r'^( *)\t', "don't use tabs to indent"),
135 (r'^( *)\t', "don't use tabs to indent"),
136 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
136 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
137 "put a backslash-escaped newline after sed 'i' command"),
137 "put a backslash-escaped newline after sed 'i' command"),
138 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
138 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
139 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
139 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
140 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
140 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
141 (r'\butil\.Abort\b', "directly use error.Abort"),
141 (r'\butil\.Abort\b', "directly use error.Abort"),
142 (r'\|&', "don't use |&, use 2>&1"),
142 (r'\|&', "don't use |&, use 2>&1"),
143 (r'\w = +\w', "only one space after = allowed"),
143 (r'\w = +\w', "only one space after = allowed"),
144 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
144 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
145 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'")
145 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'")
146 ],
146 ],
147 # warnings
147 # warnings
148 [
148 [
149 (r'^function', "don't use 'function', use old style"),
149 (r'^function', "don't use 'function', use old style"),
150 (r'^diff.*-\w*N', "don't use 'diff -N'"),
150 (r'^diff.*-\w*N', "don't use 'diff -N'"),
151 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
151 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
152 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
152 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
153 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
153 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
154 ]
154 ]
155 ]
155 ]
156
156
157 testfilters = [
157 testfilters = [
158 (r"( *)(#([^\n]*\S)?)", repcomment),
158 (r"( *)(#([^\n]*\S)?)", repcomment),
159 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
159 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
160 ]
160 ]
161
161
162 winglobmsg = "use (glob) to match Windows paths too"
162 winglobmsg = "use (glob) to match Windows paths too"
163 uprefix = r"^ \$ "
163 uprefix = r"^ \$ "
164 utestpats = [
164 utestpats = [
165 [
165 [
166 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
166 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
167 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
167 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
168 "use regex test output patterns instead of sed"),
168 "use regex test output patterns instead of sed"),
169 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
169 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
170 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
170 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
171 (uprefix + r'.*\|\| echo.*(fail|error)',
171 (uprefix + r'.*\|\| echo.*(fail|error)',
172 "explicit exit code checks unnecessary"),
172 "explicit exit code checks unnecessary"),
173 (uprefix + r'set -e', "don't use set -e"),
173 (uprefix + r'set -e', "don't use set -e"),
174 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
174 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
175 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
175 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
176 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
176 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
177 '# no-msys'), # in test-pull.t which is skipped on windows
177 '# no-msys'), # in test-pull.t which is skipped on windows
178 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
178 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
179 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
179 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
180 winglobmsg),
180 winglobmsg),
181 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
181 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
182 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
182 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
183 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
183 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
184 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
184 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
185 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
185 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
186 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
186 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
187 (r'^ moving \S+/.*[^)]$', winglobmsg),
187 (r'^ moving \S+/.*[^)]$', winglobmsg),
188 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
188 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
189 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
189 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
190 (r'^ .*file://\$TESTTMP',
190 (r'^ .*file://\$TESTTMP',
191 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
191 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
192 (r'^ [^$>].*27\.0\.0\.1.*[^)]$',
192 (r'^ [^$>].*27\.0\.0\.1.*[^)]$',
193 'use (glob) to match localhost IP on hosts without 127.0.0.1 too'),
193 'use (glob) to match localhost IP on hosts without 127.0.0.1 too'),
194 (r'^ (cat|find): .*: No such file or directory',
194 (r'^ (cat|find): .*: No such file or directory',
195 'use test -f to test for file existence'),
195 'use test -f to test for file existence'),
196 (r'^ diff -[^ -]*p',
196 (r'^ diff -[^ -]*p',
197 "don't use (external) diff with -p for portability"),
197 "don't use (external) diff with -p for portability"),
198 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
198 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
199 "glob timezone field in diff output for portability"),
199 "glob timezone field in diff output for portability"),
200 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
200 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
201 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
201 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
202 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
202 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
203 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
203 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
204 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
204 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
205 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
205 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
206 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
206 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
207 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
207 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
208 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
208 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
209 ],
209 ],
210 # warnings
210 # warnings
211 [
211 [
212 (r'^ (?!.*127\.0\.0\.1)[^*?/\n]* \(glob\)$',
212 (r'^ (?!.*127\.0\.0\.1)[^*?/\n]* \(glob\)$',
213 "glob match with no glob string (?, *, /, and 127.0.0.1)"),
213 "glob match with no glob string (?, *, /, and 127.0.0.1)"),
214 ]
214 ]
215 ]
215 ]
216
216
217 for i in [0, 1]:
217 for i in [0, 1]:
218 for tp in testpats[i]:
218 for tp in testpats[i]:
219 p = tp[0]
219 p = tp[0]
220 m = tp[1]
220 m = tp[1]
221 if p.startswith(r'^'):
221 if p.startswith(r'^'):
222 p = r"^ [$>] (%s)" % p[1:]
222 p = r"^ [$>] (%s)" % p[1:]
223 else:
223 else:
224 p = r"^ [$>] .*(%s)" % p
224 p = r"^ [$>] .*(%s)" % p
225 utestpats[i].append((p, m) + tp[2:])
225 utestpats[i].append((p, m) + tp[2:])
226
226
227 utestfilters = [
227 utestfilters = [
228 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
228 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
229 (r"( +)(#([^\n]*\S)?)", repcomment),
229 (r"( +)(#([^\n]*\S)?)", repcomment),
230 ]
230 ]
231
231
232 pypats = [
232 pypats = [
233 [
233 [
234 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
234 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
235 "tuple parameter unpacking not available in Python 3+"),
235 "tuple parameter unpacking not available in Python 3+"),
236 (r'lambda\s*\(.*,.*\)',
236 (r'lambda\s*\(.*,.*\)',
237 "tuple parameter unpacking not available in Python 3+"),
237 "tuple parameter unpacking not available in Python 3+"),
238 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
238 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
239 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
239 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
240 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
240 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
241 'dict-from-generator'),
241 'dict-from-generator'),
242 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
242 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
243 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
243 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
244 (r'^\s*\t', "don't use tabs"),
244 (r'^\s*\t', "don't use tabs"),
245 (r'\S;\s*\n', "semicolon"),
245 (r'\S;\s*\n', "semicolon"),
246 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
246 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
247 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
247 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
248 (r'(\w|\)),\w', "missing whitespace after ,"),
248 (r'(\w|\)),\w', "missing whitespace after ,"),
249 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
249 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
250 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
250 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
251 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
251 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
252 (r'.{81}', "line too long"),
252 (r'.{81}', "line too long"),
253 (r'[^\n]\Z', "no trailing newline"),
253 (r'[^\n]\Z', "no trailing newline"),
254 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
254 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
255 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
255 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
256 # "don't use underbars in identifiers"),
256 # "don't use underbars in identifiers"),
257 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
257 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
258 "don't use camelcase in identifiers"),
258 "don't use camelcase in identifiers"),
259 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
259 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
260 "linebreak after :"),
260 "linebreak after :"),
261 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
261 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
262 r'#.*old-style'),
262 r'#.*old-style'),
263 (r'class\s[^( \n]+\(\):',
263 (r'class\s[^( \n]+\(\):',
264 "class foo() creates old style object, use class foo(object)",
264 "class foo() creates old style object, use class foo(object)",
265 r'#.*old-style'),
265 r'#.*old-style'),
266 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
266 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
267 if k not in ('print', 'exec')),
267 if k not in ('print', 'exec')),
268 "Python keyword is not a function"),
268 "Python keyword is not a function"),
269 (r',]', "unneeded trailing ',' in list"),
269 (r',]', "unneeded trailing ',' in list"),
270 # (r'class\s[A-Z][^\(]*\((?!Exception)',
270 # (r'class\s[A-Z][^\(]*\((?!Exception)',
271 # "don't capitalize non-exception classes"),
271 # "don't capitalize non-exception classes"),
272 # (r'in range\(', "use xrange"),
272 # (r'in range\(', "use xrange"),
273 # (r'^\s*print\s+', "avoid using print in core and extensions"),
273 # (r'^\s*print\s+', "avoid using print in core and extensions"),
274 (r'[\x80-\xff]', "non-ASCII character literal"),
274 (r'[\x80-\xff]', "non-ASCII character literal"),
275 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
275 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
276 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
276 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
277 "gratuitous whitespace after Python keyword"),
277 "gratuitous whitespace after Python keyword"),
278 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
278 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
279 # (r'\s\s=', "gratuitous whitespace before ="),
279 # (r'\s\s=', "gratuitous whitespace before ="),
280 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
280 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
281 "missing whitespace around operator"),
281 "missing whitespace around operator"),
282 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
282 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
283 "missing whitespace around operator"),
283 "missing whitespace around operator"),
284 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
284 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
285 "missing whitespace around operator"),
285 "missing whitespace around operator"),
286 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
286 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
287 "wrong whitespace around ="),
287 "wrong whitespace around ="),
288 (r'\([^()]*( =[^=]|[^<>!=]= )',
288 (r'\([^()]*( =[^=]|[^<>!=]= )',
289 "no whitespace around = for named parameters"),
289 "no whitespace around = for named parameters"),
290 (r'raise Exception', "don't raise generic exceptions"),
290 (r'raise Exception', "don't raise generic exceptions"),
291 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
291 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
292 "don't use old-style two-argument raise, use Exception(message)"),
292 "don't use old-style two-argument raise, use Exception(message)"),
293 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
293 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
294 (r' [=!]=\s+(True|False|None)',
294 (r' [=!]=\s+(True|False|None)',
295 "comparison with singleton, use 'is' or 'is not' instead"),
295 "comparison with singleton, use 'is' or 'is not' instead"),
296 (r'^\s*(while|if) [01]:',
296 (r'^\s*(while|if) [01]:',
297 "use True/False for constant Boolean expression"),
297 "use True/False for constant Boolean expression"),
298 (r'(?:(?<!def)\s+|\()hasattr',
298 (r'(?:(?<!def)\s+|\()hasattr\(',
299 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
299 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
300 (r'opener\([^)]*\).read\(',
300 (r'opener\([^)]*\).read\(',
301 "use opener.read() instead"),
301 "use opener.read() instead"),
302 (r'opener\([^)]*\).write\(',
302 (r'opener\([^)]*\).write\(',
303 "use opener.write() instead"),
303 "use opener.write() instead"),
304 (r'[\s\(](open|file)\([^)]*\)\.read\(',
304 (r'[\s\(](open|file)\([^)]*\)\.read\(',
305 "use util.readfile() instead"),
305 "use util.readfile() instead"),
306 (r'[\s\(](open|file)\([^)]*\)\.write\(',
306 (r'[\s\(](open|file)\([^)]*\)\.write\(',
307 "use util.writefile() instead"),
307 "use util.writefile() instead"),
308 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
308 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
309 "always assign an opened file to a variable, and close it afterwards"),
309 "always assign an opened file to a variable, and close it afterwards"),
310 (r'[\s\(](open|file)\([^)]*\)\.',
310 (r'[\s\(](open|file)\([^)]*\)\.',
311 "always assign an opened file to a variable, and close it afterwards"),
311 "always assign an opened file to a variable, and close it afterwards"),
312 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
312 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
313 (r'\.debug\(\_', "don't mark debug messages for translation"),
313 (r'\.debug\(\_', "don't mark debug messages for translation"),
314 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
314 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
315 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
315 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
316 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
316 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
317 'legacy exception syntax; use "as" instead of ","'),
317 'legacy exception syntax; use "as" instead of ","'),
318 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
318 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
319 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
319 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
320 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
320 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
321 (r'os\.path\.join\(.*, *(""|\'\')\)',
321 (r'os\.path\.join\(.*, *(""|\'\')\)',
322 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
322 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
323 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
323 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
324 # XXX only catch mutable arguments on the first line of the definition
324 # XXX only catch mutable arguments on the first line of the definition
325 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
325 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
326 (r'\butil\.Abort\b', "directly use error.Abort"),
326 (r'\butil\.Abort\b', "directly use error.Abort"),
327 (r'^import Queue', "don't use Queue, use util.queue + util.empty"),
327 (r'^import Queue', "don't use Queue, use util.queue + util.empty"),
328 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
328 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
329 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
329 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
330 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
330 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
331 (r'^import urlparse', "don't use urlparse, use util.urlparse"),
331 (r'^import urlparse', "don't use urlparse, use util.urlparse"),
332 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
332 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
333 (r'^import cPickle', "don't use cPickle, use util.pickle"),
333 (r'^import cPickle', "don't use cPickle, use util.pickle"),
334 (r'^import pickle', "don't use pickle, use util.pickle"),
334 (r'^import pickle', "don't use pickle, use util.pickle"),
335 (r'^import httplib', "don't use httplib, use util.httplib"),
335 (r'^import httplib', "don't use httplib, use util.httplib"),
336 (r'^import BaseHTTPServer', "use util.httpserver instead"),
336 (r'^import BaseHTTPServer', "use util.httpserver instead"),
337 (r'\.next\(\)', "don't use .next(), use next(...)"),
337 (r'\.next\(\)', "don't use .next(), use next(...)"),
338
338
339 # rules depending on implementation of repquote()
339 # rules depending on implementation of repquote()
340 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
340 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
341 'string join across lines with no space'),
341 'string join across lines with no space'),
342 (r'''(?x)ui\.(status|progress|write|note|warn)\(
342 (r'''(?x)ui\.(status|progress|write|note|warn)\(
343 [ \t\n#]*
343 [ \t\n#]*
344 (?# any strings/comments might precede a string, which
344 (?# any strings/comments might precede a string, which
345 # contains translatable message)
345 # contains translatable message)
346 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
346 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
347 (?# sequence consisting of below might precede translatable message
347 (?# sequence consisting of below might precede translatable message
348 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
348 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
349 # - escaped character: "\\", "\n", "\0" ...
349 # - escaped character: "\\", "\n", "\0" ...
350 # - character other than '%', 'b' as '\', and 'x' as alphabet)
350 # - character other than '%', 'b' as '\', and 'x' as alphabet)
351 (['"]|\'\'\'|""")
351 (['"]|\'\'\'|""")
352 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
352 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
353 (?# this regexp can't use [^...] style,
353 (?# this regexp can't use [^...] style,
354 # because _preparepats forcibly adds "\n" into [^...],
354 # because _preparepats forcibly adds "\n" into [^...],
355 # even though this regexp wants match it against "\n")''',
355 # even though this regexp wants match it against "\n")''',
356 "missing _() in ui message (use () to hide false-positives)"),
356 "missing _() in ui message (use () to hide false-positives)"),
357 ],
357 ],
358 # warnings
358 # warnings
359 [
359 [
360 # rules depending on implementation of repquote()
360 # rules depending on implementation of repquote()
361 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
361 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
362 ]
362 ]
363 ]
363 ]
364
364
365 pyfilters = [
365 pyfilters = [
366 (r"""(?msx)(?P<comment>\#.*?$)|
366 (r"""(?msx)(?P<comment>\#.*?$)|
367 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
367 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
368 (?P<text>(([^\\]|\\.)*?))
368 (?P<text>(([^\\]|\\.)*?))
369 (?P=quote))""", reppython),
369 (?P=quote))""", reppython),
370 ]
370 ]
371
371
372 txtfilters = []
372 txtfilters = []
373
373
374 txtpats = [
374 txtpats = [
375 [
375 [
376 ('\s$', 'trailing whitespace'),
376 ('\s$', 'trailing whitespace'),
377 ('.. note::[ \n][^\n]', 'add two newlines after note::')
377 ('.. note::[ \n][^\n]', 'add two newlines after note::')
378 ],
378 ],
379 []
379 []
380 ]
380 ]
381
381
382 cpats = [
382 cpats = [
383 [
383 [
384 (r'//', "don't use //-style comments"),
384 (r'//', "don't use //-style comments"),
385 (r'^ ', "don't use spaces to indent"),
385 (r'^ ', "don't use spaces to indent"),
386 (r'\S\t', "don't use tabs except for indent"),
386 (r'\S\t', "don't use tabs except for indent"),
387 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
387 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
388 (r'.{81}', "line too long"),
388 (r'.{81}', "line too long"),
389 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
389 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
390 (r'return\(', "return is not a function"),
390 (r'return\(', "return is not a function"),
391 (r' ;', "no space before ;"),
391 (r' ;', "no space before ;"),
392 (r'[^;] \)', "no space before )"),
392 (r'[^;] \)', "no space before )"),
393 (r'[)][{]', "space between ) and {"),
393 (r'[)][{]', "space between ) and {"),
394 (r'\w+\* \w+', "use int *foo, not int* foo"),
394 (r'\w+\* \w+', "use int *foo, not int* foo"),
395 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
395 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
396 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
396 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
397 (r'\w,\w', "missing whitespace after ,"),
397 (r'\w,\w', "missing whitespace after ,"),
398 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
398 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
399 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
399 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
400 (r'^#\s+\w', "use #foo, not # foo"),
400 (r'^#\s+\w', "use #foo, not # foo"),
401 (r'[^\n]\Z', "no trailing newline"),
401 (r'[^\n]\Z', "no trailing newline"),
402 (r'^\s*#import\b', "use only #include in standard C code"),
402 (r'^\s*#import\b', "use only #include in standard C code"),
403 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
403 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
404 (r'strcat\(', "don't use strcat"),
404 (r'strcat\(', "don't use strcat"),
405
405
406 # rules depending on implementation of repquote()
406 # rules depending on implementation of repquote()
407 ],
407 ],
408 # warnings
408 # warnings
409 [
409 [
410 # rules depending on implementation of repquote()
410 # rules depending on implementation of repquote()
411 ]
411 ]
412 ]
412 ]
413
413
414 cfilters = [
414 cfilters = [
415 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
415 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
416 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
416 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
417 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
417 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
418 (r'(\()([^)]+\))', repcallspaces),
418 (r'(\()([^)]+\))', repcallspaces),
419 ]
419 ]
420
420
421 inutilpats = [
421 inutilpats = [
422 [
422 [
423 (r'\bui\.', "don't use ui in util"),
423 (r'\bui\.', "don't use ui in util"),
424 ],
424 ],
425 # warnings
425 # warnings
426 []
426 []
427 ]
427 ]
428
428
429 inrevlogpats = [
429 inrevlogpats = [
430 [
430 [
431 (r'\brepo\.', "don't use repo in revlog"),
431 (r'\brepo\.', "don't use repo in revlog"),
432 ],
432 ],
433 # warnings
433 # warnings
434 []
434 []
435 ]
435 ]
436
436
437 webtemplatefilters = []
437 webtemplatefilters = []
438
438
439 webtemplatepats = [
439 webtemplatepats = [
440 [],
440 [],
441 [
441 [
442 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
442 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
443 'follow desc keyword with either firstline or websub'),
443 'follow desc keyword with either firstline or websub'),
444 ]
444 ]
445 ]
445 ]
446
446
447 checks = [
447 checks = [
448 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
448 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
449 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
449 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
450 ('c', r'.*\.[ch]$', '', cfilters, cpats),
450 ('c', r'.*\.[ch]$', '', cfilters, cpats),
451 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
451 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
452 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
452 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
453 pyfilters, inrevlogpats),
453 pyfilters, inrevlogpats),
454 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
454 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
455 inutilpats),
455 inutilpats),
456 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
456 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
457 ('web template', r'mercurial/templates/.*\.tmpl', '',
457 ('web template', r'mercurial/templates/.*\.tmpl', '',
458 webtemplatefilters, webtemplatepats),
458 webtemplatefilters, webtemplatepats),
459 ]
459 ]
460
460
461 def _preparepats():
461 def _preparepats():
462 for c in checks:
462 for c in checks:
463 failandwarn = c[-1]
463 failandwarn = c[-1]
464 for pats in failandwarn:
464 for pats in failandwarn:
465 for i, pseq in enumerate(pats):
465 for i, pseq in enumerate(pats):
466 # fix-up regexes for multi-line searches
466 # fix-up regexes for multi-line searches
467 p = pseq[0]
467 p = pseq[0]
468 # \s doesn't match \n
468 # \s doesn't match \n
469 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
469 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
470 # [^...] doesn't match newline
470 # [^...] doesn't match newline
471 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
471 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
472
472
473 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
473 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
474 filters = c[3]
474 filters = c[3]
475 for i, flt in enumerate(filters):
475 for i, flt in enumerate(filters):
476 filters[i] = re.compile(flt[0]), flt[1]
476 filters[i] = re.compile(flt[0]), flt[1]
477
477
478 class norepeatlogger(object):
478 class norepeatlogger(object):
479 def __init__(self):
479 def __init__(self):
480 self._lastseen = None
480 self._lastseen = None
481
481
482 def log(self, fname, lineno, line, msg, blame):
482 def log(self, fname, lineno, line, msg, blame):
483 """print error related a to given line of a given file.
483 """print error related a to given line of a given file.
484
484
485 The faulty line will also be printed but only once in the case
485 The faulty line will also be printed but only once in the case
486 of multiple errors.
486 of multiple errors.
487
487
488 :fname: filename
488 :fname: filename
489 :lineno: line number
489 :lineno: line number
490 :line: actual content of the line
490 :line: actual content of the line
491 :msg: error message
491 :msg: error message
492 """
492 """
493 msgid = fname, lineno, line
493 msgid = fname, lineno, line
494 if msgid != self._lastseen:
494 if msgid != self._lastseen:
495 if blame:
495 if blame:
496 print("%s:%d (%s):" % (fname, lineno, blame))
496 print("%s:%d (%s):" % (fname, lineno, blame))
497 else:
497 else:
498 print("%s:%d:" % (fname, lineno))
498 print("%s:%d:" % (fname, lineno))
499 print(" > %s" % line)
499 print(" > %s" % line)
500 self._lastseen = msgid
500 self._lastseen = msgid
501 print(" " + msg)
501 print(" " + msg)
502
502
503 _defaultlogger = norepeatlogger()
503 _defaultlogger = norepeatlogger()
504
504
505 def getblame(f):
505 def getblame(f):
506 lines = []
506 lines = []
507 for l in os.popen('hg annotate -un %s' % f):
507 for l in os.popen('hg annotate -un %s' % f):
508 start, line = l.split(':', 1)
508 start, line = l.split(':', 1)
509 user, rev = start.split()
509 user, rev = start.split()
510 lines.append((line[1:-1], user, rev))
510 lines.append((line[1:-1], user, rev))
511 return lines
511 return lines
512
512
513 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
513 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
514 blame=False, debug=False, lineno=True):
514 blame=False, debug=False, lineno=True):
515 """checks style and portability of a given file
515 """checks style and portability of a given file
516
516
517 :f: filepath
517 :f: filepath
518 :logfunc: function used to report error
518 :logfunc: function used to report error
519 logfunc(filename, linenumber, linecontent, errormessage)
519 logfunc(filename, linenumber, linecontent, errormessage)
520 :maxerr: number of error to display before aborting.
520 :maxerr: number of error to display before aborting.
521 Set to false (default) to report all errors
521 Set to false (default) to report all errors
522
522
523 return True if no error is found, False otherwise.
523 return True if no error is found, False otherwise.
524 """
524 """
525 blamecache = None
525 blamecache = None
526 result = True
526 result = True
527
527
528 try:
528 try:
529 with opentext(f) as fp:
529 with opentext(f) as fp:
530 try:
530 try:
531 pre = post = fp.read()
531 pre = post = fp.read()
532 except UnicodeDecodeError as e:
532 except UnicodeDecodeError as e:
533 print("%s while reading %s" % (e, f))
533 print("%s while reading %s" % (e, f))
534 return result
534 return result
535 except IOError as e:
535 except IOError as e:
536 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
536 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
537 return result
537 return result
538
538
539 for name, match, magic, filters, pats in checks:
539 for name, match, magic, filters, pats in checks:
540 if debug:
540 if debug:
541 print(name, f)
541 print(name, f)
542 fc = 0
542 fc = 0
543 if not (re.match(match, f) or (magic and re.search(magic, pre))):
543 if not (re.match(match, f) or (magic and re.search(magic, pre))):
544 if debug:
544 if debug:
545 print("Skipping %s for %s it doesn't match %s" % (
545 print("Skipping %s for %s it doesn't match %s" % (
546 name, match, f))
546 name, match, f))
547 continue
547 continue
548 if "no-" "check-code" in pre:
548 if "no-" "check-code" in pre:
549 # If you're looking at this line, it's because a file has:
549 # If you're looking at this line, it's because a file has:
550 # no- check- code
550 # no- check- code
551 # but the reason to output skipping is to make life for
551 # but the reason to output skipping is to make life for
552 # tests easier. So, instead of writing it with a normal
552 # tests easier. So, instead of writing it with a normal
553 # spelling, we write it with the expected spelling from
553 # spelling, we write it with the expected spelling from
554 # tests/test-check-code.t
554 # tests/test-check-code.t
555 print("Skipping %s it has no-che?k-code (glob)" % f)
555 print("Skipping %s it has no-che?k-code (glob)" % f)
556 return "Skip" # skip checking this file
556 return "Skip" # skip checking this file
557 for p, r in filters:
557 for p, r in filters:
558 post = re.sub(p, r, post)
558 post = re.sub(p, r, post)
559 nerrs = len(pats[0]) # nerr elements are errors
559 nerrs = len(pats[0]) # nerr elements are errors
560 if warnings:
560 if warnings:
561 pats = pats[0] + pats[1]
561 pats = pats[0] + pats[1]
562 else:
562 else:
563 pats = pats[0]
563 pats = pats[0]
564 # print post # uncomment to show filtered version
564 # print post # uncomment to show filtered version
565
565
566 if debug:
566 if debug:
567 print("Checking %s for %s" % (name, f))
567 print("Checking %s for %s" % (name, f))
568
568
569 prelines = None
569 prelines = None
570 errors = []
570 errors = []
571 for i, pat in enumerate(pats):
571 for i, pat in enumerate(pats):
572 if len(pat) == 3:
572 if len(pat) == 3:
573 p, msg, ignore = pat
573 p, msg, ignore = pat
574 else:
574 else:
575 p, msg = pat
575 p, msg = pat
576 ignore = None
576 ignore = None
577 if i >= nerrs:
577 if i >= nerrs:
578 msg = "warning: " + msg
578 msg = "warning: " + msg
579
579
580 pos = 0
580 pos = 0
581 n = 0
581 n = 0
582 for m in p.finditer(post):
582 for m in p.finditer(post):
583 if prelines is None:
583 if prelines is None:
584 prelines = pre.splitlines()
584 prelines = pre.splitlines()
585 postlines = post.splitlines(True)
585 postlines = post.splitlines(True)
586
586
587 start = m.start()
587 start = m.start()
588 while n < len(postlines):
588 while n < len(postlines):
589 step = len(postlines[n])
589 step = len(postlines[n])
590 if pos + step > start:
590 if pos + step > start:
591 break
591 break
592 pos += step
592 pos += step
593 n += 1
593 n += 1
594 l = prelines[n]
594 l = prelines[n]
595
595
596 if ignore and re.search(ignore, l, re.MULTILINE):
596 if ignore and re.search(ignore, l, re.MULTILINE):
597 if debug:
597 if debug:
598 print("Skipping %s for %s:%s (ignore pattern)" % (
598 print("Skipping %s for %s:%s (ignore pattern)" % (
599 name, f, n))
599 name, f, n))
600 continue
600 continue
601 bd = ""
601 bd = ""
602 if blame:
602 if blame:
603 bd = 'working directory'
603 bd = 'working directory'
604 if not blamecache:
604 if not blamecache:
605 blamecache = getblame(f)
605 blamecache = getblame(f)
606 if n < len(blamecache):
606 if n < len(blamecache):
607 bl, bu, br = blamecache[n]
607 bl, bu, br = blamecache[n]
608 if bl == l:
608 if bl == l:
609 bd = '%s@%s' % (bu, br)
609 bd = '%s@%s' % (bu, br)
610
610
611 errors.append((f, lineno and n + 1, l, msg, bd))
611 errors.append((f, lineno and n + 1, l, msg, bd))
612 result = False
612 result = False
613
613
614 errors.sort()
614 errors.sort()
615 for e in errors:
615 for e in errors:
616 logfunc(*e)
616 logfunc(*e)
617 fc += 1
617 fc += 1
618 if maxerr and fc >= maxerr:
618 if maxerr and fc >= maxerr:
619 print(" (too many errors, giving up)")
619 print(" (too many errors, giving up)")
620 break
620 break
621
621
622 return result
622 return result
623
623
624 def main():
624 def main():
625 parser = optparse.OptionParser("%prog [options] [files]")
625 parser = optparse.OptionParser("%prog [options] [files]")
626 parser.add_option("-w", "--warnings", action="store_true",
626 parser.add_option("-w", "--warnings", action="store_true",
627 help="include warning-level checks")
627 help="include warning-level checks")
628 parser.add_option("-p", "--per-file", type="int",
628 parser.add_option("-p", "--per-file", type="int",
629 help="max warnings per file")
629 help="max warnings per file")
630 parser.add_option("-b", "--blame", action="store_true",
630 parser.add_option("-b", "--blame", action="store_true",
631 help="use annotate to generate blame info")
631 help="use annotate to generate blame info")
632 parser.add_option("", "--debug", action="store_true",
632 parser.add_option("", "--debug", action="store_true",
633 help="show debug information")
633 help="show debug information")
634 parser.add_option("", "--nolineno", action="store_false",
634 parser.add_option("", "--nolineno", action="store_false",
635 dest='lineno', help="don't show line numbers")
635 dest='lineno', help="don't show line numbers")
636
636
637 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
637 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
638 lineno=True)
638 lineno=True)
639 (options, args) = parser.parse_args()
639 (options, args) = parser.parse_args()
640
640
641 if len(args) == 0:
641 if len(args) == 0:
642 check = glob.glob("*")
642 check = glob.glob("*")
643 else:
643 else:
644 check = args
644 check = args
645
645
646 _preparepats()
646 _preparepats()
647
647
648 ret = 0
648 ret = 0
649 for f in check:
649 for f in check:
650 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
650 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
651 blame=options.blame, debug=options.debug,
651 blame=options.blame, debug=options.debug,
652 lineno=options.lineno):
652 lineno=options.lineno):
653 ret = 1
653 ret = 1
654 return ret
654 return ret
655
655
656 if __name__ == "__main__":
656 if __name__ == "__main__":
657 sys.exit(main())
657 sys.exit(main())
@@ -1,102 +1,107 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # Copyright 2014 Matt Mackall <mpm@selenic.com>
3 # Copyright 2014 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # A tool/hook to run basic sanity checks on commits/patches for
5 # A tool/hook to run basic sanity checks on commits/patches for
6 # submission to Mercurial. Install by adding the following to your
6 # submission to Mercurial. Install by adding the following to your
7 # .hg/hgrc:
7 # .hg/hgrc:
8 #
8 #
9 # [hooks]
9 # [hooks]
10 # pretxncommit = contrib/check-commit
10 # pretxncommit = contrib/check-commit
11 #
11 #
12 # The hook can be temporarily bypassed with:
12 # The hook can be temporarily bypassed with:
13 #
13 #
14 # $ BYPASS= hg commit
14 # $ BYPASS= hg commit
15 #
15 #
16 # See also: https://mercurial-scm.org/wiki/ContributingChanges
16 # See also: https://mercurial-scm.org/wiki/ContributingChanges
17
17
18 from __future__ import absolute_import, print_function
18 from __future__ import absolute_import, print_function
19
19
20 import os
20 import os
21 import re
21 import re
22 import sys
22 import sys
23
23
24 commitheader = r"^(?:# [^\n]*\n)*"
24 commitheader = r"^(?:# [^\n]*\n)*"
25 afterheader = commitheader + r"(?!#)"
25 afterheader = commitheader + r"(?!#)"
26 beforepatch = afterheader + r"(?!\n(?!@@))"
26 beforepatch = afterheader + r"(?!\n(?!@@))"
27
27
28 errors = [
28 errors = [
29 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
29 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
30 (beforepatch + r".*[(]issue \d\d\d",
30 (beforepatch + r".*[(]issue \d\d\d",
31 "no space allowed between issue and number"),
31 "no space allowed between issue and number"),
32 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
32 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
33 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
33 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
34 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
34 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
35 "summary line doesn't start with 'topic: '"),
35 "summary line doesn't start with 'topic: '"),
36 (afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"),
36 (afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"),
37 (afterheader + r"[^\n]*: *[A-Z][a-z]\S+", "don't capitalize summary lines"),
37 (afterheader + r"[^\n]*: *[A-Z][a-z]\S+", "don't capitalize summary lines"),
38 (afterheader + r"\S*[^A-Za-z0-9-]\S*: ",
38 (afterheader + r"\S*[^A-Za-z0-9-_]\S*: ",
39 "summary keyword should be most user-relevant one-word command or topic"),
39 "summary keyword should be most user-relevant one-word command or topic"),
40 (afterheader + r".*\.\s*\n", "don't add trailing period on summary line"),
40 (afterheader + r".*\.\s*\n", "don't add trailing period on summary line"),
41 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
41 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
42 (r"\n\+\n( |\+)\n", "adds double empty line"),
42 (r"\n\+\n( |\+)\n", "adds double empty line"),
43 (r"\n \n\+\n", "adds double empty line"),
43 (r"\n \n\+\n", "adds double empty line"),
44 (r"\n\+[ \t]+def [a-z]+_[a-z]", "adds a function with foo_bar naming"),
44 # Forbid "_" in function name.
45 #
46 # We skip the check for cffi related functions. They use names mapping the
47 # name of the C function. C function names may contain "_".
48 (r"\n\+[ \t]+def (?!cffi)[a-z]+_[a-z]",
49 "adds a function with foo_bar naming"),
45 ]
50 ]
46
51
47 word = re.compile('\S')
52 word = re.compile('\S')
48 def nonempty(first, second):
53 def nonempty(first, second):
49 if word.search(first):
54 if word.search(first):
50 return first
55 return first
51 return second
56 return second
52
57
53 def checkcommit(commit, node=None):
58 def checkcommit(commit, node=None):
54 exitcode = 0
59 exitcode = 0
55 printed = node is None
60 printed = node is None
56 hits = []
61 hits = []
57 for exp, msg in errors:
62 for exp, msg in errors:
58 for m in re.finditer(exp, commit):
63 for m in re.finditer(exp, commit):
59 end = m.end()
64 end = m.end()
60 trailing = re.search(r'(\\n)+$', exp)
65 trailing = re.search(r'(\\n)+$', exp)
61 if trailing:
66 if trailing:
62 end -= len(trailing.group()) / 2
67 end -= len(trailing.group()) / 2
63 hits.append((end, exp, msg))
68 hits.append((end, exp, msg))
64 if hits:
69 if hits:
65 hits.sort()
70 hits.sort()
66 pos = 0
71 pos = 0
67 last = ''
72 last = ''
68 for n, l in enumerate(commit.splitlines(True)):
73 for n, l in enumerate(commit.splitlines(True)):
69 pos += len(l)
74 pos += len(l)
70 while len(hits):
75 while len(hits):
71 end, exp, msg = hits[0]
76 end, exp, msg = hits[0]
72 if pos < end:
77 if pos < end:
73 break
78 break
74 if not printed:
79 if not printed:
75 printed = True
80 printed = True
76 print("node: %s" % node)
81 print("node: %s" % node)
77 print("%d: %s" % (n, msg))
82 print("%d: %s" % (n, msg))
78 print(" %s" % nonempty(l, last)[:-1])
83 print(" %s" % nonempty(l, last)[:-1])
79 if "BYPASS" not in os.environ:
84 if "BYPASS" not in os.environ:
80 exitcode = 1
85 exitcode = 1
81 del hits[0]
86 del hits[0]
82 last = nonempty(l, last)
87 last = nonempty(l, last)
83
88
84 return exitcode
89 return exitcode
85
90
86 def readcommit(node):
91 def readcommit(node):
87 return os.popen("hg export %s" % node).read()
92 return os.popen("hg export %s" % node).read()
88
93
89 if __name__ == "__main__":
94 if __name__ == "__main__":
90 exitcode = 0
95 exitcode = 0
91 node = os.environ.get("HG_NODE")
96 node = os.environ.get("HG_NODE")
92
97
93 if node:
98 if node:
94 commit = readcommit(node)
99 commit = readcommit(node)
95 exitcode = checkcommit(commit)
100 exitcode = checkcommit(commit)
96 elif sys.argv[1:]:
101 elif sys.argv[1:]:
97 for node in sys.argv[1:]:
102 for node in sys.argv[1:]:
98 exitcode |= checkcommit(readcommit(node), node)
103 exitcode |= checkcommit(readcommit(node), node)
99 else:
104 else:
100 commit = sys.stdin.read()
105 commit = sys.stdin.read()
101 exitcode = checkcommit(commit)
106 exitcode = checkcommit(commit)
102 sys.exit(exitcode)
107 sys.exit(exitcode)
@@ -1,96 +1,95 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-py3-compat - check Python 3 compatibility of Mercurial files
3 # check-py3-compat - check Python 3 compatibility of Mercurial files
4 #
4 #
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import, print_function
10 from __future__ import absolute_import, print_function
11
11
12 import ast
12 import ast
13 import imp
14 import os
13 import os
15 import sys
14 import sys
16 import traceback
15 import traceback
17
16
18 def check_compat_py2(f):
17 def check_compat_py2(f):
19 """Check Python 3 compatibility for a file with Python 2"""
18 """Check Python 3 compatibility for a file with Python 2"""
20 with open(f, 'rb') as fh:
19 with open(f, 'rb') as fh:
21 content = fh.read()
20 content = fh.read()
22 root = ast.parse(content)
21 root = ast.parse(content)
23
22
24 # Ignore empty files.
23 # Ignore empty files.
25 if not root.body:
24 if not root.body:
26 return
25 return
27
26
28 futures = set()
27 futures = set()
29 haveprint = False
28 haveprint = False
30 for node in ast.walk(root):
29 for node in ast.walk(root):
31 if isinstance(node, ast.ImportFrom):
30 if isinstance(node, ast.ImportFrom):
32 if node.module == '__future__':
31 if node.module == '__future__':
33 futures |= set(n.name for n in node.names)
32 futures |= set(n.name for n in node.names)
34 elif isinstance(node, ast.Print):
33 elif isinstance(node, ast.Print):
35 haveprint = True
34 haveprint = True
36
35
37 if 'absolute_import' not in futures:
36 if 'absolute_import' not in futures:
38 print('%s not using absolute_import' % f)
37 print('%s not using absolute_import' % f)
39 if haveprint and 'print_function' not in futures:
38 if haveprint and 'print_function' not in futures:
40 print('%s requires print_function' % f)
39 print('%s requires print_function' % f)
41
40
42 def check_compat_py3(f):
41 def check_compat_py3(f):
43 """Check Python 3 compatibility of a file with Python 3."""
42 """Check Python 3 compatibility of a file with Python 3."""
43 import importlib # not available on Python 2.6
44 with open(f, 'rb') as fh:
44 with open(f, 'rb') as fh:
45 content = fh.read()
45 content = fh.read()
46
46
47 try:
47 try:
48 ast.parse(content)
48 ast.parse(content)
49 except SyntaxError as e:
49 except SyntaxError as e:
50 print('%s: invalid syntax: %s' % (f, e))
50 print('%s: invalid syntax: %s' % (f, e))
51 return
51 return
52
52
53 # Try to import the module.
53 # Try to import the module.
54 # For now we only support mercurial.* and hgext.* modules because figuring
54 # For now we only support mercurial.* and hgext.* modules because figuring
55 # out module paths for things not in a package can be confusing.
55 # out module paths for things not in a package can be confusing.
56 if f.startswith(('hgext/', 'mercurial/')) and not f.endswith('__init__.py'):
56 if f.startswith(('hgext/', 'mercurial/')) and not f.endswith('__init__.py'):
57 assert f.endswith('.py')
57 assert f.endswith('.py')
58 name = f.replace('/', '.')[:-3]
58 name = f.replace('/', '.')[:-3].replace('.pure.', '.')
59 with open(f, 'r') as fh:
59 try:
60 try:
60 importlib.import_module(name)
61 imp.load_module(name, fh, '', ('py', 'r', imp.PY_SOURCE))
61 except Exception as e:
62 except Exception as e:
62 exc_type, exc_value, tb = sys.exc_info()
63 exc_type, exc_value, tb = sys.exc_info()
63 # We walk the stack and ignore frames from our custom importer,
64 # We walk the stack and ignore frames from our custom importer,
64 # import mechanisms, and stdlib modules. This kinda/sorta
65 # import mechanisms, and stdlib modules. This kinda/sorta
65 # emulates CPython behavior in import.c while also attempting
66 # emulates CPython behavior in import.c while also attempting
66 # to pin blame on a Mercurial file.
67 # to pin blame on a Mercurial file.
67 for frame in reversed(traceback.extract_tb(tb)):
68 for frame in reversed(traceback.extract_tb(tb)):
68 if frame.name == '_call_with_frames_removed':
69 if frame.name == '_call_with_frames_removed':
69 continue
70 continue
70 if 'importlib' in frame.filename:
71 if 'importlib' in frame.filename:
71 continue
72 continue
72 if 'mercurial/__init__.py' in frame.filename:
73 if 'mercurial/__init__.py' in frame.filename:
73 continue
74 continue
74 if frame.filename.startswith(sys.prefix):
75 if frame.filename.startswith(sys.prefix):
75 continue
76 continue
76 break
77 break
78
77
79 if frame.filename:
78 if frame.filename:
80 filename = os.path.basename(frame.filename)
79 filename = os.path.basename(frame.filename)
81 print('%s: error importing: <%s> %s (error at %s:%d)' % (
80 print('%s: error importing: <%s> %s (error at %s:%d)' % (
82 f, type(e).__name__, e, filename, frame.lineno))
81 f, type(e).__name__, e, filename, frame.lineno))
83 else:
82 else:
84 print('%s: error importing module: <%s> %s (line %d)' % (
83 print('%s: error importing module: <%s> %s (line %d)' % (
85 f, type(e).__name__, e, frame.lineno))
84 f, type(e).__name__, e, frame.lineno))
86
85
87 if __name__ == '__main__':
86 if __name__ == '__main__':
88 if sys.version_info[0] == 2:
87 if sys.version_info[0] == 2:
89 fn = check_compat_py2
88 fn = check_compat_py2
90 else:
89 else:
91 fn = check_compat_py3
90 fn = check_compat_py3
92
91
93 for f in sys.argv[1:]:
92 for f in sys.argv[1:]:
94 fn(f)
93 fn(f)
95
94
96 sys.exit(0)
95 sys.exit(0)
@@ -1,590 +1,585 b''
1 /*
1 /*
2 * A command server client that uses Unix domain socket
2 * A command server client that uses Unix domain socket
3 *
3 *
4 * Copyright (c) 2011 Yuya Nishihara <yuya@tcha.org>
4 * Copyright (c) 2011 Yuya Nishihara <yuya@tcha.org>
5 *
5 *
6 * This software may be used and distributed according to the terms of the
6 * This software may be used and distributed according to the terms of the
7 * GNU General Public License version 2 or any later version.
7 * GNU General Public License version 2 or any later version.
8 */
8 */
9
9
10 #include <arpa/inet.h> /* for ntohl(), htonl() */
10 #include <arpa/inet.h> /* for ntohl(), htonl() */
11 #include <assert.h>
11 #include <assert.h>
12 #include <ctype.h>
12 #include <ctype.h>
13 #include <errno.h>
13 #include <errno.h>
14 #include <fcntl.h>
14 #include <fcntl.h>
15 #include <signal.h>
15 #include <signal.h>
16 #include <stdint.h>
16 #include <stdint.h>
17 #include <stdio.h>
17 #include <stdio.h>
18 #include <stdlib.h>
18 #include <stdlib.h>
19 #include <string.h>
19 #include <string.h>
20 #include <sys/socket.h>
20 #include <sys/socket.h>
21 #include <sys/stat.h>
21 #include <sys/stat.h>
22 #include <sys/un.h>
22 #include <sys/un.h>
23 #include <unistd.h>
23 #include <unistd.h>
24
24
25 #include "hgclient.h"
25 #include "hgclient.h"
26 #include "util.h"
26 #include "util.h"
27
27
28 enum {
28 enum {
29 CAP_GETENCODING = 0x0001,
29 CAP_GETENCODING = 0x0001,
30 CAP_RUNCOMMAND = 0x0002,
30 CAP_RUNCOMMAND = 0x0002,
31 /* cHg extension: */
31 /* cHg extension: */
32 CAP_ATTACHIO = 0x0100,
32 CAP_ATTACHIO = 0x0100,
33 CAP_CHDIR = 0x0200,
33 CAP_CHDIR = 0x0200,
34 CAP_GETPAGER = 0x0400,
34 CAP_GETPAGER = 0x0400,
35 CAP_SETENV = 0x0800,
35 CAP_SETENV = 0x0800,
36 CAP_SETUMASK = 0x1000,
36 CAP_SETUMASK = 0x1000,
37 CAP_VALIDATE = 0x2000,
37 CAP_VALIDATE = 0x2000,
38 };
38 };
39
39
40 typedef struct {
40 typedef struct {
41 const char *name;
41 const char *name;
42 unsigned int flag;
42 unsigned int flag;
43 } cappair_t;
43 } cappair_t;
44
44
45 static const cappair_t captable[] = {
45 static const cappair_t captable[] = {
46 {"getencoding", CAP_GETENCODING},
46 {"getencoding", CAP_GETENCODING},
47 {"runcommand", CAP_RUNCOMMAND},
47 {"runcommand", CAP_RUNCOMMAND},
48 {"attachio", CAP_ATTACHIO},
48 {"attachio", CAP_ATTACHIO},
49 {"chdir", CAP_CHDIR},
49 {"chdir", CAP_CHDIR},
50 {"getpager", CAP_GETPAGER},
50 {"getpager", CAP_GETPAGER},
51 {"setenv", CAP_SETENV},
51 {"setenv", CAP_SETENV},
52 {"setumask", CAP_SETUMASK},
52 {"setumask", CAP_SETUMASK},
53 {"validate", CAP_VALIDATE},
53 {"validate", CAP_VALIDATE},
54 {NULL, 0}, /* terminator */
54 {NULL, 0}, /* terminator */
55 };
55 };
56
56
57 typedef struct {
57 typedef struct {
58 char ch;
58 char ch;
59 char *data;
59 char *data;
60 size_t maxdatasize;
60 size_t maxdatasize;
61 size_t datasize;
61 size_t datasize;
62 } context_t;
62 } context_t;
63
63
64 struct hgclient_tag_ {
64 struct hgclient_tag_ {
65 int sockfd;
65 int sockfd;
66 pid_t pgid;
66 pid_t pgid;
67 pid_t pid;
67 pid_t pid;
68 context_t ctx;
68 context_t ctx;
69 unsigned int capflags;
69 unsigned int capflags;
70 };
70 };
71
71
72 static const size_t defaultdatasize = 4096;
72 static const size_t defaultdatasize = 4096;
73
73
74 static void initcontext(context_t *ctx)
74 static void initcontext(context_t *ctx)
75 {
75 {
76 ctx->ch = '\0';
76 ctx->ch = '\0';
77 ctx->data = malloc(defaultdatasize);
77 ctx->data = malloc(defaultdatasize);
78 ctx->maxdatasize = (ctx->data) ? defaultdatasize : 0;
78 ctx->maxdatasize = (ctx->data) ? defaultdatasize : 0;
79 ctx->datasize = 0;
79 ctx->datasize = 0;
80 debugmsg("initialize context buffer with size %zu", ctx->maxdatasize);
80 debugmsg("initialize context buffer with size %zu", ctx->maxdatasize);
81 }
81 }
82
82
83 static void enlargecontext(context_t *ctx, size_t newsize)
83 static void enlargecontext(context_t *ctx, size_t newsize)
84 {
84 {
85 if (newsize <= ctx->maxdatasize)
85 if (newsize <= ctx->maxdatasize)
86 return;
86 return;
87
87
88 newsize = defaultdatasize
88 newsize = defaultdatasize
89 * ((newsize + defaultdatasize - 1) / defaultdatasize);
89 * ((newsize + defaultdatasize - 1) / defaultdatasize);
90 ctx->data = reallocx(ctx->data, newsize);
90 ctx->data = reallocx(ctx->data, newsize);
91 ctx->maxdatasize = newsize;
91 ctx->maxdatasize = newsize;
92 debugmsg("enlarge context buffer to %zu", ctx->maxdatasize);
92 debugmsg("enlarge context buffer to %zu", ctx->maxdatasize);
93 }
93 }
94
94
95 static void freecontext(context_t *ctx)
95 static void freecontext(context_t *ctx)
96 {
96 {
97 debugmsg("free context buffer");
97 debugmsg("free context buffer");
98 free(ctx->data);
98 free(ctx->data);
99 ctx->data = NULL;
99 ctx->data = NULL;
100 ctx->maxdatasize = 0;
100 ctx->maxdatasize = 0;
101 ctx->datasize = 0;
101 ctx->datasize = 0;
102 }
102 }
103
103
104 /* Read channeled response from cmdserver */
104 /* Read channeled response from cmdserver */
105 static void readchannel(hgclient_t *hgc)
105 static void readchannel(hgclient_t *hgc)
106 {
106 {
107 assert(hgc);
107 assert(hgc);
108
108
109 ssize_t rsize = recv(hgc->sockfd, &hgc->ctx.ch, sizeof(hgc->ctx.ch), 0);
109 ssize_t rsize = recv(hgc->sockfd, &hgc->ctx.ch, sizeof(hgc->ctx.ch), 0);
110 if (rsize != sizeof(hgc->ctx.ch)) {
110 if (rsize != sizeof(hgc->ctx.ch)) {
111 /* server would have exception and traceback would be printed */
111 /* server would have exception and traceback would be printed */
112 debugmsg("failed to read channel");
112 debugmsg("failed to read channel");
113 exit(255);
113 exit(255);
114 }
114 }
115
115
116 uint32_t datasize_n;
116 uint32_t datasize_n;
117 rsize = recv(hgc->sockfd, &datasize_n, sizeof(datasize_n), 0);
117 rsize = recv(hgc->sockfd, &datasize_n, sizeof(datasize_n), 0);
118 if (rsize != sizeof(datasize_n))
118 if (rsize != sizeof(datasize_n))
119 abortmsg("failed to read data size");
119 abortmsg("failed to read data size");
120
120
121 /* datasize denotes the maximum size to write if input request */
121 /* datasize denotes the maximum size to write if input request */
122 hgc->ctx.datasize = ntohl(datasize_n);
122 hgc->ctx.datasize = ntohl(datasize_n);
123 enlargecontext(&hgc->ctx, hgc->ctx.datasize);
123 enlargecontext(&hgc->ctx, hgc->ctx.datasize);
124
124
125 if (isupper(hgc->ctx.ch) && hgc->ctx.ch != 'S')
125 if (isupper(hgc->ctx.ch) && hgc->ctx.ch != 'S')
126 return; /* assumes input request */
126 return; /* assumes input request */
127
127
128 size_t cursize = 0;
128 size_t cursize = 0;
129 int emptycount = 0;
130 while (cursize < hgc->ctx.datasize) {
129 while (cursize < hgc->ctx.datasize) {
131 rsize = recv(hgc->sockfd, hgc->ctx.data + cursize,
130 rsize = recv(hgc->sockfd, hgc->ctx.data + cursize,
132 hgc->ctx.datasize - cursize, 0);
131 hgc->ctx.datasize - cursize, 0);
133 /* rsize == 0 normally indicates EOF, while it's also a valid
132 if (rsize < 1)
134 * packet size for unix socket. treat it as EOF and abort if
135 * we get many empty responses in a row. */
136 emptycount = (rsize == 0 ? emptycount + 1 : 0);
137 if (rsize < 0 || emptycount > 20)
138 abortmsg("failed to read data block");
133 abortmsg("failed to read data block");
139 cursize += rsize;
134 cursize += rsize;
140 }
135 }
141 }
136 }
142
137
143 static void sendall(int sockfd, const void *data, size_t datasize)
138 static void sendall(int sockfd, const void *data, size_t datasize)
144 {
139 {
145 const char *p = data;
140 const char *p = data;
146 const char *const endp = p + datasize;
141 const char *const endp = p + datasize;
147 while (p < endp) {
142 while (p < endp) {
148 ssize_t r = send(sockfd, p, endp - p, 0);
143 ssize_t r = send(sockfd, p, endp - p, 0);
149 if (r < 0)
144 if (r < 0)
150 abortmsgerrno("cannot communicate");
145 abortmsgerrno("cannot communicate");
151 p += r;
146 p += r;
152 }
147 }
153 }
148 }
154
149
155 /* Write lengh-data block to cmdserver */
150 /* Write lengh-data block to cmdserver */
156 static void writeblock(const hgclient_t *hgc)
151 static void writeblock(const hgclient_t *hgc)
157 {
152 {
158 assert(hgc);
153 assert(hgc);
159
154
160 const uint32_t datasize_n = htonl(hgc->ctx.datasize);
155 const uint32_t datasize_n = htonl(hgc->ctx.datasize);
161 sendall(hgc->sockfd, &datasize_n, sizeof(datasize_n));
156 sendall(hgc->sockfd, &datasize_n, sizeof(datasize_n));
162
157
163 sendall(hgc->sockfd, hgc->ctx.data, hgc->ctx.datasize);
158 sendall(hgc->sockfd, hgc->ctx.data, hgc->ctx.datasize);
164 }
159 }
165
160
166 static void writeblockrequest(const hgclient_t *hgc, const char *chcmd)
161 static void writeblockrequest(const hgclient_t *hgc, const char *chcmd)
167 {
162 {
168 debugmsg("request %s, block size %zu", chcmd, hgc->ctx.datasize);
163 debugmsg("request %s, block size %zu", chcmd, hgc->ctx.datasize);
169
164
170 char buf[strlen(chcmd) + 1];
165 char buf[strlen(chcmd) + 1];
171 memcpy(buf, chcmd, sizeof(buf) - 1);
166 memcpy(buf, chcmd, sizeof(buf) - 1);
172 buf[sizeof(buf) - 1] = '\n';
167 buf[sizeof(buf) - 1] = '\n';
173 sendall(hgc->sockfd, buf, sizeof(buf));
168 sendall(hgc->sockfd, buf, sizeof(buf));
174
169
175 writeblock(hgc);
170 writeblock(hgc);
176 }
171 }
177
172
178 /* Build '\0'-separated list of args. argsize < 0 denotes that args are
173 /* Build '\0'-separated list of args. argsize < 0 denotes that args are
179 * terminated by NULL. */
174 * terminated by NULL. */
180 static void packcmdargs(context_t *ctx, const char *const args[],
175 static void packcmdargs(context_t *ctx, const char *const args[],
181 ssize_t argsize)
176 ssize_t argsize)
182 {
177 {
183 ctx->datasize = 0;
178 ctx->datasize = 0;
184 const char *const *const end = (argsize >= 0) ? args + argsize : NULL;
179 const char *const *const end = (argsize >= 0) ? args + argsize : NULL;
185 for (const char *const *it = args; it != end && *it; ++it) {
180 for (const char *const *it = args; it != end && *it; ++it) {
186 const size_t n = strlen(*it) + 1; /* include '\0' */
181 const size_t n = strlen(*it) + 1; /* include '\0' */
187 enlargecontext(ctx, ctx->datasize + n);
182 enlargecontext(ctx, ctx->datasize + n);
188 memcpy(ctx->data + ctx->datasize, *it, n);
183 memcpy(ctx->data + ctx->datasize, *it, n);
189 ctx->datasize += n;
184 ctx->datasize += n;
190 }
185 }
191
186
192 if (ctx->datasize > 0)
187 if (ctx->datasize > 0)
193 --ctx->datasize; /* strip last '\0' */
188 --ctx->datasize; /* strip last '\0' */
194 }
189 }
195
190
196 /* Extract '\0'-separated list of args to new buffer, terminated by NULL */
191 /* Extract '\0'-separated list of args to new buffer, terminated by NULL */
197 static const char **unpackcmdargsnul(const context_t *ctx)
192 static const char **unpackcmdargsnul(const context_t *ctx)
198 {
193 {
199 const char **args = NULL;
194 const char **args = NULL;
200 size_t nargs = 0, maxnargs = 0;
195 size_t nargs = 0, maxnargs = 0;
201 const char *s = ctx->data;
196 const char *s = ctx->data;
202 const char *e = ctx->data + ctx->datasize;
197 const char *e = ctx->data + ctx->datasize;
203 for (;;) {
198 for (;;) {
204 if (nargs + 1 >= maxnargs) { /* including last NULL */
199 if (nargs + 1 >= maxnargs) { /* including last NULL */
205 maxnargs += 256;
200 maxnargs += 256;
206 args = reallocx(args, maxnargs * sizeof(args[0]));
201 args = reallocx(args, maxnargs * sizeof(args[0]));
207 }
202 }
208 args[nargs] = s;
203 args[nargs] = s;
209 nargs++;
204 nargs++;
210 s = memchr(s, '\0', e - s);
205 s = memchr(s, '\0', e - s);
211 if (!s)
206 if (!s)
212 break;
207 break;
213 s++;
208 s++;
214 }
209 }
215 args[nargs] = NULL;
210 args[nargs] = NULL;
216 return args;
211 return args;
217 }
212 }
218
213
219 static void handlereadrequest(hgclient_t *hgc)
214 static void handlereadrequest(hgclient_t *hgc)
220 {
215 {
221 context_t *ctx = &hgc->ctx;
216 context_t *ctx = &hgc->ctx;
222 size_t r = fread(ctx->data, sizeof(ctx->data[0]), ctx->datasize, stdin);
217 size_t r = fread(ctx->data, sizeof(ctx->data[0]), ctx->datasize, stdin);
223 ctx->datasize = r;
218 ctx->datasize = r;
224 writeblock(hgc);
219 writeblock(hgc);
225 }
220 }
226
221
227 /* Read single-line */
222 /* Read single-line */
228 static void handlereadlinerequest(hgclient_t *hgc)
223 static void handlereadlinerequest(hgclient_t *hgc)
229 {
224 {
230 context_t *ctx = &hgc->ctx;
225 context_t *ctx = &hgc->ctx;
231 if (!fgets(ctx->data, ctx->datasize, stdin))
226 if (!fgets(ctx->data, ctx->datasize, stdin))
232 ctx->data[0] = '\0';
227 ctx->data[0] = '\0';
233 ctx->datasize = strlen(ctx->data);
228 ctx->datasize = strlen(ctx->data);
234 writeblock(hgc);
229 writeblock(hgc);
235 }
230 }
236
231
237 /* Execute the requested command and write exit code */
232 /* Execute the requested command and write exit code */
238 static void handlesystemrequest(hgclient_t *hgc)
233 static void handlesystemrequest(hgclient_t *hgc)
239 {
234 {
240 context_t *ctx = &hgc->ctx;
235 context_t *ctx = &hgc->ctx;
241 enlargecontext(ctx, ctx->datasize + 1);
236 enlargecontext(ctx, ctx->datasize + 1);
242 ctx->data[ctx->datasize] = '\0'; /* terminate last string */
237 ctx->data[ctx->datasize] = '\0'; /* terminate last string */
243
238
244 const char **args = unpackcmdargsnul(ctx);
239 const char **args = unpackcmdargsnul(ctx);
245 if (!args[0] || !args[1])
240 if (!args[0] || !args[1])
246 abortmsg("missing command or cwd in system request");
241 abortmsg("missing command or cwd in system request");
247 debugmsg("run '%s' at '%s'", args[0], args[1]);
242 debugmsg("run '%s' at '%s'", args[0], args[1]);
248 int32_t r = runshellcmd(args[0], args + 2, args[1]);
243 int32_t r = runshellcmd(args[0], args + 2, args[1]);
249 free(args);
244 free(args);
250
245
251 uint32_t r_n = htonl(r);
246 uint32_t r_n = htonl(r);
252 memcpy(ctx->data, &r_n, sizeof(r_n));
247 memcpy(ctx->data, &r_n, sizeof(r_n));
253 ctx->datasize = sizeof(r_n);
248 ctx->datasize = sizeof(r_n);
254 writeblock(hgc);
249 writeblock(hgc);
255 }
250 }
256
251
257 /* Read response of command execution until receiving 'r'-esult */
252 /* Read response of command execution until receiving 'r'-esult */
258 static void handleresponse(hgclient_t *hgc)
253 static void handleresponse(hgclient_t *hgc)
259 {
254 {
260 for (;;) {
255 for (;;) {
261 readchannel(hgc);
256 readchannel(hgc);
262 context_t *ctx = &hgc->ctx;
257 context_t *ctx = &hgc->ctx;
263 debugmsg("response read from channel %c, size %zu",
258 debugmsg("response read from channel %c, size %zu",
264 ctx->ch, ctx->datasize);
259 ctx->ch, ctx->datasize);
265 switch (ctx->ch) {
260 switch (ctx->ch) {
266 case 'o':
261 case 'o':
267 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
262 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
268 stdout);
263 stdout);
269 break;
264 break;
270 case 'e':
265 case 'e':
271 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
266 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
272 stderr);
267 stderr);
273 break;
268 break;
274 case 'd':
269 case 'd':
275 /* assumes last char is '\n' */
270 /* assumes last char is '\n' */
276 ctx->data[ctx->datasize - 1] = '\0';
271 ctx->data[ctx->datasize - 1] = '\0';
277 debugmsg("server: %s", ctx->data);
272 debugmsg("server: %s", ctx->data);
278 break;
273 break;
279 case 'r':
274 case 'r':
280 return;
275 return;
281 case 'I':
276 case 'I':
282 handlereadrequest(hgc);
277 handlereadrequest(hgc);
283 break;
278 break;
284 case 'L':
279 case 'L':
285 handlereadlinerequest(hgc);
280 handlereadlinerequest(hgc);
286 break;
281 break;
287 case 'S':
282 case 'S':
288 handlesystemrequest(hgc);
283 handlesystemrequest(hgc);
289 break;
284 break;
290 default:
285 default:
291 if (isupper(ctx->ch))
286 if (isupper(ctx->ch))
292 abortmsg("cannot handle response (ch = %c)",
287 abortmsg("cannot handle response (ch = %c)",
293 ctx->ch);
288 ctx->ch);
294 }
289 }
295 }
290 }
296 }
291 }
297
292
298 static unsigned int parsecapabilities(const char *s, const char *e)
293 static unsigned int parsecapabilities(const char *s, const char *e)
299 {
294 {
300 unsigned int flags = 0;
295 unsigned int flags = 0;
301 while (s < e) {
296 while (s < e) {
302 const char *t = strchr(s, ' ');
297 const char *t = strchr(s, ' ');
303 if (!t || t > e)
298 if (!t || t > e)
304 t = e;
299 t = e;
305 const cappair_t *cap;
300 const cappair_t *cap;
306 for (cap = captable; cap->flag; ++cap) {
301 for (cap = captable; cap->flag; ++cap) {
307 size_t n = t - s;
302 size_t n = t - s;
308 if (strncmp(s, cap->name, n) == 0 &&
303 if (strncmp(s, cap->name, n) == 0 &&
309 strlen(cap->name) == n) {
304 strlen(cap->name) == n) {
310 flags |= cap->flag;
305 flags |= cap->flag;
311 break;
306 break;
312 }
307 }
313 }
308 }
314 s = t + 1;
309 s = t + 1;
315 }
310 }
316 return flags;
311 return flags;
317 }
312 }
318
313
319 static void readhello(hgclient_t *hgc)
314 static void readhello(hgclient_t *hgc)
320 {
315 {
321 readchannel(hgc);
316 readchannel(hgc);
322 context_t *ctx = &hgc->ctx;
317 context_t *ctx = &hgc->ctx;
323 if (ctx->ch != 'o') {
318 if (ctx->ch != 'o') {
324 char ch = ctx->ch;
319 char ch = ctx->ch;
325 if (ch == 'e') {
320 if (ch == 'e') {
326 /* write early error and will exit */
321 /* write early error and will exit */
327 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
322 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
328 stderr);
323 stderr);
329 handleresponse(hgc);
324 handleresponse(hgc);
330 }
325 }
331 abortmsg("unexpected channel of hello message (ch = %c)", ch);
326 abortmsg("unexpected channel of hello message (ch = %c)", ch);
332 }
327 }
333 enlargecontext(ctx, ctx->datasize + 1);
328 enlargecontext(ctx, ctx->datasize + 1);
334 ctx->data[ctx->datasize] = '\0';
329 ctx->data[ctx->datasize] = '\0';
335 debugmsg("hello received: %s (size = %zu)", ctx->data, ctx->datasize);
330 debugmsg("hello received: %s (size = %zu)", ctx->data, ctx->datasize);
336
331
337 const char *s = ctx->data;
332 const char *s = ctx->data;
338 const char *const dataend = ctx->data + ctx->datasize;
333 const char *const dataend = ctx->data + ctx->datasize;
339 while (s < dataend) {
334 while (s < dataend) {
340 const char *t = strchr(s, ':');
335 const char *t = strchr(s, ':');
341 if (!t || t[1] != ' ')
336 if (!t || t[1] != ' ')
342 break;
337 break;
343 const char *u = strchr(t + 2, '\n');
338 const char *u = strchr(t + 2, '\n');
344 if (!u)
339 if (!u)
345 u = dataend;
340 u = dataend;
346 if (strncmp(s, "capabilities:", t - s + 1) == 0) {
341 if (strncmp(s, "capabilities:", t - s + 1) == 0) {
347 hgc->capflags = parsecapabilities(t + 2, u);
342 hgc->capflags = parsecapabilities(t + 2, u);
348 } else if (strncmp(s, "pgid:", t - s + 1) == 0) {
343 } else if (strncmp(s, "pgid:", t - s + 1) == 0) {
349 hgc->pgid = strtol(t + 2, NULL, 10);
344 hgc->pgid = strtol(t + 2, NULL, 10);
350 } else if (strncmp(s, "pid:", t - s + 1) == 0) {
345 } else if (strncmp(s, "pid:", t - s + 1) == 0) {
351 hgc->pid = strtol(t + 2, NULL, 10);
346 hgc->pid = strtol(t + 2, NULL, 10);
352 }
347 }
353 s = u + 1;
348 s = u + 1;
354 }
349 }
355 debugmsg("capflags=0x%04x, pid=%d", hgc->capflags, hgc->pid);
350 debugmsg("capflags=0x%04x, pid=%d", hgc->capflags, hgc->pid);
356 }
351 }
357
352
358 static void attachio(hgclient_t *hgc)
353 static void attachio(hgclient_t *hgc)
359 {
354 {
360 debugmsg("request attachio");
355 debugmsg("request attachio");
361 static const char chcmd[] = "attachio\n";
356 static const char chcmd[] = "attachio\n";
362 sendall(hgc->sockfd, chcmd, sizeof(chcmd) - 1);
357 sendall(hgc->sockfd, chcmd, sizeof(chcmd) - 1);
363 readchannel(hgc);
358 readchannel(hgc);
364 context_t *ctx = &hgc->ctx;
359 context_t *ctx = &hgc->ctx;
365 if (ctx->ch != 'I')
360 if (ctx->ch != 'I')
366 abortmsg("unexpected response for attachio (ch = %c)", ctx->ch);
361 abortmsg("unexpected response for attachio (ch = %c)", ctx->ch);
367
362
368 static const int fds[3] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
363 static const int fds[3] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
369 struct msghdr msgh;
364 struct msghdr msgh;
370 memset(&msgh, 0, sizeof(msgh));
365 memset(&msgh, 0, sizeof(msgh));
371 struct iovec iov = {ctx->data, ctx->datasize}; /* dummy payload */
366 struct iovec iov = {ctx->data, ctx->datasize}; /* dummy payload */
372 msgh.msg_iov = &iov;
367 msgh.msg_iov = &iov;
373 msgh.msg_iovlen = 1;
368 msgh.msg_iovlen = 1;
374 char fdbuf[CMSG_SPACE(sizeof(fds))];
369 char fdbuf[CMSG_SPACE(sizeof(fds))];
375 msgh.msg_control = fdbuf;
370 msgh.msg_control = fdbuf;
376 msgh.msg_controllen = sizeof(fdbuf);
371 msgh.msg_controllen = sizeof(fdbuf);
377 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh);
372 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh);
378 cmsg->cmsg_level = SOL_SOCKET;
373 cmsg->cmsg_level = SOL_SOCKET;
379 cmsg->cmsg_type = SCM_RIGHTS;
374 cmsg->cmsg_type = SCM_RIGHTS;
380 cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
375 cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
381 memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
376 memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
382 msgh.msg_controllen = cmsg->cmsg_len;
377 msgh.msg_controllen = cmsg->cmsg_len;
383 ssize_t r = sendmsg(hgc->sockfd, &msgh, 0);
378 ssize_t r = sendmsg(hgc->sockfd, &msgh, 0);
384 if (r < 0)
379 if (r < 0)
385 abortmsgerrno("sendmsg failed");
380 abortmsgerrno("sendmsg failed");
386
381
387 handleresponse(hgc);
382 handleresponse(hgc);
388 int32_t n;
383 int32_t n;
389 if (ctx->datasize != sizeof(n))
384 if (ctx->datasize != sizeof(n))
390 abortmsg("unexpected size of attachio result");
385 abortmsg("unexpected size of attachio result");
391 memcpy(&n, ctx->data, sizeof(n));
386 memcpy(&n, ctx->data, sizeof(n));
392 n = ntohl(n);
387 n = ntohl(n);
393 if (n != sizeof(fds) / sizeof(fds[0]))
388 if (n != sizeof(fds) / sizeof(fds[0]))
394 abortmsg("failed to send fds (n = %d)", n);
389 abortmsg("failed to send fds (n = %d)", n);
395 }
390 }
396
391
397 static void chdirtocwd(hgclient_t *hgc)
392 static void chdirtocwd(hgclient_t *hgc)
398 {
393 {
399 if (!getcwd(hgc->ctx.data, hgc->ctx.maxdatasize))
394 if (!getcwd(hgc->ctx.data, hgc->ctx.maxdatasize))
400 abortmsgerrno("failed to getcwd");
395 abortmsgerrno("failed to getcwd");
401 hgc->ctx.datasize = strlen(hgc->ctx.data);
396 hgc->ctx.datasize = strlen(hgc->ctx.data);
402 writeblockrequest(hgc, "chdir");
397 writeblockrequest(hgc, "chdir");
403 }
398 }
404
399
405 static void forwardumask(hgclient_t *hgc)
400 static void forwardumask(hgclient_t *hgc)
406 {
401 {
407 mode_t mask = umask(0);
402 mode_t mask = umask(0);
408 umask(mask);
403 umask(mask);
409
404
410 static const char command[] = "setumask\n";
405 static const char command[] = "setumask\n";
411 sendall(hgc->sockfd, command, sizeof(command) - 1);
406 sendall(hgc->sockfd, command, sizeof(command) - 1);
412 uint32_t data = htonl(mask);
407 uint32_t data = htonl(mask);
413 sendall(hgc->sockfd, &data, sizeof(data));
408 sendall(hgc->sockfd, &data, sizeof(data));
414 }
409 }
415
410
416 /*!
411 /*!
417 * Open connection to per-user cmdserver
412 * Open connection to per-user cmdserver
418 *
413 *
419 * If no background server running, returns NULL.
414 * If no background server running, returns NULL.
420 */
415 */
421 hgclient_t *hgc_open(const char *sockname)
416 hgclient_t *hgc_open(const char *sockname)
422 {
417 {
423 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
418 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
424 if (fd < 0)
419 if (fd < 0)
425 abortmsgerrno("cannot create socket");
420 abortmsgerrno("cannot create socket");
426
421
427 /* don't keep fd on fork(), so that it can be closed when the parent
422 /* don't keep fd on fork(), so that it can be closed when the parent
428 * process get terminated. */
423 * process get terminated. */
429 fsetcloexec(fd);
424 fsetcloexec(fd);
430
425
431 struct sockaddr_un addr;
426 struct sockaddr_un addr;
432 addr.sun_family = AF_UNIX;
427 addr.sun_family = AF_UNIX;
433 strncpy(addr.sun_path, sockname, sizeof(addr.sun_path));
428 strncpy(addr.sun_path, sockname, sizeof(addr.sun_path));
434 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
429 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
435
430
436 int r = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
431 int r = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
437 if (r < 0) {
432 if (r < 0) {
438 close(fd);
433 close(fd);
439 if (errno == ENOENT || errno == ECONNREFUSED)
434 if (errno == ENOENT || errno == ECONNREFUSED)
440 return NULL;
435 return NULL;
441 abortmsgerrno("cannot connect to %s", addr.sun_path);
436 abortmsgerrno("cannot connect to %s", addr.sun_path);
442 }
437 }
443 debugmsg("connected to %s", addr.sun_path);
438 debugmsg("connected to %s", addr.sun_path);
444
439
445 hgclient_t *hgc = mallocx(sizeof(hgclient_t));
440 hgclient_t *hgc = mallocx(sizeof(hgclient_t));
446 memset(hgc, 0, sizeof(*hgc));
441 memset(hgc, 0, sizeof(*hgc));
447 hgc->sockfd = fd;
442 hgc->sockfd = fd;
448 initcontext(&hgc->ctx);
443 initcontext(&hgc->ctx);
449
444
450 readhello(hgc);
445 readhello(hgc);
451 if (!(hgc->capflags & CAP_RUNCOMMAND))
446 if (!(hgc->capflags & CAP_RUNCOMMAND))
452 abortmsg("insufficient capability: runcommand");
447 abortmsg("insufficient capability: runcommand");
453 if (hgc->capflags & CAP_ATTACHIO)
448 if (hgc->capflags & CAP_ATTACHIO)
454 attachio(hgc);
449 attachio(hgc);
455 if (hgc->capflags & CAP_CHDIR)
450 if (hgc->capflags & CAP_CHDIR)
456 chdirtocwd(hgc);
451 chdirtocwd(hgc);
457 if (hgc->capflags & CAP_SETUMASK)
452 if (hgc->capflags & CAP_SETUMASK)
458 forwardumask(hgc);
453 forwardumask(hgc);
459
454
460 return hgc;
455 return hgc;
461 }
456 }
462
457
463 /*!
458 /*!
464 * Close connection and free allocated memory
459 * Close connection and free allocated memory
465 */
460 */
466 void hgc_close(hgclient_t *hgc)
461 void hgc_close(hgclient_t *hgc)
467 {
462 {
468 assert(hgc);
463 assert(hgc);
469 freecontext(&hgc->ctx);
464 freecontext(&hgc->ctx);
470 close(hgc->sockfd);
465 close(hgc->sockfd);
471 free(hgc);
466 free(hgc);
472 }
467 }
473
468
474 pid_t hgc_peerpgid(const hgclient_t *hgc)
469 pid_t hgc_peerpgid(const hgclient_t *hgc)
475 {
470 {
476 assert(hgc);
471 assert(hgc);
477 return hgc->pgid;
472 return hgc->pgid;
478 }
473 }
479
474
480 pid_t hgc_peerpid(const hgclient_t *hgc)
475 pid_t hgc_peerpid(const hgclient_t *hgc)
481 {
476 {
482 assert(hgc);
477 assert(hgc);
483 return hgc->pid;
478 return hgc->pid;
484 }
479 }
485
480
486 /*!
481 /*!
487 * Send command line arguments to let the server load the repo config and check
482 * Send command line arguments to let the server load the repo config and check
488 * whether it can process our request directly or not.
483 * whether it can process our request directly or not.
489 * Make sure hgc_setenv is called before calling this.
484 * Make sure hgc_setenv is called before calling this.
490 *
485 *
491 * @return - NULL, the server believes it can handle our request, or does not
486 * @return - NULL, the server believes it can handle our request, or does not
492 * support "validate" command.
487 * support "validate" command.
493 * - a list of strings, the server probably cannot handle our request
488 * - a list of strings, the server probably cannot handle our request
494 * and it sent instructions telling us what to do next. See
489 * and it sent instructions telling us what to do next. See
495 * chgserver.py for possible instruction formats.
490 * chgserver.py for possible instruction formats.
496 * the list should be freed by the caller.
491 * the list should be freed by the caller.
497 * the last string is guaranteed to be NULL.
492 * the last string is guaranteed to be NULL.
498 */
493 */
499 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
494 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
500 size_t argsize)
495 size_t argsize)
501 {
496 {
502 assert(hgc);
497 assert(hgc);
503 if (!(hgc->capflags & CAP_VALIDATE))
498 if (!(hgc->capflags & CAP_VALIDATE))
504 return NULL;
499 return NULL;
505
500
506 packcmdargs(&hgc->ctx, args, argsize);
501 packcmdargs(&hgc->ctx, args, argsize);
507 writeblockrequest(hgc, "validate");
502 writeblockrequest(hgc, "validate");
508 handleresponse(hgc);
503 handleresponse(hgc);
509
504
510 /* the server returns '\0' if it can handle our request */
505 /* the server returns '\0' if it can handle our request */
511 if (hgc->ctx.datasize <= 1)
506 if (hgc->ctx.datasize <= 1)
512 return NULL;
507 return NULL;
513
508
514 /* make sure the buffer is '\0' terminated */
509 /* make sure the buffer is '\0' terminated */
515 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
510 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
516 hgc->ctx.data[hgc->ctx.datasize] = '\0';
511 hgc->ctx.data[hgc->ctx.datasize] = '\0';
517 return unpackcmdargsnul(&hgc->ctx);
512 return unpackcmdargsnul(&hgc->ctx);
518 }
513 }
519
514
520 /*!
515 /*!
521 * Execute the specified Mercurial command
516 * Execute the specified Mercurial command
522 *
517 *
523 * @return result code
518 * @return result code
524 */
519 */
525 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize)
520 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize)
526 {
521 {
527 assert(hgc);
522 assert(hgc);
528
523
529 packcmdargs(&hgc->ctx, args, argsize);
524 packcmdargs(&hgc->ctx, args, argsize);
530 writeblockrequest(hgc, "runcommand");
525 writeblockrequest(hgc, "runcommand");
531 handleresponse(hgc);
526 handleresponse(hgc);
532
527
533 int32_t exitcode_n;
528 int32_t exitcode_n;
534 if (hgc->ctx.datasize != sizeof(exitcode_n)) {
529 if (hgc->ctx.datasize != sizeof(exitcode_n)) {
535 abortmsg("unexpected size of exitcode");
530 abortmsg("unexpected size of exitcode");
536 }
531 }
537 memcpy(&exitcode_n, hgc->ctx.data, sizeof(exitcode_n));
532 memcpy(&exitcode_n, hgc->ctx.data, sizeof(exitcode_n));
538 return ntohl(exitcode_n);
533 return ntohl(exitcode_n);
539 }
534 }
540
535
541 /*!
536 /*!
542 * (Re-)send client's stdio channels so that the server can access to tty
537 * (Re-)send client's stdio channels so that the server can access to tty
543 */
538 */
544 void hgc_attachio(hgclient_t *hgc)
539 void hgc_attachio(hgclient_t *hgc)
545 {
540 {
546 assert(hgc);
541 assert(hgc);
547 if (!(hgc->capflags & CAP_ATTACHIO))
542 if (!(hgc->capflags & CAP_ATTACHIO))
548 return;
543 return;
549 attachio(hgc);
544 attachio(hgc);
550 }
545 }
551
546
552 /*!
547 /*!
553 * Get pager command for the given Mercurial command args
548 * Get pager command for the given Mercurial command args
554 *
549 *
555 * If no pager enabled, returns NULL. The return value becomes invalid
550 * If no pager enabled, returns NULL. The return value becomes invalid
556 * once you run another request to hgc.
551 * once you run another request to hgc.
557 */
552 */
558 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
553 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
559 size_t argsize)
554 size_t argsize)
560 {
555 {
561 assert(hgc);
556 assert(hgc);
562
557
563 if (!(hgc->capflags & CAP_GETPAGER))
558 if (!(hgc->capflags & CAP_GETPAGER))
564 return NULL;
559 return NULL;
565
560
566 packcmdargs(&hgc->ctx, args, argsize);
561 packcmdargs(&hgc->ctx, args, argsize);
567 writeblockrequest(hgc, "getpager");
562 writeblockrequest(hgc, "getpager");
568 handleresponse(hgc);
563 handleresponse(hgc);
569
564
570 if (hgc->ctx.datasize < 1 || hgc->ctx.data[0] == '\0')
565 if (hgc->ctx.datasize < 1 || hgc->ctx.data[0] == '\0')
571 return NULL;
566 return NULL;
572 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
567 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
573 hgc->ctx.data[hgc->ctx.datasize] = '\0';
568 hgc->ctx.data[hgc->ctx.datasize] = '\0';
574 return hgc->ctx.data;
569 return hgc->ctx.data;
575 }
570 }
576
571
577 /*!
572 /*!
578 * Update server's environment variables
573 * Update server's environment variables
579 *
574 *
580 * @param envp list of environment variables in "NAME=VALUE" format,
575 * @param envp list of environment variables in "NAME=VALUE" format,
581 * terminated by NULL.
576 * terminated by NULL.
582 */
577 */
583 void hgc_setenv(hgclient_t *hgc, const char *const envp[])
578 void hgc_setenv(hgclient_t *hgc, const char *const envp[])
584 {
579 {
585 assert(hgc && envp);
580 assert(hgc && envp);
586 if (!(hgc->capflags & CAP_SETENV))
581 if (!(hgc->capflags & CAP_SETENV))
587 return;
582 return;
588 packcmdargs(&hgc->ctx, envp, /*argsize*/ -1);
583 packcmdargs(&hgc->ctx, envp, /*argsize*/ -1);
589 writeblockrequest(hgc, "setenv");
584 writeblockrequest(hgc, "setenv");
590 }
585 }
@@ -1,902 +1,1103 b''
1 # perf.py - performance test routines
1 # perf.py - performance test routines
2 '''helper extension to measure performance'''
2 '''helper extension to measure performance'''
3
3
4 # "historical portability" policy of perf.py:
4 # "historical portability" policy of perf.py:
5 #
5 #
6 # We have to do:
6 # We have to do:
7 # - make perf.py "loadable" with as wide Mercurial version as possible
7 # - make perf.py "loadable" with as wide Mercurial version as possible
8 # This doesn't mean that perf commands work correctly with that Mercurial.
8 # This doesn't mean that perf commands work correctly with that Mercurial.
9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
10 # - make historical perf command work correctly with as wide Mercurial
10 # - make historical perf command work correctly with as wide Mercurial
11 # version as possible
11 # version as possible
12 #
12 #
13 # We have to do, if possible with reasonable cost:
13 # We have to do, if possible with reasonable cost:
14 # - make recent perf command for historical feature work correctly
14 # - make recent perf command for historical feature work correctly
15 # with early Mercurial
15 # with early Mercurial
16 #
16 #
17 # We don't have to do:
17 # We don't have to do:
18 # - make perf command for recent feature work correctly with early
18 # - make perf command for recent feature work correctly with early
19 # Mercurial
19 # Mercurial
20
20
21 from __future__ import absolute_import
21 from __future__ import absolute_import
22 import functools
22 import functools
23 import os
23 import os
24 import random
24 import random
25 import sys
25 import sys
26 import time
26 import time
27 from mercurial import (
27 from mercurial import (
28 changegroup,
28 cmdutil,
29 cmdutil,
29 commands,
30 commands,
30 copies,
31 copies,
31 error,
32 error,
32 extensions,
33 extensions,
33 mdiff,
34 mdiff,
34 merge,
35 merge,
35 revlog,
36 revlog,
36 util,
37 util,
37 )
38 )
38
39
39 # for "historical portability":
40 # for "historical portability":
40 # try to import modules separately (in dict order), and ignore
41 # try to import modules separately (in dict order), and ignore
41 # failure, because these aren't available with early Mercurial
42 # failure, because these aren't available with early Mercurial
42 try:
43 try:
43 from mercurial import branchmap # since 2.5 (or bcee63733aad)
44 from mercurial import branchmap # since 2.5 (or bcee63733aad)
44 except ImportError:
45 except ImportError:
45 pass
46 pass
46 try:
47 try:
47 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
48 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
48 except ImportError:
49 except ImportError:
49 pass
50 pass
50 try:
51 try:
51 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
52 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
52 except ImportError:
53 except ImportError:
53 pass
54 pass
54 try:
55 try:
55 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
56 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
56 except ImportError:
57 except ImportError:
57 pass
58 pass
58
59
59 # for "historical portability":
60 # for "historical portability":
60 # define util.safehasattr forcibly, because util.safehasattr has been
61 # define util.safehasattr forcibly, because util.safehasattr has been
61 # available since 1.9.3 (or 94b200a11cf7)
62 # available since 1.9.3 (or 94b200a11cf7)
62 _undefined = object()
63 _undefined = object()
63 def safehasattr(thing, attr):
64 def safehasattr(thing, attr):
64 return getattr(thing, attr, _undefined) is not _undefined
65 return getattr(thing, attr, _undefined) is not _undefined
65 setattr(util, 'safehasattr', safehasattr)
66 setattr(util, 'safehasattr', safehasattr)
66
67
67 # for "historical portability":
68 # for "historical portability":
68 # use locally defined empty option list, if formatteropts isn't
69 # use locally defined empty option list, if formatteropts isn't
69 # available, because commands.formatteropts has been available since
70 # available, because commands.formatteropts has been available since
70 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
71 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
71 # available since 2.2 (or ae5f92e154d3)
72 # available since 2.2 (or ae5f92e154d3)
72 formatteropts = getattr(commands, "formatteropts", [])
73 formatteropts = getattr(commands, "formatteropts", [])
73
74
74 # for "historical portability":
75 # for "historical portability":
75 # use locally defined option list, if debugrevlogopts isn't available,
76 # use locally defined option list, if debugrevlogopts isn't available,
76 # because commands.debugrevlogopts has been available since 3.7 (or
77 # because commands.debugrevlogopts has been available since 3.7 (or
77 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
78 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
78 # since 1.9 (or a79fea6b3e77).
79 # since 1.9 (or a79fea6b3e77).
79 revlogopts = getattr(commands, "debugrevlogopts", [
80 revlogopts = getattr(commands, "debugrevlogopts", [
80 ('c', 'changelog', False, ('open changelog')),
81 ('c', 'changelog', False, ('open changelog')),
81 ('m', 'manifest', False, ('open manifest')),
82 ('m', 'manifest', False, ('open manifest')),
82 ('', 'dir', False, ('open directory manifest')),
83 ('', 'dir', False, ('open directory manifest')),
83 ])
84 ])
84
85
85 cmdtable = {}
86 cmdtable = {}
86
87
87 # for "historical portability":
88 # for "historical portability":
88 # define parsealiases locally, because cmdutil.parsealiases has been
89 # define parsealiases locally, because cmdutil.parsealiases has been
89 # available since 1.5 (or 6252852b4332)
90 # available since 1.5 (or 6252852b4332)
90 def parsealiases(cmd):
91 def parsealiases(cmd):
91 return cmd.lstrip("^").split("|")
92 return cmd.lstrip("^").split("|")
92
93
93 if safehasattr(cmdutil, 'command'):
94 if safehasattr(cmdutil, 'command'):
94 import inspect
95 import inspect
95 command = cmdutil.command(cmdtable)
96 command = cmdutil.command(cmdtable)
96 if 'norepo' not in inspect.getargspec(command)[0]:
97 if 'norepo' not in inspect.getargspec(command)[0]:
97 # for "historical portability":
98 # for "historical portability":
98 # wrap original cmdutil.command, because "norepo" option has
99 # wrap original cmdutil.command, because "norepo" option has
99 # been available since 3.1 (or 75a96326cecb)
100 # been available since 3.1 (or 75a96326cecb)
100 _command = command
101 _command = command
101 def command(name, options=(), synopsis=None, norepo=False):
102 def command(name, options=(), synopsis=None, norepo=False):
102 if norepo:
103 if norepo:
103 commands.norepo += ' %s' % ' '.join(parsealiases(name))
104 commands.norepo += ' %s' % ' '.join(parsealiases(name))
104 return _command(name, list(options), synopsis)
105 return _command(name, list(options), synopsis)
105 else:
106 else:
106 # for "historical portability":
107 # for "historical portability":
107 # define "@command" annotation locally, because cmdutil.command
108 # define "@command" annotation locally, because cmdutil.command
108 # has been available since 1.9 (or 2daa5179e73f)
109 # has been available since 1.9 (or 2daa5179e73f)
109 def command(name, options=(), synopsis=None, norepo=False):
110 def command(name, options=(), synopsis=None, norepo=False):
110 def decorator(func):
111 def decorator(func):
111 if synopsis:
112 if synopsis:
112 cmdtable[name] = func, list(options), synopsis
113 cmdtable[name] = func, list(options), synopsis
113 else:
114 else:
114 cmdtable[name] = func, list(options)
115 cmdtable[name] = func, list(options)
115 if norepo:
116 if norepo:
116 commands.norepo += ' %s' % ' '.join(parsealiases(name))
117 commands.norepo += ' %s' % ' '.join(parsealiases(name))
117 return func
118 return func
118 return decorator
119 return decorator
119
120
120 def getlen(ui):
121 def getlen(ui):
121 if ui.configbool("perf", "stub"):
122 if ui.configbool("perf", "stub"):
122 return lambda x: 1
123 return lambda x: 1
123 return len
124 return len
124
125
125 def gettimer(ui, opts=None):
126 def gettimer(ui, opts=None):
126 """return a timer function and formatter: (timer, formatter)
127 """return a timer function and formatter: (timer, formatter)
127
128
128 This function exists to gather the creation of formatter in a single
129 This function exists to gather the creation of formatter in a single
129 place instead of duplicating it in all performance commands."""
130 place instead of duplicating it in all performance commands."""
130
131
131 # enforce an idle period before execution to counteract power management
132 # enforce an idle period before execution to counteract power management
132 # experimental config: perf.presleep
133 # experimental config: perf.presleep
133 time.sleep(ui.configint("perf", "presleep", 1))
134 time.sleep(getint(ui, "perf", "presleep", 1))
134
135
135 if opts is None:
136 if opts is None:
136 opts = {}
137 opts = {}
137 # redirect all to stderr
138 # redirect all to stderr
138 ui = ui.copy()
139 ui = ui.copy()
139 ui.fout = ui.ferr
140 uifout = safeattrsetter(ui, 'fout', ignoremissing=True)
141 if uifout:
142 # for "historical portability":
143 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
144 uifout.set(ui.ferr)
145
140 # get a formatter
146 # get a formatter
141 fm = ui.formatter('perf', opts)
147 uiformatter = getattr(ui, 'formatter', None)
148 if uiformatter:
149 fm = uiformatter('perf', opts)
150 else:
151 # for "historical portability":
152 # define formatter locally, because ui.formatter has been
153 # available since 2.2 (or ae5f92e154d3)
154 from mercurial import node
155 class defaultformatter(object):
156 """Minimized composition of baseformatter and plainformatter
157 """
158 def __init__(self, ui, topic, opts):
159 self._ui = ui
160 if ui.debugflag:
161 self.hexfunc = node.hex
162 else:
163 self.hexfunc = node.short
164 def __nonzero__(self):
165 return False
166 def startitem(self):
167 pass
168 def data(self, **data):
169 pass
170 def write(self, fields, deftext, *fielddata, **opts):
171 self._ui.write(deftext % fielddata, **opts)
172 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
173 if cond:
174 self._ui.write(deftext % fielddata, **opts)
175 def plain(self, text, **opts):
176 self._ui.write(text, **opts)
177 def end(self):
178 pass
179 fm = defaultformatter(ui, 'perf', opts)
180
142 # stub function, runs code only once instead of in a loop
181 # stub function, runs code only once instead of in a loop
143 # experimental config: perf.stub
182 # experimental config: perf.stub
144 if ui.configbool("perf", "stub"):
183 if ui.configbool("perf", "stub"):
145 return functools.partial(stub_timer, fm), fm
184 return functools.partial(stub_timer, fm), fm
146 return functools.partial(_timer, fm), fm
185 return functools.partial(_timer, fm), fm
147
186
148 def stub_timer(fm, func, title=None):
187 def stub_timer(fm, func, title=None):
149 func()
188 func()
150
189
151 def _timer(fm, func, title=None):
190 def _timer(fm, func, title=None):
152 results = []
191 results = []
153 begin = time.time()
192 begin = time.time()
154 count = 0
193 count = 0
155 while True:
194 while True:
156 ostart = os.times()
195 ostart = os.times()
157 cstart = time.time()
196 cstart = time.time()
158 r = func()
197 r = func()
159 cstop = time.time()
198 cstop = time.time()
160 ostop = os.times()
199 ostop = os.times()
161 count += 1
200 count += 1
162 a, b = ostart, ostop
201 a, b = ostart, ostop
163 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
202 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
164 if cstop - begin > 3 and count >= 100:
203 if cstop - begin > 3 and count >= 100:
165 break
204 break
166 if cstop - begin > 10 and count >= 3:
205 if cstop - begin > 10 and count >= 3:
167 break
206 break
168
207
169 fm.startitem()
208 fm.startitem()
170
209
171 if title:
210 if title:
172 fm.write('title', '! %s\n', title)
211 fm.write('title', '! %s\n', title)
173 if r:
212 if r:
174 fm.write('result', '! result: %s\n', r)
213 fm.write('result', '! result: %s\n', r)
175 m = min(results)
214 m = min(results)
176 fm.plain('!')
215 fm.plain('!')
177 fm.write('wall', ' wall %f', m[0])
216 fm.write('wall', ' wall %f', m[0])
178 fm.write('comb', ' comb %f', m[1] + m[2])
217 fm.write('comb', ' comb %f', m[1] + m[2])
179 fm.write('user', ' user %f', m[1])
218 fm.write('user', ' user %f', m[1])
180 fm.write('sys', ' sys %f', m[2])
219 fm.write('sys', ' sys %f', m[2])
181 fm.write('count', ' (best of %d)', count)
220 fm.write('count', ' (best of %d)', count)
182 fm.plain('\n')
221 fm.plain('\n')
183
222
223 # utilities for historical portability
224
225 def getint(ui, section, name, default):
226 # for "historical portability":
227 # ui.configint has been available since 1.9 (or fa2b596db182)
228 v = ui.config(section, name, None)
229 if v is None:
230 return default
231 try:
232 return int(v)
233 except ValueError:
234 raise error.ConfigError(("%s.%s is not an integer ('%s')")
235 % (section, name, v))
236
237 def safeattrsetter(obj, name, ignoremissing=False):
238 """Ensure that 'obj' has 'name' attribute before subsequent setattr
239
240 This function is aborted, if 'obj' doesn't have 'name' attribute
241 at runtime. This avoids overlooking removal of an attribute, which
242 breaks assumption of performance measurement, in the future.
243
244 This function returns the object to (1) assign a new value, and
245 (2) restore an original value to the attribute.
246
247 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
248 abortion, and this function returns None. This is useful to
249 examine an attribute, which isn't ensured in all Mercurial
250 versions.
251 """
252 if not util.safehasattr(obj, name):
253 if ignoremissing:
254 return None
255 raise error.Abort(("missing attribute %s of %s might break assumption"
256 " of performance measurement") % (name, obj))
257
258 origvalue = getattr(obj, name)
259 class attrutil(object):
260 def set(self, newvalue):
261 setattr(obj, name, newvalue)
262 def restore(self):
263 setattr(obj, name, origvalue)
264
265 return attrutil()
266
267 # utilities to examine each internal API changes
268
269 def getbranchmapsubsettable():
270 # for "historical portability":
271 # subsettable is defined in:
272 # - branchmap since 2.9 (or 175c6fd8cacc)
273 # - repoview since 2.5 (or 59a9f18d4587)
274 for mod in (branchmap, repoview):
275 subsettable = getattr(mod, 'subsettable', None)
276 if subsettable:
277 return subsettable
278
279 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
280 # branchmap and repoview modules exist, but subsettable attribute
281 # doesn't)
282 raise error.Abort(("perfbranchmap not available with this Mercurial"),
283 hint="use 2.5 or later")
284
285 def getsvfs(repo):
286 """Return appropriate object to access files under .hg/store
287 """
288 # for "historical portability":
289 # repo.svfs has been available since 2.3 (or 7034365089bf)
290 svfs = getattr(repo, 'svfs', None)
291 if svfs:
292 return svfs
293 else:
294 return getattr(repo, 'sopener')
295
296 def getvfs(repo):
297 """Return appropriate object to access files under .hg
298 """
299 # for "historical portability":
300 # repo.vfs has been available since 2.3 (or 7034365089bf)
301 vfs = getattr(repo, 'vfs', None)
302 if vfs:
303 return vfs
304 else:
305 return getattr(repo, 'opener')
306
307 def repocleartagscachefunc(repo):
308 """Return the function to clear tags cache according to repo internal API
309 """
310 if util.safehasattr(repo, '_tagscache'): # since 2.0 (or 9dca7653b525)
311 # in this case, setattr(repo, '_tagscache', None) or so isn't
312 # correct way to clear tags cache, because existing code paths
313 # expect _tagscache to be a structured object.
314 def clearcache():
315 # _tagscache has been filteredpropertycache since 2.5 (or
316 # 98c867ac1330), and delattr() can't work in such case
317 if '_tagscache' in vars(repo):
318 del repo.__dict__['_tagscache']
319 return clearcache
320
321 repotags = safeattrsetter(repo, '_tags', ignoremissing=True)
322 if repotags: # since 1.4 (or 5614a628d173)
323 return lambda : repotags.set(None)
324
325 repotagscache = safeattrsetter(repo, 'tagscache', ignoremissing=True)
326 if repotagscache: # since 0.6 (or d7df759d0e97)
327 return lambda : repotagscache.set(None)
328
329 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
330 # this point, but it isn't so problematic, because:
331 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
332 # in perftags() causes failure soon
333 # - perf.py itself has been available since 1.1 (or eb240755386d)
334 raise error.Abort(("tags API of this hg command is unknown"))
335
336 # perf commands
337
184 @command('perfwalk', formatteropts)
338 @command('perfwalk', formatteropts)
185 def perfwalk(ui, repo, *pats, **opts):
339 def perfwalk(ui, repo, *pats, **opts):
186 timer, fm = gettimer(ui, opts)
340 timer, fm = gettimer(ui, opts)
187 try:
341 try:
188 m = scmutil.match(repo[None], pats, {})
342 m = scmutil.match(repo[None], pats, {})
189 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
343 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
190 except Exception:
344 except Exception:
191 try:
345 try:
192 m = scmutil.match(repo[None], pats, {})
346 m = scmutil.match(repo[None], pats, {})
193 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
347 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
194 except Exception:
348 except Exception:
195 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
349 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
196 fm.end()
350 fm.end()
197
351
198 @command('perfannotate', formatteropts)
352 @command('perfannotate', formatteropts)
199 def perfannotate(ui, repo, f, **opts):
353 def perfannotate(ui, repo, f, **opts):
200 timer, fm = gettimer(ui, opts)
354 timer, fm = gettimer(ui, opts)
201 fc = repo['.'][f]
355 fc = repo['.'][f]
202 timer(lambda: len(fc.annotate(True)))
356 timer(lambda: len(fc.annotate(True)))
203 fm.end()
357 fm.end()
204
358
205 @command('perfstatus',
359 @command('perfstatus',
206 [('u', 'unknown', False,
360 [('u', 'unknown', False,
207 'ask status to look for unknown files')] + formatteropts)
361 'ask status to look for unknown files')] + formatteropts)
208 def perfstatus(ui, repo, **opts):
362 def perfstatus(ui, repo, **opts):
209 #m = match.always(repo.root, repo.getcwd())
363 #m = match.always(repo.root, repo.getcwd())
210 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
364 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
211 # False))))
365 # False))))
212 timer, fm = gettimer(ui, opts)
366 timer, fm = gettimer(ui, opts)
213 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
367 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
214 fm.end()
368 fm.end()
215
369
216 @command('perfaddremove', formatteropts)
370 @command('perfaddremove', formatteropts)
217 def perfaddremove(ui, repo, **opts):
371 def perfaddremove(ui, repo, **opts):
218 timer, fm = gettimer(ui, opts)
372 timer, fm = gettimer(ui, opts)
219 try:
373 try:
220 oldquiet = repo.ui.quiet
374 oldquiet = repo.ui.quiet
221 repo.ui.quiet = True
375 repo.ui.quiet = True
222 matcher = scmutil.match(repo[None])
376 matcher = scmutil.match(repo[None])
223 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
377 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
224 finally:
378 finally:
225 repo.ui.quiet = oldquiet
379 repo.ui.quiet = oldquiet
226 fm.end()
380 fm.end()
227
381
228 def clearcaches(cl):
382 def clearcaches(cl):
229 # behave somewhat consistently across internal API changes
383 # behave somewhat consistently across internal API changes
230 if util.safehasattr(cl, 'clearcaches'):
384 if util.safehasattr(cl, 'clearcaches'):
231 cl.clearcaches()
385 cl.clearcaches()
232 elif util.safehasattr(cl, '_nodecache'):
386 elif util.safehasattr(cl, '_nodecache'):
233 from mercurial.node import nullid, nullrev
387 from mercurial.node import nullid, nullrev
234 cl._nodecache = {nullid: nullrev}
388 cl._nodecache = {nullid: nullrev}
235 cl._nodepos = None
389 cl._nodepos = None
236
390
237 @command('perfheads', formatteropts)
391 @command('perfheads', formatteropts)
238 def perfheads(ui, repo, **opts):
392 def perfheads(ui, repo, **opts):
239 timer, fm = gettimer(ui, opts)
393 timer, fm = gettimer(ui, opts)
240 cl = repo.changelog
394 cl = repo.changelog
241 def d():
395 def d():
242 len(cl.headrevs())
396 len(cl.headrevs())
243 clearcaches(cl)
397 clearcaches(cl)
244 timer(d)
398 timer(d)
245 fm.end()
399 fm.end()
246
400
247 @command('perftags', formatteropts)
401 @command('perftags', formatteropts)
248 def perftags(ui, repo, **opts):
402 def perftags(ui, repo, **opts):
249 import mercurial.changelog
403 import mercurial.changelog
250 import mercurial.manifest
404 import mercurial.manifest
251 timer, fm = gettimer(ui, opts)
405 timer, fm = gettimer(ui, opts)
406 svfs = getsvfs(repo)
407 repocleartagscache = repocleartagscachefunc(repo)
252 def t():
408 def t():
253 repo.changelog = mercurial.changelog.changelog(repo.svfs)
409 repo.changelog = mercurial.changelog.changelog(svfs)
254 repo.manifest = mercurial.manifest.manifest(repo.svfs)
410 repo.manifest = mercurial.manifest.manifest(svfs)
255 repo._tags = None
411 repocleartagscache()
256 return len(repo.tags())
412 return len(repo.tags())
257 timer(t)
413 timer(t)
258 fm.end()
414 fm.end()
259
415
260 @command('perfancestors', formatteropts)
416 @command('perfancestors', formatteropts)
261 def perfancestors(ui, repo, **opts):
417 def perfancestors(ui, repo, **opts):
262 timer, fm = gettimer(ui, opts)
418 timer, fm = gettimer(ui, opts)
263 heads = repo.changelog.headrevs()
419 heads = repo.changelog.headrevs()
264 def d():
420 def d():
265 for a in repo.changelog.ancestors(heads):
421 for a in repo.changelog.ancestors(heads):
266 pass
422 pass
267 timer(d)
423 timer(d)
268 fm.end()
424 fm.end()
269
425
270 @command('perfancestorset', formatteropts)
426 @command('perfancestorset', formatteropts)
271 def perfancestorset(ui, repo, revset, **opts):
427 def perfancestorset(ui, repo, revset, **opts):
272 timer, fm = gettimer(ui, opts)
428 timer, fm = gettimer(ui, opts)
273 revs = repo.revs(revset)
429 revs = repo.revs(revset)
274 heads = repo.changelog.headrevs()
430 heads = repo.changelog.headrevs()
275 def d():
431 def d():
276 s = repo.changelog.ancestors(heads)
432 s = repo.changelog.ancestors(heads)
277 for rev in revs:
433 for rev in revs:
278 rev in s
434 rev in s
279 timer(d)
435 timer(d)
280 fm.end()
436 fm.end()
281
437
438 @command('perfchangegroupchangelog', formatteropts +
439 [('', 'version', '02', 'changegroup version'),
440 ('r', 'rev', '', 'revisions to add to changegroup')])
441 def perfchangegroupchangelog(ui, repo, version='02', rev=None, **opts):
442 """Benchmark producing a changelog group for a changegroup.
443
444 This measures the time spent processing the changelog during a
445 bundle operation. This occurs during `hg bundle` and on a server
446 processing a `getbundle` wire protocol request (handles clones
447 and pull requests).
448
449 By default, all revisions are added to the changegroup.
450 """
451 cl = repo.changelog
452 revs = [cl.lookup(r) for r in repo.revs(rev or 'all()')]
453 bundler = changegroup.getbundler(version, repo)
454
455 def lookup(node):
456 # The real bundler reads the revision in order to access the
457 # manifest node and files list. Do that here.
458 cl.read(node)
459 return node
460
461 def d():
462 for chunk in bundler.group(revs, cl, lookup):
463 pass
464
465 timer, fm = gettimer(ui, opts)
466 timer(d)
467 fm.end()
468
282 @command('perfdirs', formatteropts)
469 @command('perfdirs', formatteropts)
283 def perfdirs(ui, repo, **opts):
470 def perfdirs(ui, repo, **opts):
284 timer, fm = gettimer(ui, opts)
471 timer, fm = gettimer(ui, opts)
285 dirstate = repo.dirstate
472 dirstate = repo.dirstate
286 'a' in dirstate
473 'a' in dirstate
287 def d():
474 def d():
288 dirstate.dirs()
475 dirstate.dirs()
289 del dirstate._dirs
476 del dirstate._dirs
290 timer(d)
477 timer(d)
291 fm.end()
478 fm.end()
292
479
293 @command('perfdirstate', formatteropts)
480 @command('perfdirstate', formatteropts)
294 def perfdirstate(ui, repo, **opts):
481 def perfdirstate(ui, repo, **opts):
295 timer, fm = gettimer(ui, opts)
482 timer, fm = gettimer(ui, opts)
296 "a" in repo.dirstate
483 "a" in repo.dirstate
297 def d():
484 def d():
298 repo.dirstate.invalidate()
485 repo.dirstate.invalidate()
299 "a" in repo.dirstate
486 "a" in repo.dirstate
300 timer(d)
487 timer(d)
301 fm.end()
488 fm.end()
302
489
303 @command('perfdirstatedirs', formatteropts)
490 @command('perfdirstatedirs', formatteropts)
304 def perfdirstatedirs(ui, repo, **opts):
491 def perfdirstatedirs(ui, repo, **opts):
305 timer, fm = gettimer(ui, opts)
492 timer, fm = gettimer(ui, opts)
306 "a" in repo.dirstate
493 "a" in repo.dirstate
307 def d():
494 def d():
308 "a" in repo.dirstate._dirs
495 "a" in repo.dirstate._dirs
309 del repo.dirstate._dirs
496 del repo.dirstate._dirs
310 timer(d)
497 timer(d)
311 fm.end()
498 fm.end()
312
499
313 @command('perfdirstatefoldmap', formatteropts)
500 @command('perfdirstatefoldmap', formatteropts)
314 def perfdirstatefoldmap(ui, repo, **opts):
501 def perfdirstatefoldmap(ui, repo, **opts):
315 timer, fm = gettimer(ui, opts)
502 timer, fm = gettimer(ui, opts)
316 dirstate = repo.dirstate
503 dirstate = repo.dirstate
317 'a' in dirstate
504 'a' in dirstate
318 def d():
505 def d():
319 dirstate._filefoldmap.get('a')
506 dirstate._filefoldmap.get('a')
320 del dirstate._filefoldmap
507 del dirstate._filefoldmap
321 timer(d)
508 timer(d)
322 fm.end()
509 fm.end()
323
510
324 @command('perfdirfoldmap', formatteropts)
511 @command('perfdirfoldmap', formatteropts)
325 def perfdirfoldmap(ui, repo, **opts):
512 def perfdirfoldmap(ui, repo, **opts):
326 timer, fm = gettimer(ui, opts)
513 timer, fm = gettimer(ui, opts)
327 dirstate = repo.dirstate
514 dirstate = repo.dirstate
328 'a' in dirstate
515 'a' in dirstate
329 def d():
516 def d():
330 dirstate._dirfoldmap.get('a')
517 dirstate._dirfoldmap.get('a')
331 del dirstate._dirfoldmap
518 del dirstate._dirfoldmap
332 del dirstate._dirs
519 del dirstate._dirs
333 timer(d)
520 timer(d)
334 fm.end()
521 fm.end()
335
522
336 @command('perfdirstatewrite', formatteropts)
523 @command('perfdirstatewrite', formatteropts)
337 def perfdirstatewrite(ui, repo, **opts):
524 def perfdirstatewrite(ui, repo, **opts):
338 timer, fm = gettimer(ui, opts)
525 timer, fm = gettimer(ui, opts)
339 ds = repo.dirstate
526 ds = repo.dirstate
340 "a" in ds
527 "a" in ds
341 def d():
528 def d():
342 ds._dirty = True
529 ds._dirty = True
343 ds.write(repo.currenttransaction())
530 ds.write(repo.currenttransaction())
344 timer(d)
531 timer(d)
345 fm.end()
532 fm.end()
346
533
347 @command('perfmergecalculate',
534 @command('perfmergecalculate',
348 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
535 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
349 def perfmergecalculate(ui, repo, rev, **opts):
536 def perfmergecalculate(ui, repo, rev, **opts):
350 timer, fm = gettimer(ui, opts)
537 timer, fm = gettimer(ui, opts)
351 wctx = repo[None]
538 wctx = repo[None]
352 rctx = scmutil.revsingle(repo, rev, rev)
539 rctx = scmutil.revsingle(repo, rev, rev)
353 ancestor = wctx.ancestor(rctx)
540 ancestor = wctx.ancestor(rctx)
354 # we don't want working dir files to be stat'd in the benchmark, so prime
541 # we don't want working dir files to be stat'd in the benchmark, so prime
355 # that cache
542 # that cache
356 wctx.dirty()
543 wctx.dirty()
357 def d():
544 def d():
358 # acceptremote is True because we don't want prompts in the middle of
545 # acceptremote is True because we don't want prompts in the middle of
359 # our benchmark
546 # our benchmark
360 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
547 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
361 acceptremote=True, followcopies=True)
548 acceptremote=True, followcopies=True)
362 timer(d)
549 timer(d)
363 fm.end()
550 fm.end()
364
551
365 @command('perfpathcopies', [], "REV REV")
552 @command('perfpathcopies', [], "REV REV")
366 def perfpathcopies(ui, repo, rev1, rev2, **opts):
553 def perfpathcopies(ui, repo, rev1, rev2, **opts):
367 timer, fm = gettimer(ui, opts)
554 timer, fm = gettimer(ui, opts)
368 ctx1 = scmutil.revsingle(repo, rev1, rev1)
555 ctx1 = scmutil.revsingle(repo, rev1, rev1)
369 ctx2 = scmutil.revsingle(repo, rev2, rev2)
556 ctx2 = scmutil.revsingle(repo, rev2, rev2)
370 def d():
557 def d():
371 copies.pathcopies(ctx1, ctx2)
558 copies.pathcopies(ctx1, ctx2)
372 timer(d)
559 timer(d)
373 fm.end()
560 fm.end()
374
561
375 @command('perfmanifest', [], 'REV')
562 @command('perfmanifest', [], 'REV')
376 def perfmanifest(ui, repo, rev, **opts):
563 def perfmanifest(ui, repo, rev, **opts):
377 timer, fm = gettimer(ui, opts)
564 timer, fm = gettimer(ui, opts)
378 ctx = scmutil.revsingle(repo, rev, rev)
565 ctx = scmutil.revsingle(repo, rev, rev)
379 t = ctx.manifestnode()
566 t = ctx.manifestnode()
380 def d():
567 def d():
381 repo.manifest.clearcaches()
568 repo.manifest.clearcaches()
382 repo.manifest.read(t)
569 repo.manifest.read(t)
383 timer(d)
570 timer(d)
384 fm.end()
571 fm.end()
385
572
386 @command('perfchangeset', formatteropts)
573 @command('perfchangeset', formatteropts)
387 def perfchangeset(ui, repo, rev, **opts):
574 def perfchangeset(ui, repo, rev, **opts):
388 timer, fm = gettimer(ui, opts)
575 timer, fm = gettimer(ui, opts)
389 n = repo[rev].node()
576 n = repo[rev].node()
390 def d():
577 def d():
391 repo.changelog.read(n)
578 repo.changelog.read(n)
392 #repo.changelog._cache = None
579 #repo.changelog._cache = None
393 timer(d)
580 timer(d)
394 fm.end()
581 fm.end()
395
582
396 @command('perfindex', formatteropts)
583 @command('perfindex', formatteropts)
397 def perfindex(ui, repo, **opts):
584 def perfindex(ui, repo, **opts):
398 import mercurial.revlog
585 import mercurial.revlog
399 timer, fm = gettimer(ui, opts)
586 timer, fm = gettimer(ui, opts)
400 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
587 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
401 n = repo["tip"].node()
588 n = repo["tip"].node()
589 svfs = getsvfs(repo)
402 def d():
590 def d():
403 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
591 cl = mercurial.revlog.revlog(svfs, "00changelog.i")
404 cl.rev(n)
592 cl.rev(n)
405 timer(d)
593 timer(d)
406 fm.end()
594 fm.end()
407
595
408 @command('perfstartup', formatteropts)
596 @command('perfstartup', formatteropts)
409 def perfstartup(ui, repo, **opts):
597 def perfstartup(ui, repo, **opts):
410 timer, fm = gettimer(ui, opts)
598 timer, fm = gettimer(ui, opts)
411 cmd = sys.argv[0]
599 cmd = sys.argv[0]
412 def d():
600 def d():
413 if os.name != 'nt':
601 if os.name != 'nt':
414 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
602 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
415 else:
603 else:
416 os.environ['HGRCPATH'] = ''
604 os.environ['HGRCPATH'] = ''
417 os.system("%s version -q > NUL" % cmd)
605 os.system("%s version -q > NUL" % cmd)
418 timer(d)
606 timer(d)
419 fm.end()
607 fm.end()
420
608
421 @command('perfparents', formatteropts)
609 @command('perfparents', formatteropts)
422 def perfparents(ui, repo, **opts):
610 def perfparents(ui, repo, **opts):
423 timer, fm = gettimer(ui, opts)
611 timer, fm = gettimer(ui, opts)
424 # control the number of commits perfparents iterates over
612 # control the number of commits perfparents iterates over
425 # experimental config: perf.parentscount
613 # experimental config: perf.parentscount
426 count = ui.configint("perf", "parentscount", 1000)
614 count = getint(ui, "perf", "parentscount", 1000)
427 if len(repo.changelog) < count:
615 if len(repo.changelog) < count:
428 raise error.Abort("repo needs %d commits for this test" % count)
616 raise error.Abort("repo needs %d commits for this test" % count)
429 repo = repo.unfiltered()
617 repo = repo.unfiltered()
430 nl = [repo.changelog.node(i) for i in xrange(count)]
618 nl = [repo.changelog.node(i) for i in xrange(count)]
431 def d():
619 def d():
432 for n in nl:
620 for n in nl:
433 repo.changelog.parents(n)
621 repo.changelog.parents(n)
434 timer(d)
622 timer(d)
435 fm.end()
623 fm.end()
436
624
437 @command('perfctxfiles', formatteropts)
625 @command('perfctxfiles', formatteropts)
438 def perfctxfiles(ui, repo, x, **opts):
626 def perfctxfiles(ui, repo, x, **opts):
439 x = int(x)
627 x = int(x)
440 timer, fm = gettimer(ui, opts)
628 timer, fm = gettimer(ui, opts)
441 def d():
629 def d():
442 len(repo[x].files())
630 len(repo[x].files())
443 timer(d)
631 timer(d)
444 fm.end()
632 fm.end()
445
633
446 @command('perfrawfiles', formatteropts)
634 @command('perfrawfiles', formatteropts)
447 def perfrawfiles(ui, repo, x, **opts):
635 def perfrawfiles(ui, repo, x, **opts):
448 x = int(x)
636 x = int(x)
449 timer, fm = gettimer(ui, opts)
637 timer, fm = gettimer(ui, opts)
450 cl = repo.changelog
638 cl = repo.changelog
451 def d():
639 def d():
452 len(cl.read(x)[3])
640 len(cl.read(x)[3])
453 timer(d)
641 timer(d)
454 fm.end()
642 fm.end()
455
643
456 @command('perflookup', formatteropts)
644 @command('perflookup', formatteropts)
457 def perflookup(ui, repo, rev, **opts):
645 def perflookup(ui, repo, rev, **opts):
458 timer, fm = gettimer(ui, opts)
646 timer, fm = gettimer(ui, opts)
459 timer(lambda: len(repo.lookup(rev)))
647 timer(lambda: len(repo.lookup(rev)))
460 fm.end()
648 fm.end()
461
649
462 @command('perfrevrange', formatteropts)
650 @command('perfrevrange', formatteropts)
463 def perfrevrange(ui, repo, *specs, **opts):
651 def perfrevrange(ui, repo, *specs, **opts):
464 timer, fm = gettimer(ui, opts)
652 timer, fm = gettimer(ui, opts)
465 revrange = scmutil.revrange
653 revrange = scmutil.revrange
466 timer(lambda: len(revrange(repo, specs)))
654 timer(lambda: len(revrange(repo, specs)))
467 fm.end()
655 fm.end()
468
656
469 @command('perfnodelookup', formatteropts)
657 @command('perfnodelookup', formatteropts)
470 def perfnodelookup(ui, repo, rev, **opts):
658 def perfnodelookup(ui, repo, rev, **opts):
471 timer, fm = gettimer(ui, opts)
659 timer, fm = gettimer(ui, opts)
472 import mercurial.revlog
660 import mercurial.revlog
473 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
661 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
474 n = repo[rev].node()
662 n = repo[rev].node()
475 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
663 cl = mercurial.revlog.revlog(getsvfs(repo), "00changelog.i")
476 def d():
664 def d():
477 cl.rev(n)
665 cl.rev(n)
478 clearcaches(cl)
666 clearcaches(cl)
479 timer(d)
667 timer(d)
480 fm.end()
668 fm.end()
481
669
482 @command('perflog',
670 @command('perflog',
483 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
671 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
484 def perflog(ui, repo, rev=None, **opts):
672 def perflog(ui, repo, rev=None, **opts):
485 if rev is None:
673 if rev is None:
486 rev=[]
674 rev=[]
487 timer, fm = gettimer(ui, opts)
675 timer, fm = gettimer(ui, opts)
488 ui.pushbuffer()
676 ui.pushbuffer()
489 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
677 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
490 copies=opts.get('rename')))
678 copies=opts.get('rename')))
491 ui.popbuffer()
679 ui.popbuffer()
492 fm.end()
680 fm.end()
493
681
494 @command('perfmoonwalk', formatteropts)
682 @command('perfmoonwalk', formatteropts)
495 def perfmoonwalk(ui, repo, **opts):
683 def perfmoonwalk(ui, repo, **opts):
496 """benchmark walking the changelog backwards
684 """benchmark walking the changelog backwards
497
685
498 This also loads the changelog data for each revision in the changelog.
686 This also loads the changelog data for each revision in the changelog.
499 """
687 """
500 timer, fm = gettimer(ui, opts)
688 timer, fm = gettimer(ui, opts)
501 def moonwalk():
689 def moonwalk():
502 for i in xrange(len(repo), -1, -1):
690 for i in xrange(len(repo), -1, -1):
503 ctx = repo[i]
691 ctx = repo[i]
504 ctx.branch() # read changelog data (in addition to the index)
692 ctx.branch() # read changelog data (in addition to the index)
505 timer(moonwalk)
693 timer(moonwalk)
506 fm.end()
694 fm.end()
507
695
508 @command('perftemplating', formatteropts)
696 @command('perftemplating', formatteropts)
509 def perftemplating(ui, repo, rev=None, **opts):
697 def perftemplating(ui, repo, rev=None, **opts):
510 if rev is None:
698 if rev is None:
511 rev=[]
699 rev=[]
512 timer, fm = gettimer(ui, opts)
700 timer, fm = gettimer(ui, opts)
513 ui.pushbuffer()
701 ui.pushbuffer()
514 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
702 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
515 template='{date|shortdate} [{rev}:{node|short}]'
703 template='{date|shortdate} [{rev}:{node|short}]'
516 ' {author|person}: {desc|firstline}\n'))
704 ' {author|person}: {desc|firstline}\n'))
517 ui.popbuffer()
705 ui.popbuffer()
518 fm.end()
706 fm.end()
519
707
520 @command('perfcca', formatteropts)
708 @command('perfcca', formatteropts)
521 def perfcca(ui, repo, **opts):
709 def perfcca(ui, repo, **opts):
522 timer, fm = gettimer(ui, opts)
710 timer, fm = gettimer(ui, opts)
523 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
711 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
524 fm.end()
712 fm.end()
525
713
526 @command('perffncacheload', formatteropts)
714 @command('perffncacheload', formatteropts)
527 def perffncacheload(ui, repo, **opts):
715 def perffncacheload(ui, repo, **opts):
528 timer, fm = gettimer(ui, opts)
716 timer, fm = gettimer(ui, opts)
529 s = repo.store
717 s = repo.store
530 def d():
718 def d():
531 s.fncache._load()
719 s.fncache._load()
532 timer(d)
720 timer(d)
533 fm.end()
721 fm.end()
534
722
535 @command('perffncachewrite', formatteropts)
723 @command('perffncachewrite', formatteropts)
536 def perffncachewrite(ui, repo, **opts):
724 def perffncachewrite(ui, repo, **opts):
537 timer, fm = gettimer(ui, opts)
725 timer, fm = gettimer(ui, opts)
538 s = repo.store
726 s = repo.store
539 s.fncache._load()
727 s.fncache._load()
540 lock = repo.lock()
728 lock = repo.lock()
541 tr = repo.transaction('perffncachewrite')
729 tr = repo.transaction('perffncachewrite')
542 def d():
730 def d():
543 s.fncache._dirty = True
731 s.fncache._dirty = True
544 s.fncache.write(tr)
732 s.fncache.write(tr)
545 timer(d)
733 timer(d)
734 tr.close()
546 lock.release()
735 lock.release()
547 tr.close()
548 fm.end()
736 fm.end()
549
737
550 @command('perffncacheencode', formatteropts)
738 @command('perffncacheencode', formatteropts)
551 def perffncacheencode(ui, repo, **opts):
739 def perffncacheencode(ui, repo, **opts):
552 timer, fm = gettimer(ui, opts)
740 timer, fm = gettimer(ui, opts)
553 s = repo.store
741 s = repo.store
554 s.fncache._load()
742 s.fncache._load()
555 def d():
743 def d():
556 for p in s.fncache.entries:
744 for p in s.fncache.entries:
557 s.encode(p)
745 s.encode(p)
558 timer(d)
746 timer(d)
559 fm.end()
747 fm.end()
560
748
561 @command('perfdiffwd', formatteropts)
749 @command('perfdiffwd', formatteropts)
562 def perfdiffwd(ui, repo, **opts):
750 def perfdiffwd(ui, repo, **opts):
563 """Profile diff of working directory changes"""
751 """Profile diff of working directory changes"""
564 timer, fm = gettimer(ui, opts)
752 timer, fm = gettimer(ui, opts)
565 options = {
753 options = {
566 'w': 'ignore_all_space',
754 'w': 'ignore_all_space',
567 'b': 'ignore_space_change',
755 'b': 'ignore_space_change',
568 'B': 'ignore_blank_lines',
756 'B': 'ignore_blank_lines',
569 }
757 }
570
758
571 for diffopt in ('', 'w', 'b', 'B', 'wB'):
759 for diffopt in ('', 'w', 'b', 'B', 'wB'):
572 opts = dict((options[c], '1') for c in diffopt)
760 opts = dict((options[c], '1') for c in diffopt)
573 def d():
761 def d():
574 ui.pushbuffer()
762 ui.pushbuffer()
575 commands.diff(ui, repo, **opts)
763 commands.diff(ui, repo, **opts)
576 ui.popbuffer()
764 ui.popbuffer()
577 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
765 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
578 timer(d, title)
766 timer(d, title)
579 fm.end()
767 fm.end()
580
768
581 @command('perfrevlog', revlogopts + formatteropts +
769 @command('perfrevlog', revlogopts + formatteropts +
582 [('d', 'dist', 100, 'distance between the revisions'),
770 [('d', 'dist', 100, 'distance between the revisions'),
583 ('s', 'startrev', 0, 'revision to start reading at')],
771 ('s', 'startrev', 0, 'revision to start reading at'),
772 ('', 'reverse', False, 'read in reverse')],
584 '-c|-m|FILE')
773 '-c|-m|FILE')
585 def perfrevlog(ui, repo, file_=None, startrev=0, **opts):
774 def perfrevlog(ui, repo, file_=None, startrev=0, reverse=False, **opts):
586 """Benchmark reading a series of revisions from a revlog.
775 """Benchmark reading a series of revisions from a revlog.
587
776
588 By default, we read every ``-d/--dist`` revision from 0 to tip of
777 By default, we read every ``-d/--dist`` revision from 0 to tip of
589 the specified revlog.
778 the specified revlog.
590
779
591 The start revision can be defined via ``-s/--startrev``.
780 The start revision can be defined via ``-s/--startrev``.
592 """
781 """
593 timer, fm = gettimer(ui, opts)
782 timer, fm = gettimer(ui, opts)
594 dist = opts['dist']
595 _len = getlen(ui)
783 _len = getlen(ui)
784
596 def d():
785 def d():
597 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
786 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
598 for x in xrange(startrev, _len(r), dist):
787
788 startrev = 0
789 endrev = _len(r)
790 dist = opts['dist']
791
792 if reverse:
793 startrev, endrev = endrev, startrev
794 dist = -1 * dist
795
796 for x in xrange(startrev, endrev, dist):
599 r.revision(r.node(x))
797 r.revision(r.node(x))
600
798
601 timer(d)
799 timer(d)
602 fm.end()
800 fm.end()
603
801
604 @command('perfrevlogrevision', revlogopts + formatteropts +
802 @command('perfrevlogrevision', revlogopts + formatteropts +
605 [('', 'cache', False, 'use caches instead of clearing')],
803 [('', 'cache', False, 'use caches instead of clearing')],
606 '-c|-m|FILE REV')
804 '-c|-m|FILE REV')
607 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
805 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
608 """Benchmark obtaining a revlog revision.
806 """Benchmark obtaining a revlog revision.
609
807
610 Obtaining a revlog revision consists of roughly the following steps:
808 Obtaining a revlog revision consists of roughly the following steps:
611
809
612 1. Compute the delta chain
810 1. Compute the delta chain
613 2. Obtain the raw chunks for that delta chain
811 2. Obtain the raw chunks for that delta chain
614 3. Decompress each raw chunk
812 3. Decompress each raw chunk
615 4. Apply binary patches to obtain fulltext
813 4. Apply binary patches to obtain fulltext
616 5. Verify hash of fulltext
814 5. Verify hash of fulltext
617
815
618 This command measures the time spent in each of these phases.
816 This command measures the time spent in each of these phases.
619 """
817 """
620 if opts.get('changelog') or opts.get('manifest'):
818 if opts.get('changelog') or opts.get('manifest'):
621 file_, rev = None, file_
819 file_, rev = None, file_
622 elif rev is None:
820 elif rev is None:
623 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
821 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
624
822
625 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
823 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
626 node = r.lookup(rev)
824 node = r.lookup(rev)
627 rev = r.rev(node)
825 rev = r.rev(node)
628
826
629 def dodeltachain(rev):
827 def dodeltachain(rev):
630 if not cache:
828 if not cache:
631 r.clearcaches()
829 r.clearcaches()
632 r._deltachain(rev)
830 r._deltachain(rev)
633
831
634 def doread(chain):
832 def doread(chain):
635 if not cache:
833 if not cache:
636 r.clearcaches()
834 r.clearcaches()
637 r._chunkraw(chain[0], chain[-1])
835 r._chunkraw(chain[0], chain[-1])
638
836
639 def dodecompress(data, chain):
837 def dodecompress(data, chain):
640 if not cache:
838 if not cache:
641 r.clearcaches()
839 r.clearcaches()
642
840
643 start = r.start
841 start = r.start
644 length = r.length
842 length = r.length
645 inline = r._inline
843 inline = r._inline
646 iosize = r._io.size
844 iosize = r._io.size
647 buffer = util.buffer
845 buffer = util.buffer
648 offset = start(chain[0])
846 offset = start(chain[0])
649
847
650 for rev in chain:
848 for rev in chain:
651 chunkstart = start(rev)
849 chunkstart = start(rev)
652 if inline:
850 if inline:
653 chunkstart += (rev + 1) * iosize
851 chunkstart += (rev + 1) * iosize
654 chunklength = length(rev)
852 chunklength = length(rev)
655 b = buffer(data, chunkstart - offset, chunklength)
853 b = buffer(data, chunkstart - offset, chunklength)
656 revlog.decompress(b)
854 revlog.decompress(b)
657
855
658 def dopatch(text, bins):
856 def dopatch(text, bins):
659 if not cache:
857 if not cache:
660 r.clearcaches()
858 r.clearcaches()
661 mdiff.patches(text, bins)
859 mdiff.patches(text, bins)
662
860
663 def dohash(text):
861 def dohash(text):
664 if not cache:
862 if not cache:
665 r.clearcaches()
863 r.clearcaches()
666 r._checkhash(text, node, rev)
864 r._checkhash(text, node, rev)
667
865
668 def dorevision():
866 def dorevision():
669 if not cache:
867 if not cache:
670 r.clearcaches()
868 r.clearcaches()
671 r.revision(node)
869 r.revision(node)
672
870
673 chain = r._deltachain(rev)[0]
871 chain = r._deltachain(rev)[0]
674 data = r._chunkraw(chain[0], chain[-1])[1]
872 data = r._chunkraw(chain[0], chain[-1])[1]
675 bins = r._chunks(chain)
873 bins = r._chunks(chain)
676 text = str(bins[0])
874 text = str(bins[0])
677 bins = bins[1:]
875 bins = bins[1:]
678 text = mdiff.patches(text, bins)
876 text = mdiff.patches(text, bins)
679
877
680 benches = [
878 benches = [
681 (lambda: dorevision(), 'full'),
879 (lambda: dorevision(), 'full'),
682 (lambda: dodeltachain(rev), 'deltachain'),
880 (lambda: dodeltachain(rev), 'deltachain'),
683 (lambda: doread(chain), 'read'),
881 (lambda: doread(chain), 'read'),
684 (lambda: dodecompress(data, chain), 'decompress'),
882 (lambda: dodecompress(data, chain), 'decompress'),
685 (lambda: dopatch(text, bins), 'patch'),
883 (lambda: dopatch(text, bins), 'patch'),
686 (lambda: dohash(text), 'hash'),
884 (lambda: dohash(text), 'hash'),
687 ]
885 ]
688
886
689 for fn, title in benches:
887 for fn, title in benches:
690 timer, fm = gettimer(ui, opts)
888 timer, fm = gettimer(ui, opts)
691 timer(fn, title=title)
889 timer(fn, title=title)
692 fm.end()
890 fm.end()
693
891
694 @command('perfrevset',
892 @command('perfrevset',
695 [('C', 'clear', False, 'clear volatile cache between each call.'),
893 [('C', 'clear', False, 'clear volatile cache between each call.'),
696 ('', 'contexts', False, 'obtain changectx for each revision')]
894 ('', 'contexts', False, 'obtain changectx for each revision')]
697 + formatteropts, "REVSET")
895 + formatteropts, "REVSET")
698 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
896 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
699 """benchmark the execution time of a revset
897 """benchmark the execution time of a revset
700
898
701 Use the --clean option if need to evaluate the impact of build volatile
899 Use the --clean option if need to evaluate the impact of build volatile
702 revisions set cache on the revset execution. Volatile cache hold filtered
900 revisions set cache on the revset execution. Volatile cache hold filtered
703 and obsolete related cache."""
901 and obsolete related cache."""
704 timer, fm = gettimer(ui, opts)
902 timer, fm = gettimer(ui, opts)
705 def d():
903 def d():
706 if clear:
904 if clear:
707 repo.invalidatevolatilesets()
905 repo.invalidatevolatilesets()
708 if contexts:
906 if contexts:
709 for ctx in repo.set(expr): pass
907 for ctx in repo.set(expr): pass
710 else:
908 else:
711 for r in repo.revs(expr): pass
909 for r in repo.revs(expr): pass
712 timer(d)
910 timer(d)
713 fm.end()
911 fm.end()
714
912
715 @command('perfvolatilesets', formatteropts)
913 @command('perfvolatilesets', formatteropts)
716 def perfvolatilesets(ui, repo, *names, **opts):
914 def perfvolatilesets(ui, repo, *names, **opts):
717 """benchmark the computation of various volatile set
915 """benchmark the computation of various volatile set
718
916
719 Volatile set computes element related to filtering and obsolescence."""
917 Volatile set computes element related to filtering and obsolescence."""
720 timer, fm = gettimer(ui, opts)
918 timer, fm = gettimer(ui, opts)
721 repo = repo.unfiltered()
919 repo = repo.unfiltered()
722
920
723 def getobs(name):
921 def getobs(name):
724 def d():
922 def d():
725 repo.invalidatevolatilesets()
923 repo.invalidatevolatilesets()
726 obsolete.getrevs(repo, name)
924 obsolete.getrevs(repo, name)
727 return d
925 return d
728
926
729 allobs = sorted(obsolete.cachefuncs)
927 allobs = sorted(obsolete.cachefuncs)
730 if names:
928 if names:
731 allobs = [n for n in allobs if n in names]
929 allobs = [n for n in allobs if n in names]
732
930
733 for name in allobs:
931 for name in allobs:
734 timer(getobs(name), title=name)
932 timer(getobs(name), title=name)
735
933
736 def getfiltered(name):
934 def getfiltered(name):
737 def d():
935 def d():
738 repo.invalidatevolatilesets()
936 repo.invalidatevolatilesets()
739 repoview.filterrevs(repo, name)
937 repoview.filterrevs(repo, name)
740 return d
938 return d
741
939
742 allfilter = sorted(repoview.filtertable)
940 allfilter = sorted(repoview.filtertable)
743 if names:
941 if names:
744 allfilter = [n for n in allfilter if n in names]
942 allfilter = [n for n in allfilter if n in names]
745
943
746 for name in allfilter:
944 for name in allfilter:
747 timer(getfiltered(name), title=name)
945 timer(getfiltered(name), title=name)
748 fm.end()
946 fm.end()
749
947
750 @command('perfbranchmap',
948 @command('perfbranchmap',
751 [('f', 'full', False,
949 [('f', 'full', False,
752 'Includes build time of subset'),
950 'Includes build time of subset'),
753 ] + formatteropts)
951 ] + formatteropts)
754 def perfbranchmap(ui, repo, full=False, **opts):
952 def perfbranchmap(ui, repo, full=False, **opts):
755 """benchmark the update of a branchmap
953 """benchmark the update of a branchmap
756
954
757 This benchmarks the full repo.branchmap() call with read and write disabled
955 This benchmarks the full repo.branchmap() call with read and write disabled
758 """
956 """
759 timer, fm = gettimer(ui, opts)
957 timer, fm = gettimer(ui, opts)
760 def getbranchmap(filtername):
958 def getbranchmap(filtername):
761 """generate a benchmark function for the filtername"""
959 """generate a benchmark function for the filtername"""
762 if filtername is None:
960 if filtername is None:
763 view = repo
961 view = repo
764 else:
962 else:
765 view = repo.filtered(filtername)
963 view = repo.filtered(filtername)
766 def d():
964 def d():
767 if full:
965 if full:
768 view._branchcaches.clear()
966 view._branchcaches.clear()
769 else:
967 else:
770 view._branchcaches.pop(filtername, None)
968 view._branchcaches.pop(filtername, None)
771 view.branchmap()
969 view.branchmap()
772 return d
970 return d
773 # add filter in smaller subset to bigger subset
971 # add filter in smaller subset to bigger subset
774 possiblefilters = set(repoview.filtertable)
972 possiblefilters = set(repoview.filtertable)
973 subsettable = getbranchmapsubsettable()
775 allfilters = []
974 allfilters = []
776 while possiblefilters:
975 while possiblefilters:
777 for name in possiblefilters:
976 for name in possiblefilters:
778 subset = branchmap.subsettable.get(name)
977 subset = subsettable.get(name)
779 if subset not in possiblefilters:
978 if subset not in possiblefilters:
780 break
979 break
781 else:
980 else:
782 assert False, 'subset cycle %s!' % possiblefilters
981 assert False, 'subset cycle %s!' % possiblefilters
783 allfilters.append(name)
982 allfilters.append(name)
784 possiblefilters.remove(name)
983 possiblefilters.remove(name)
785
984
786 # warm the cache
985 # warm the cache
787 if not full:
986 if not full:
788 for name in allfilters:
987 for name in allfilters:
789 repo.filtered(name).branchmap()
988 repo.filtered(name).branchmap()
790 # add unfiltered
989 # add unfiltered
791 allfilters.append(None)
990 allfilters.append(None)
792 oldread = branchmap.read
991
793 oldwrite = branchmap.branchcache.write
992 branchcacheread = safeattrsetter(branchmap, 'read')
993 branchcachewrite = safeattrsetter(branchmap.branchcache, 'write')
994 branchcacheread.set(lambda repo: None)
995 branchcachewrite.set(lambda bc, repo: None)
794 try:
996 try:
795 branchmap.read = lambda repo: None
796 branchmap.write = lambda repo: None
797 for name in allfilters:
997 for name in allfilters:
798 timer(getbranchmap(name), title=str(name))
998 timer(getbranchmap(name), title=str(name))
799 finally:
999 finally:
800 branchmap.read = oldread
1000 branchcacheread.restore()
801 branchmap.branchcache.write = oldwrite
1001 branchcachewrite.restore()
802 fm.end()
1002 fm.end()
803
1003
804 @command('perfloadmarkers')
1004 @command('perfloadmarkers')
805 def perfloadmarkers(ui, repo):
1005 def perfloadmarkers(ui, repo):
806 """benchmark the time to parse the on-disk markers for a repo
1006 """benchmark the time to parse the on-disk markers for a repo
807
1007
808 Result is the number of markers in the repo."""
1008 Result is the number of markers in the repo."""
809 timer, fm = gettimer(ui)
1009 timer, fm = gettimer(ui)
810 timer(lambda: len(obsolete.obsstore(repo.svfs)))
1010 svfs = getsvfs(repo)
1011 timer(lambda: len(obsolete.obsstore(svfs)))
811 fm.end()
1012 fm.end()
812
1013
813 @command('perflrucachedict', formatteropts +
1014 @command('perflrucachedict', formatteropts +
814 [('', 'size', 4, 'size of cache'),
1015 [('', 'size', 4, 'size of cache'),
815 ('', 'gets', 10000, 'number of key lookups'),
1016 ('', 'gets', 10000, 'number of key lookups'),
816 ('', 'sets', 10000, 'number of key sets'),
1017 ('', 'sets', 10000, 'number of key sets'),
817 ('', 'mixed', 10000, 'number of mixed mode operations'),
1018 ('', 'mixed', 10000, 'number of mixed mode operations'),
818 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
1019 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
819 norepo=True)
1020 norepo=True)
820 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
1021 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
821 mixedgetfreq=50, **opts):
1022 mixedgetfreq=50, **opts):
822 def doinit():
1023 def doinit():
823 for i in xrange(10000):
1024 for i in xrange(10000):
824 util.lrucachedict(size)
1025 util.lrucachedict(size)
825
1026
826 values = []
1027 values = []
827 for i in xrange(size):
1028 for i in xrange(size):
828 values.append(random.randint(0, sys.maxint))
1029 values.append(random.randint(0, sys.maxint))
829
1030
830 # Get mode fills the cache and tests raw lookup performance with no
1031 # Get mode fills the cache and tests raw lookup performance with no
831 # eviction.
1032 # eviction.
832 getseq = []
1033 getseq = []
833 for i in xrange(gets):
1034 for i in xrange(gets):
834 getseq.append(random.choice(values))
1035 getseq.append(random.choice(values))
835
1036
836 def dogets():
1037 def dogets():
837 d = util.lrucachedict(size)
1038 d = util.lrucachedict(size)
838 for v in values:
1039 for v in values:
839 d[v] = v
1040 d[v] = v
840 for key in getseq:
1041 for key in getseq:
841 value = d[key]
1042 value = d[key]
842 value # silence pyflakes warning
1043 value # silence pyflakes warning
843
1044
844 # Set mode tests insertion speed with cache eviction.
1045 # Set mode tests insertion speed with cache eviction.
845 setseq = []
1046 setseq = []
846 for i in xrange(sets):
1047 for i in xrange(sets):
847 setseq.append(random.randint(0, sys.maxint))
1048 setseq.append(random.randint(0, sys.maxint))
848
1049
849 def dosets():
1050 def dosets():
850 d = util.lrucachedict(size)
1051 d = util.lrucachedict(size)
851 for v in setseq:
1052 for v in setseq:
852 d[v] = v
1053 d[v] = v
853
1054
854 # Mixed mode randomly performs gets and sets with eviction.
1055 # Mixed mode randomly performs gets and sets with eviction.
855 mixedops = []
1056 mixedops = []
856 for i in xrange(mixed):
1057 for i in xrange(mixed):
857 r = random.randint(0, 100)
1058 r = random.randint(0, 100)
858 if r < mixedgetfreq:
1059 if r < mixedgetfreq:
859 op = 0
1060 op = 0
860 else:
1061 else:
861 op = 1
1062 op = 1
862
1063
863 mixedops.append((op, random.randint(0, size * 2)))
1064 mixedops.append((op, random.randint(0, size * 2)))
864
1065
865 def domixed():
1066 def domixed():
866 d = util.lrucachedict(size)
1067 d = util.lrucachedict(size)
867
1068
868 for op, v in mixedops:
1069 for op, v in mixedops:
869 if op == 0:
1070 if op == 0:
870 try:
1071 try:
871 d[v]
1072 d[v]
872 except KeyError:
1073 except KeyError:
873 pass
1074 pass
874 else:
1075 else:
875 d[v] = v
1076 d[v] = v
876
1077
877 benches = [
1078 benches = [
878 (doinit, 'init'),
1079 (doinit, 'init'),
879 (dogets, 'gets'),
1080 (dogets, 'gets'),
880 (dosets, 'sets'),
1081 (dosets, 'sets'),
881 (domixed, 'mixed')
1082 (domixed, 'mixed')
882 ]
1083 ]
883
1084
884 for fn, title in benches:
1085 for fn, title in benches:
885 timer, fm = gettimer(ui, opts)
1086 timer, fm = gettimer(ui, opts)
886 timer(fn, title=title)
1087 timer(fn, title=title)
887 fm.end()
1088 fm.end()
888
1089
889 def uisetup(ui):
1090 def uisetup(ui):
890 if (util.safehasattr(cmdutil, 'openrevlog') and
1091 if (util.safehasattr(cmdutil, 'openrevlog') and
891 not util.safehasattr(commands, 'debugrevlogopts')):
1092 not util.safehasattr(commands, 'debugrevlogopts')):
892 # for "historical portability":
1093 # for "historical portability":
893 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
1094 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
894 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
1095 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
895 # openrevlog() should cause failure, because it has been
1096 # openrevlog() should cause failure, because it has been
896 # available since 3.5 (or 49c583ca48c4).
1097 # available since 3.5 (or 49c583ca48c4).
897 def openrevlog(orig, repo, cmd, file_, opts):
1098 def openrevlog(orig, repo, cmd, file_, opts):
898 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
1099 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
899 raise error.Abort("This version doesn't support --dir option",
1100 raise error.Abort("This version doesn't support --dir option",
900 hint="use 3.5 or later")
1101 hint="use 3.5 or later")
901 return orig(repo, cmd, file_, opts)
1102 return orig(repo, cmd, file_, opts)
902 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
1103 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
@@ -1,516 +1,516 b''
1 # synthrepo.py - repo synthesis
1 # synthrepo.py - repo synthesis
2 #
2 #
3 # Copyright 2012 Facebook
3 # Copyright 2012 Facebook
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 '''synthesize structurally interesting change history
8 '''synthesize structurally interesting change history
9
9
10 This extension is useful for creating a repository with properties
10 This extension is useful for creating a repository with properties
11 that are statistically similar to an existing repository. During
11 that are statistically similar to an existing repository. During
12 analysis, a simple probability table is constructed from the history
12 analysis, a simple probability table is constructed from the history
13 of an existing repository. During synthesis, these properties are
13 of an existing repository. During synthesis, these properties are
14 reconstructed.
14 reconstructed.
15
15
16 Properties that are analyzed and synthesized include the following:
16 Properties that are analyzed and synthesized include the following:
17
17
18 - Lines added or removed when an existing file is modified
18 - Lines added or removed when an existing file is modified
19 - Number and sizes of files added
19 - Number and sizes of files added
20 - Number of files removed
20 - Number of files removed
21 - Line lengths
21 - Line lengths
22 - Topological distance to parent changeset(s)
22 - Topological distance to parent changeset(s)
23 - Probability of a commit being a merge
23 - Probability of a commit being a merge
24 - Probability of a newly added file being added to a new directory
24 - Probability of a newly added file being added to a new directory
25 - Interarrival time, and time zone, of commits
25 - Interarrival time, and time zone, of commits
26 - Number of files in each directory
26 - Number of files in each directory
27
27
28 A few obvious properties that are not currently handled realistically:
28 A few obvious properties that are not currently handled realistically:
29
29
30 - Merges are treated as regular commits with two parents, which is not
30 - Merges are treated as regular commits with two parents, which is not
31 realistic
31 realistic
32 - Modifications are not treated as operations on hunks of lines, but
32 - Modifications are not treated as operations on hunks of lines, but
33 as insertions and deletions of randomly chosen single lines
33 as insertions and deletions of randomly chosen single lines
34 - Committer ID (always random)
34 - Committer ID (always random)
35 - Executability of files
35 - Executability of files
36 - Symlinks and binary files are ignored
36 - Symlinks and binary files are ignored
37 '''
37 '''
38
38
39 from __future__ import absolute_import
39 from __future__ import absolute_import
40 import bisect
40 import bisect
41 import collections
41 import collections
42 import itertools
42 import itertools
43 import json
43 import json
44 import os
44 import os
45 import random
45 import random
46 import sys
46 import sys
47 import time
47 import time
48
48
49 from mercurial.i18n import _
49 from mercurial.i18n import _
50 from mercurial.node import (
50 from mercurial.node import (
51 nullid,
51 nullid,
52 nullrev,
52 nullrev,
53 short,
53 short,
54 )
54 )
55 from mercurial import (
55 from mercurial import (
56 cmdutil,
56 cmdutil,
57 context,
57 context,
58 error,
58 error,
59 hg,
59 hg,
60 patch,
60 patch,
61 scmutil,
61 scmutil,
62 util,
62 util,
63 )
63 )
64
64
65 # Note for extension authors: ONLY specify testedwith = 'internal' for
65 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
66 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
66 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
67 # be specifying the version(s) of Mercurial they are tested with, or
67 # be specifying the version(s) of Mercurial they are tested with, or
68 # leave the attribute unspecified.
68 # leave the attribute unspecified.
69 testedwith = 'internal'
69 testedwith = 'ships-with-hg-core'
70
70
71 cmdtable = {}
71 cmdtable = {}
72 command = cmdutil.command(cmdtable)
72 command = cmdutil.command(cmdtable)
73
73
74 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
74 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
75
75
76 def zerodict():
76 def zerodict():
77 return collections.defaultdict(lambda: 0)
77 return collections.defaultdict(lambda: 0)
78
78
79 def roundto(x, k):
79 def roundto(x, k):
80 if x > k * 2:
80 if x > k * 2:
81 return int(round(x / float(k)) * k)
81 return int(round(x / float(k)) * k)
82 return int(round(x))
82 return int(round(x))
83
83
84 def parsegitdiff(lines):
84 def parsegitdiff(lines):
85 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
85 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
86 binary = False
86 binary = False
87 for line in lines:
87 for line in lines:
88 start = line[:6]
88 start = line[:6]
89 if start == 'diff -':
89 if start == 'diff -':
90 if filename:
90 if filename:
91 yield filename, mar, lineadd, lineremove, binary
91 yield filename, mar, lineadd, lineremove, binary
92 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
92 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
93 filename = patch.gitre.match(line).group(1)
93 filename = patch.gitre.match(line).group(1)
94 elif start in newfile:
94 elif start in newfile:
95 mar = 'a'
95 mar = 'a'
96 elif start == 'GIT bi':
96 elif start == 'GIT bi':
97 binary = True
97 binary = True
98 elif start == 'delete':
98 elif start == 'delete':
99 mar = 'r'
99 mar = 'r'
100 elif start:
100 elif start:
101 s = start[0]
101 s = start[0]
102 if s == '-' and not line.startswith('--- '):
102 if s == '-' and not line.startswith('--- '):
103 lineremove += 1
103 lineremove += 1
104 elif s == '+' and not line.startswith('+++ '):
104 elif s == '+' and not line.startswith('+++ '):
105 lineadd[roundto(len(line) - 1, 5)] += 1
105 lineadd[roundto(len(line) - 1, 5)] += 1
106 if filename:
106 if filename:
107 yield filename, mar, lineadd, lineremove, binary
107 yield filename, mar, lineadd, lineremove, binary
108
108
109 @command('analyze',
109 @command('analyze',
110 [('o', 'output', '', _('write output to given file'), _('FILE')),
110 [('o', 'output', '', _('write output to given file'), _('FILE')),
111 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
111 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
112 _('hg analyze'), optionalrepo=True)
112 _('hg analyze'), optionalrepo=True)
113 def analyze(ui, repo, *revs, **opts):
113 def analyze(ui, repo, *revs, **opts):
114 '''create a simple model of a repository to use for later synthesis
114 '''create a simple model of a repository to use for later synthesis
115
115
116 This command examines every changeset in the given range (or all
116 This command examines every changeset in the given range (or all
117 of history if none are specified) and creates a simple statistical
117 of history if none are specified) and creates a simple statistical
118 model of the history of the repository. It also measures the directory
118 model of the history of the repository. It also measures the directory
119 structure of the repository as checked out.
119 structure of the repository as checked out.
120
120
121 The model is written out to a JSON file, and can be used by
121 The model is written out to a JSON file, and can be used by
122 :hg:`synthesize` to create or augment a repository with synthetic
122 :hg:`synthesize` to create or augment a repository with synthetic
123 commits that have a structure that is statistically similar to the
123 commits that have a structure that is statistically similar to the
124 analyzed repository.
124 analyzed repository.
125 '''
125 '''
126 root = repo.root
126 root = repo.root
127 if not root.endswith(os.path.sep):
127 if not root.endswith(os.path.sep):
128 root += os.path.sep
128 root += os.path.sep
129
129
130 revs = list(revs)
130 revs = list(revs)
131 revs.extend(opts['rev'])
131 revs.extend(opts['rev'])
132 if not revs:
132 if not revs:
133 revs = [':']
133 revs = [':']
134
134
135 output = opts['output']
135 output = opts['output']
136 if not output:
136 if not output:
137 output = os.path.basename(root) + '.json'
137 output = os.path.basename(root) + '.json'
138
138
139 if output == '-':
139 if output == '-':
140 fp = sys.stdout
140 fp = sys.stdout
141 else:
141 else:
142 fp = open(output, 'w')
142 fp = open(output, 'w')
143
143
144 # Always obtain file counts of each directory in the given root directory.
144 # Always obtain file counts of each directory in the given root directory.
145 def onerror(e):
145 def onerror(e):
146 ui.warn(_('error walking directory structure: %s\n') % e)
146 ui.warn(_('error walking directory structure: %s\n') % e)
147
147
148 dirs = {}
148 dirs = {}
149 rootprefixlen = len(root)
149 rootprefixlen = len(root)
150 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
150 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
151 dirpathfromroot = dirpath[rootprefixlen:]
151 dirpathfromroot = dirpath[rootprefixlen:]
152 dirs[dirpathfromroot] = len(filenames)
152 dirs[dirpathfromroot] = len(filenames)
153 if '.hg' in dirnames:
153 if '.hg' in dirnames:
154 dirnames.remove('.hg')
154 dirnames.remove('.hg')
155
155
156 lineschanged = zerodict()
156 lineschanged = zerodict()
157 children = zerodict()
157 children = zerodict()
158 p1distance = zerodict()
158 p1distance = zerodict()
159 p2distance = zerodict()
159 p2distance = zerodict()
160 linesinfilesadded = zerodict()
160 linesinfilesadded = zerodict()
161 fileschanged = zerodict()
161 fileschanged = zerodict()
162 filesadded = zerodict()
162 filesadded = zerodict()
163 filesremoved = zerodict()
163 filesremoved = zerodict()
164 linelengths = zerodict()
164 linelengths = zerodict()
165 interarrival = zerodict()
165 interarrival = zerodict()
166 parents = zerodict()
166 parents = zerodict()
167 dirsadded = zerodict()
167 dirsadded = zerodict()
168 tzoffset = zerodict()
168 tzoffset = zerodict()
169
169
170 # If a mercurial repo is available, also model the commit history.
170 # If a mercurial repo is available, also model the commit history.
171 if repo:
171 if repo:
172 revs = scmutil.revrange(repo, revs)
172 revs = scmutil.revrange(repo, revs)
173 revs.sort()
173 revs.sort()
174
174
175 progress = ui.progress
175 progress = ui.progress
176 _analyzing = _('analyzing')
176 _analyzing = _('analyzing')
177 _changesets = _('changesets')
177 _changesets = _('changesets')
178 _total = len(revs)
178 _total = len(revs)
179
179
180 for i, rev in enumerate(revs):
180 for i, rev in enumerate(revs):
181 progress(_analyzing, i, unit=_changesets, total=_total)
181 progress(_analyzing, i, unit=_changesets, total=_total)
182 ctx = repo[rev]
182 ctx = repo[rev]
183 pl = ctx.parents()
183 pl = ctx.parents()
184 pctx = pl[0]
184 pctx = pl[0]
185 prev = pctx.rev()
185 prev = pctx.rev()
186 children[prev] += 1
186 children[prev] += 1
187 p1distance[rev - prev] += 1
187 p1distance[rev - prev] += 1
188 parents[len(pl)] += 1
188 parents[len(pl)] += 1
189 tzoffset[ctx.date()[1]] += 1
189 tzoffset[ctx.date()[1]] += 1
190 if len(pl) > 1:
190 if len(pl) > 1:
191 p2distance[rev - pl[1].rev()] += 1
191 p2distance[rev - pl[1].rev()] += 1
192 if prev == rev - 1:
192 if prev == rev - 1:
193 lastctx = pctx
193 lastctx = pctx
194 else:
194 else:
195 lastctx = repo[rev - 1]
195 lastctx = repo[rev - 1]
196 if lastctx.rev() != nullrev:
196 if lastctx.rev() != nullrev:
197 timedelta = ctx.date()[0] - lastctx.date()[0]
197 timedelta = ctx.date()[0] - lastctx.date()[0]
198 interarrival[roundto(timedelta, 300)] += 1
198 interarrival[roundto(timedelta, 300)] += 1
199 diff = sum((d.splitlines() for d in ctx.diff(pctx, git=True)), [])
199 diff = sum((d.splitlines() for d in ctx.diff(pctx, git=True)), [])
200 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
200 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
201 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
201 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
202 if isbin:
202 if isbin:
203 continue
203 continue
204 added = sum(lineadd.itervalues(), 0)
204 added = sum(lineadd.itervalues(), 0)
205 if mar == 'm':
205 if mar == 'm':
206 if added and lineremove:
206 if added and lineremove:
207 lineschanged[roundto(added, 5),
207 lineschanged[roundto(added, 5),
208 roundto(lineremove, 5)] += 1
208 roundto(lineremove, 5)] += 1
209 filechanges += 1
209 filechanges += 1
210 elif mar == 'a':
210 elif mar == 'a':
211 fileadds += 1
211 fileadds += 1
212 if '/' in filename:
212 if '/' in filename:
213 filedir = filename.rsplit('/', 1)[0]
213 filedir = filename.rsplit('/', 1)[0]
214 if filedir not in pctx.dirs():
214 if filedir not in pctx.dirs():
215 diradds += 1
215 diradds += 1
216 linesinfilesadded[roundto(added, 5)] += 1
216 linesinfilesadded[roundto(added, 5)] += 1
217 elif mar == 'r':
217 elif mar == 'r':
218 fileremoves += 1
218 fileremoves += 1
219 for length, count in lineadd.iteritems():
219 for length, count in lineadd.iteritems():
220 linelengths[length] += count
220 linelengths[length] += count
221 fileschanged[filechanges] += 1
221 fileschanged[filechanges] += 1
222 filesadded[fileadds] += 1
222 filesadded[fileadds] += 1
223 dirsadded[diradds] += 1
223 dirsadded[diradds] += 1
224 filesremoved[fileremoves] += 1
224 filesremoved[fileremoves] += 1
225
225
226 invchildren = zerodict()
226 invchildren = zerodict()
227
227
228 for rev, count in children.iteritems():
228 for rev, count in children.iteritems():
229 invchildren[count] += 1
229 invchildren[count] += 1
230
230
231 if output != '-':
231 if output != '-':
232 ui.status(_('writing output to %s\n') % output)
232 ui.status(_('writing output to %s\n') % output)
233
233
234 def pronk(d):
234 def pronk(d):
235 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
235 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
236
236
237 json.dump({'revs': len(revs),
237 json.dump({'revs': len(revs),
238 'initdirs': pronk(dirs),
238 'initdirs': pronk(dirs),
239 'lineschanged': pronk(lineschanged),
239 'lineschanged': pronk(lineschanged),
240 'children': pronk(invchildren),
240 'children': pronk(invchildren),
241 'fileschanged': pronk(fileschanged),
241 'fileschanged': pronk(fileschanged),
242 'filesadded': pronk(filesadded),
242 'filesadded': pronk(filesadded),
243 'linesinfilesadded': pronk(linesinfilesadded),
243 'linesinfilesadded': pronk(linesinfilesadded),
244 'dirsadded': pronk(dirsadded),
244 'dirsadded': pronk(dirsadded),
245 'filesremoved': pronk(filesremoved),
245 'filesremoved': pronk(filesremoved),
246 'linelengths': pronk(linelengths),
246 'linelengths': pronk(linelengths),
247 'parents': pronk(parents),
247 'parents': pronk(parents),
248 'p1distance': pronk(p1distance),
248 'p1distance': pronk(p1distance),
249 'p2distance': pronk(p2distance),
249 'p2distance': pronk(p2distance),
250 'interarrival': pronk(interarrival),
250 'interarrival': pronk(interarrival),
251 'tzoffset': pronk(tzoffset),
251 'tzoffset': pronk(tzoffset),
252 },
252 },
253 fp)
253 fp)
254 fp.close()
254 fp.close()
255
255
256 @command('synthesize',
256 @command('synthesize',
257 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
257 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
258 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
258 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
259 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
259 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
260 _('hg synthesize [OPTION].. DESCFILE'))
260 _('hg synthesize [OPTION].. DESCFILE'))
261 def synthesize(ui, repo, descpath, **opts):
261 def synthesize(ui, repo, descpath, **opts):
262 '''synthesize commits based on a model of an existing repository
262 '''synthesize commits based on a model of an existing repository
263
263
264 The model must have been generated by :hg:`analyze`. Commits will
264 The model must have been generated by :hg:`analyze`. Commits will
265 be generated randomly according to the probabilities described in
265 be generated randomly according to the probabilities described in
266 the model. If --initfiles is set, the repository will be seeded with
266 the model. If --initfiles is set, the repository will be seeded with
267 the given number files following the modeled repository's directory
267 the given number files following the modeled repository's directory
268 structure.
268 structure.
269
269
270 When synthesizing new content, commit descriptions, and user
270 When synthesizing new content, commit descriptions, and user
271 names, words will be chosen randomly from a dictionary that is
271 names, words will be chosen randomly from a dictionary that is
272 presumed to contain one word per line. Use --dict to specify the
272 presumed to contain one word per line. Use --dict to specify the
273 path to an alternate dictionary to use.
273 path to an alternate dictionary to use.
274 '''
274 '''
275 try:
275 try:
276 fp = hg.openpath(ui, descpath)
276 fp = hg.openpath(ui, descpath)
277 except Exception as err:
277 except Exception as err:
278 raise error.Abort('%s: %s' % (descpath, err[0].strerror))
278 raise error.Abort('%s: %s' % (descpath, err[0].strerror))
279 desc = json.load(fp)
279 desc = json.load(fp)
280 fp.close()
280 fp.close()
281
281
282 def cdf(l):
282 def cdf(l):
283 if not l:
283 if not l:
284 return [], []
284 return [], []
285 vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True))
285 vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True))
286 t = float(sum(probs, 0))
286 t = float(sum(probs, 0))
287 s, cdfs = 0, []
287 s, cdfs = 0, []
288 for v in probs:
288 for v in probs:
289 s += v
289 s += v
290 cdfs.append(s / t)
290 cdfs.append(s / t)
291 return vals, cdfs
291 return vals, cdfs
292
292
293 lineschanged = cdf(desc['lineschanged'])
293 lineschanged = cdf(desc['lineschanged'])
294 fileschanged = cdf(desc['fileschanged'])
294 fileschanged = cdf(desc['fileschanged'])
295 filesadded = cdf(desc['filesadded'])
295 filesadded = cdf(desc['filesadded'])
296 dirsadded = cdf(desc['dirsadded'])
296 dirsadded = cdf(desc['dirsadded'])
297 filesremoved = cdf(desc['filesremoved'])
297 filesremoved = cdf(desc['filesremoved'])
298 linelengths = cdf(desc['linelengths'])
298 linelengths = cdf(desc['linelengths'])
299 parents = cdf(desc['parents'])
299 parents = cdf(desc['parents'])
300 p1distance = cdf(desc['p1distance'])
300 p1distance = cdf(desc['p1distance'])
301 p2distance = cdf(desc['p2distance'])
301 p2distance = cdf(desc['p2distance'])
302 interarrival = cdf(desc['interarrival'])
302 interarrival = cdf(desc['interarrival'])
303 linesinfilesadded = cdf(desc['linesinfilesadded'])
303 linesinfilesadded = cdf(desc['linesinfilesadded'])
304 tzoffset = cdf(desc['tzoffset'])
304 tzoffset = cdf(desc['tzoffset'])
305
305
306 dictfile = opts.get('dict') or '/usr/share/dict/words'
306 dictfile = opts.get('dict') or '/usr/share/dict/words'
307 try:
307 try:
308 fp = open(dictfile, 'rU')
308 fp = open(dictfile, 'rU')
309 except IOError as err:
309 except IOError as err:
310 raise error.Abort('%s: %s' % (dictfile, err.strerror))
310 raise error.Abort('%s: %s' % (dictfile, err.strerror))
311 words = fp.read().splitlines()
311 words = fp.read().splitlines()
312 fp.close()
312 fp.close()
313
313
314 initdirs = {}
314 initdirs = {}
315 if desc['initdirs']:
315 if desc['initdirs']:
316 for k, v in desc['initdirs']:
316 for k, v in desc['initdirs']:
317 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
317 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
318 initdirs = renamedirs(initdirs, words)
318 initdirs = renamedirs(initdirs, words)
319 initdirscdf = cdf(initdirs)
319 initdirscdf = cdf(initdirs)
320
320
321 def pick(cdf):
321 def pick(cdf):
322 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
322 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
323
323
324 def pickpath():
324 def pickpath():
325 return os.path.join(pick(initdirscdf), random.choice(words))
325 return os.path.join(pick(initdirscdf), random.choice(words))
326
326
327 def makeline(minimum=0):
327 def makeline(minimum=0):
328 total = max(minimum, pick(linelengths))
328 total = max(minimum, pick(linelengths))
329 c, l = 0, []
329 c, l = 0, []
330 while c < total:
330 while c < total:
331 w = random.choice(words)
331 w = random.choice(words)
332 c += len(w) + 1
332 c += len(w) + 1
333 l.append(w)
333 l.append(w)
334 return ' '.join(l)
334 return ' '.join(l)
335
335
336 wlock = repo.wlock()
336 wlock = repo.wlock()
337 lock = repo.lock()
337 lock = repo.lock()
338
338
339 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
339 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
340
340
341 progress = ui.progress
341 progress = ui.progress
342 _synthesizing = _('synthesizing')
342 _synthesizing = _('synthesizing')
343 _files = _('initial files')
343 _files = _('initial files')
344 _changesets = _('changesets')
344 _changesets = _('changesets')
345
345
346 # Synthesize a single initial revision adding files to the repo according
346 # Synthesize a single initial revision adding files to the repo according
347 # to the modeled directory structure.
347 # to the modeled directory structure.
348 initcount = int(opts['initfiles'])
348 initcount = int(opts['initfiles'])
349 if initcount and initdirs:
349 if initcount and initdirs:
350 pctx = repo[None].parents()[0]
350 pctx = repo[None].parents()[0]
351 dirs = set(pctx.dirs())
351 dirs = set(pctx.dirs())
352 files = {}
352 files = {}
353
353
354 def validpath(path):
354 def validpath(path):
355 # Don't pick filenames which are already directory names.
355 # Don't pick filenames which are already directory names.
356 if path in dirs:
356 if path in dirs:
357 return False
357 return False
358 # Don't pick directories which were used as file names.
358 # Don't pick directories which were used as file names.
359 while path:
359 while path:
360 if path in files:
360 if path in files:
361 return False
361 return False
362 path = os.path.dirname(path)
362 path = os.path.dirname(path)
363 return True
363 return True
364
364
365 for i in xrange(0, initcount):
365 for i in xrange(0, initcount):
366 ui.progress(_synthesizing, i, unit=_files, total=initcount)
366 ui.progress(_synthesizing, i, unit=_files, total=initcount)
367
367
368 path = pickpath()
368 path = pickpath()
369 while not validpath(path):
369 while not validpath(path):
370 path = pickpath()
370 path = pickpath()
371 data = '%s contents\n' % path
371 data = '%s contents\n' % path
372 files[path] = context.memfilectx(repo, path, data)
372 files[path] = context.memfilectx(repo, path, data)
373 dir = os.path.dirname(path)
373 dir = os.path.dirname(path)
374 while dir and dir not in dirs:
374 while dir and dir not in dirs:
375 dirs.add(dir)
375 dirs.add(dir)
376 dir = os.path.dirname(dir)
376 dir = os.path.dirname(dir)
377
377
378 def filectxfn(repo, memctx, path):
378 def filectxfn(repo, memctx, path):
379 return files[path]
379 return files[path]
380
380
381 ui.progress(_synthesizing, None)
381 ui.progress(_synthesizing, None)
382 message = 'synthesized wide repo with %d files' % (len(files),)
382 message = 'synthesized wide repo with %d files' % (len(files),)
383 mc = context.memctx(repo, [pctx.node(), nullid], message,
383 mc = context.memctx(repo, [pctx.node(), nullid], message,
384 files.iterkeys(), filectxfn, ui.username(),
384 files.iterkeys(), filectxfn, ui.username(),
385 '%d %d' % util.makedate())
385 '%d %d' % util.makedate())
386 initnode = mc.commit()
386 initnode = mc.commit()
387 if ui.debugflag:
387 if ui.debugflag:
388 hexfn = hex
388 hexfn = hex
389 else:
389 else:
390 hexfn = short
390 hexfn = short
391 ui.status(_('added commit %s with %d files\n')
391 ui.status(_('added commit %s with %d files\n')
392 % (hexfn(initnode), len(files)))
392 % (hexfn(initnode), len(files)))
393
393
394 # Synthesize incremental revisions to the repository, adding repo depth.
394 # Synthesize incremental revisions to the repository, adding repo depth.
395 count = int(opts['count'])
395 count = int(opts['count'])
396 heads = set(map(repo.changelog.rev, repo.heads()))
396 heads = set(map(repo.changelog.rev, repo.heads()))
397 for i in xrange(count):
397 for i in xrange(count):
398 progress(_synthesizing, i, unit=_changesets, total=count)
398 progress(_synthesizing, i, unit=_changesets, total=count)
399
399
400 node = repo.changelog.node
400 node = repo.changelog.node
401 revs = len(repo)
401 revs = len(repo)
402
402
403 def pickhead(heads, distance):
403 def pickhead(heads, distance):
404 if heads:
404 if heads:
405 lheads = sorted(heads)
405 lheads = sorted(heads)
406 rev = revs - min(pick(distance), revs)
406 rev = revs - min(pick(distance), revs)
407 if rev < lheads[-1]:
407 if rev < lheads[-1]:
408 rev = lheads[bisect.bisect_left(lheads, rev)]
408 rev = lheads[bisect.bisect_left(lheads, rev)]
409 else:
409 else:
410 rev = lheads[-1]
410 rev = lheads[-1]
411 return rev, node(rev)
411 return rev, node(rev)
412 return nullrev, nullid
412 return nullrev, nullid
413
413
414 r1 = revs - min(pick(p1distance), revs)
414 r1 = revs - min(pick(p1distance), revs)
415 p1 = node(r1)
415 p1 = node(r1)
416
416
417 # the number of heads will grow without bound if we use a pure
417 # the number of heads will grow without bound if we use a pure
418 # model, so artificially constrain their proliferation
418 # model, so artificially constrain their proliferation
419 toomanyheads = len(heads) > random.randint(1, 20)
419 toomanyheads = len(heads) > random.randint(1, 20)
420 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
420 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
421 r2, p2 = pickhead(heads.difference([r1]), p2distance)
421 r2, p2 = pickhead(heads.difference([r1]), p2distance)
422 else:
422 else:
423 r2, p2 = nullrev, nullid
423 r2, p2 = nullrev, nullid
424
424
425 pl = [p1, p2]
425 pl = [p1, p2]
426 pctx = repo[r1]
426 pctx = repo[r1]
427 mf = pctx.manifest()
427 mf = pctx.manifest()
428 mfk = mf.keys()
428 mfk = mf.keys()
429 changes = {}
429 changes = {}
430 if mfk:
430 if mfk:
431 for __ in xrange(pick(fileschanged)):
431 for __ in xrange(pick(fileschanged)):
432 for __ in xrange(10):
432 for __ in xrange(10):
433 fctx = pctx.filectx(random.choice(mfk))
433 fctx = pctx.filectx(random.choice(mfk))
434 path = fctx.path()
434 path = fctx.path()
435 if not (path in nevertouch or fctx.isbinary() or
435 if not (path in nevertouch or fctx.isbinary() or
436 'l' in fctx.flags()):
436 'l' in fctx.flags()):
437 break
437 break
438 lines = fctx.data().splitlines()
438 lines = fctx.data().splitlines()
439 add, remove = pick(lineschanged)
439 add, remove = pick(lineschanged)
440 for __ in xrange(remove):
440 for __ in xrange(remove):
441 if not lines:
441 if not lines:
442 break
442 break
443 del lines[random.randrange(0, len(lines))]
443 del lines[random.randrange(0, len(lines))]
444 for __ in xrange(add):
444 for __ in xrange(add):
445 lines.insert(random.randint(0, len(lines)), makeline())
445 lines.insert(random.randint(0, len(lines)), makeline())
446 path = fctx.path()
446 path = fctx.path()
447 changes[path] = context.memfilectx(repo, path,
447 changes[path] = context.memfilectx(repo, path,
448 '\n'.join(lines) + '\n')
448 '\n'.join(lines) + '\n')
449 for __ in xrange(pick(filesremoved)):
449 for __ in xrange(pick(filesremoved)):
450 path = random.choice(mfk)
450 path = random.choice(mfk)
451 for __ in xrange(10):
451 for __ in xrange(10):
452 path = random.choice(mfk)
452 path = random.choice(mfk)
453 if path not in changes:
453 if path not in changes:
454 changes[path] = None
454 changes[path] = None
455 break
455 break
456 if filesadded:
456 if filesadded:
457 dirs = list(pctx.dirs())
457 dirs = list(pctx.dirs())
458 dirs.insert(0, '')
458 dirs.insert(0, '')
459 for __ in xrange(pick(filesadded)):
459 for __ in xrange(pick(filesadded)):
460 pathstr = ''
460 pathstr = ''
461 while pathstr in dirs:
461 while pathstr in dirs:
462 path = [random.choice(dirs)]
462 path = [random.choice(dirs)]
463 if pick(dirsadded):
463 if pick(dirsadded):
464 path.append(random.choice(words))
464 path.append(random.choice(words))
465 path.append(random.choice(words))
465 path.append(random.choice(words))
466 pathstr = '/'.join(filter(None, path))
466 pathstr = '/'.join(filter(None, path))
467 data = '\n'.join(makeline()
467 data = '\n'.join(makeline()
468 for __ in xrange(pick(linesinfilesadded))) + '\n'
468 for __ in xrange(pick(linesinfilesadded))) + '\n'
469 changes[pathstr] = context.memfilectx(repo, pathstr, data)
469 changes[pathstr] = context.memfilectx(repo, pathstr, data)
470 def filectxfn(repo, memctx, path):
470 def filectxfn(repo, memctx, path):
471 return changes[path]
471 return changes[path]
472 if not changes:
472 if not changes:
473 continue
473 continue
474 if revs:
474 if revs:
475 date = repo['tip'].date()[0] + pick(interarrival)
475 date = repo['tip'].date()[0] + pick(interarrival)
476 else:
476 else:
477 date = time.time() - (86400 * count)
477 date = time.time() - (86400 * count)
478 # dates in mercurial must be positive, fit in 32-bit signed integers.
478 # dates in mercurial must be positive, fit in 32-bit signed integers.
479 date = min(0x7fffffff, max(0, date))
479 date = min(0x7fffffff, max(0, date))
480 user = random.choice(words) + '@' + random.choice(words)
480 user = random.choice(words) + '@' + random.choice(words)
481 mc = context.memctx(repo, pl, makeline(minimum=2),
481 mc = context.memctx(repo, pl, makeline(minimum=2),
482 sorted(changes.iterkeys()),
482 sorted(changes.iterkeys()),
483 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
483 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
484 newnode = mc.commit()
484 newnode = mc.commit()
485 heads.add(repo.changelog.rev(newnode))
485 heads.add(repo.changelog.rev(newnode))
486 heads.discard(r1)
486 heads.discard(r1)
487 heads.discard(r2)
487 heads.discard(r2)
488
488
489 lock.release()
489 lock.release()
490 wlock.release()
490 wlock.release()
491
491
492 def renamedirs(dirs, words):
492 def renamedirs(dirs, words):
493 '''Randomly rename the directory names in the per-dir file count dict.'''
493 '''Randomly rename the directory names in the per-dir file count dict.'''
494 wordgen = itertools.cycle(words)
494 wordgen = itertools.cycle(words)
495 replacements = {'': ''}
495 replacements = {'': ''}
496 def rename(dirpath):
496 def rename(dirpath):
497 '''Recursively rename the directory and all path prefixes.
497 '''Recursively rename the directory and all path prefixes.
498
498
499 The mapping from path to renamed path is stored for all path prefixes
499 The mapping from path to renamed path is stored for all path prefixes
500 as in dynamic programming, ensuring linear runtime and consistent
500 as in dynamic programming, ensuring linear runtime and consistent
501 renaming regardless of iteration order through the model.
501 renaming regardless of iteration order through the model.
502 '''
502 '''
503 if dirpath in replacements:
503 if dirpath in replacements:
504 return replacements[dirpath]
504 return replacements[dirpath]
505 head, _ = os.path.split(dirpath)
505 head, _ = os.path.split(dirpath)
506 if head:
506 if head:
507 head = rename(head)
507 head = rename(head)
508 else:
508 else:
509 head = ''
509 head = ''
510 renamed = os.path.join(head, next(wordgen))
510 renamed = os.path.join(head, next(wordgen))
511 replacements[dirpath] = renamed
511 replacements[dirpath] = renamed
512 return renamed
512 return renamed
513 result = []
513 result = []
514 for dirpath, count in dirs.iteritems():
514 for dirpath, count in dirs.iteritems():
515 result.append([rename(dirpath.lstrip(os.sep)), count])
515 result.append([rename(dirpath.lstrip(os.sep)), count])
516 return result
516 return result
@@ -1,52 +1,53 b''
1 <?xml version="1.0" encoding="utf-8"?>
1 <?xml version="1.0" encoding="utf-8"?>
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3
3
4 <?include guids.wxi ?>
4 <?include guids.wxi ?>
5 <?include defines.wxi ?>
5 <?include defines.wxi ?>
6
6
7 <Fragment>
7 <Fragment>
8 <ComponentGroup Id='helpFolder'>
8 <ComponentGroup Id='helpFolder'>
9 <ComponentRef Id='help.root' />
9 <ComponentRef Id='help.root' />
10 <ComponentRef Id='help.internals' />
10 <ComponentRef Id='help.internals' />
11 </ComponentGroup>
11 </ComponentGroup>
12 </Fragment>
12 </Fragment>
13
13
14 <Fragment>
14 <Fragment>
15 <DirectoryRef Id="INSTALLDIR">
15 <DirectoryRef Id="INSTALLDIR">
16 <Directory Id="helpdir" Name="help" FileSource="$(var.SourceDir)">
16 <Directory Id="helpdir" Name="help" FileSource="$(var.SourceDir)">
17 <Component Id="help.root" Guid="$(var.help.root.guid)" Win64='$(var.IsX64)'>
17 <Component Id="help.root" Guid="$(var.help.root.guid)" Win64='$(var.IsX64)'>
18 <File Name="config.txt" KeyPath="yes" />
18 <File Name="config.txt" KeyPath="yes" />
19 <File Name="dates.txt" />
19 <File Name="dates.txt" />
20 <File Name="diffs.txt" />
20 <File Name="diffs.txt" />
21 <File Name="environment.txt" />
21 <File Name="environment.txt" />
22 <File Name="extensions.txt" />
22 <File Name="extensions.txt" />
23 <File Name="filesets.txt" />
23 <File Name="filesets.txt" />
24 <File Name="glossary.txt" />
24 <File Name="glossary.txt" />
25 <File Name="hgignore.txt" />
25 <File Name="hgignore.txt" />
26 <File Name="hgweb.txt" />
26 <File Name="hgweb.txt" />
27 <File Name="merge-tools.txt" />
27 <File Name="merge-tools.txt" />
28 <File Name="multirevs.txt" />
28 <File Name="multirevs.txt" />
29 <File Name="patterns.txt" />
29 <File Name="patterns.txt" />
30 <File Name="phases.txt" />
30 <File Name="phases.txt" />
31 <File Name="revisions.txt" />
31 <File Name="revisions.txt" />
32 <File Name="revsets.txt" />
32 <File Name="revsets.txt" />
33 <File Name="scripting.txt" />
33 <File Name="scripting.txt" />
34 <File Name="subrepos.txt" />
34 <File Name="subrepos.txt" />
35 <File Name="templates.txt" />
35 <File Name="templates.txt" />
36 <File Name="urls.txt" />
36 <File Name="urls.txt" />
37 </Component>
37 </Component>
38
38
39 <Directory Id="help.internaldir" Name="internals">
39 <Directory Id="help.internaldir" Name="internals">
40 <Component Id="help.internals" Guid="$(var.help.internals.guid)" Win64='$(var.IsX64)'>
40 <Component Id="help.internals" Guid="$(var.help.internals.guid)" Win64='$(var.IsX64)'>
41 <File Id="internals.bundles.txt" Name="bundles.txt" KeyPath="yes" />
41 <File Id="internals.bundles.txt" Name="bundles.txt" KeyPath="yes" />
42 <File Id="internals.changegroups.txt" Name="changegroups.txt" />
42 <File Id="internals.changegroups.txt" Name="changegroups.txt" />
43 <File Id="internals.requirements.txt" Name="requirements.txt" />
43 <File Id="internals.requirements.txt" Name="requirements.txt" />
44 <File Id="internals.revlogs.txt" Name="revlogs.txt" />
44 <File Id="internals.revlogs.txt" Name="revlogs.txt" />
45 <File Id="internals.wireprotocol.txt" Name="wireprotocol.txt" />
45 </Component>
46 </Component>
46 </Directory>
47 </Directory>
47
48
48 </Directory>
49 </Directory>
49 </DirectoryRef>
50 </DirectoryRef>
50 </Fragment>
51 </Fragment>
51
52
52 </Wix>
53 </Wix>
@@ -1,1222 +1,1222 b''
1 #compdef hg
1 #compdef hg
2
2
3 # Zsh completion script for mercurial. Rename this file to _hg and copy
3 # Zsh completion script for mercurial. Rename this file to _hg and copy
4 # it into your zsh function path (/usr/share/zsh/site-functions for
4 # it into your zsh function path (/usr/share/zsh/site-functions for
5 # instance)
5 # instance)
6 #
6 #
7 # If you do not want to install it globally, you can copy it somewhere
7 # If you do not want to install it globally, you can copy it somewhere
8 # else and add that directory to $fpath. This must be done before
8 # else and add that directory to $fpath. This must be done before
9 # compinit is called. If the file is copied to ~/.zsh.d, your ~/.zshrc
9 # compinit is called. If the file is copied to ~/.zsh.d, your ~/.zshrc
10 # file could look like this:
10 # file could look like this:
11 #
11 #
12 # fpath=("$HOME/.zsh.d" $fpath)
12 # fpath=("$HOME/.zsh.d" $fpath)
13 # autoload -U compinit
13 # autoload -U compinit
14 # compinit
14 # compinit
15 #
15 #
16 # Copyright (C) 2005, 2006 Steve Borho <steve@borho.org>
16 # Copyright (C) 2005, 2006 Steve Borho <steve@borho.org>
17 # Copyright (C) 2006-10 Brendan Cully <brendan@kublai.com>
17 # Copyright (C) 2006-10 Brendan Cully <brendan@kublai.com>
18 #
18 #
19 # Permission is hereby granted, without written agreement and without
19 # Permission is hereby granted, without written agreement and without
20 # licence or royalty fees, to use, copy, modify, and distribute this
20 # licence or royalty fees, to use, copy, modify, and distribute this
21 # software and to distribute modified versions of this software for any
21 # software and to distribute modified versions of this software for any
22 # purpose, provided that the above copyright notice and the following
22 # purpose, provided that the above copyright notice and the following
23 # two paragraphs appear in all copies of this software.
23 # two paragraphs appear in all copies of this software.
24 #
24 #
25 # In no event shall the authors be liable to any party for direct,
25 # In no event shall the authors be liable to any party for direct,
26 # indirect, special, incidental, or consequential damages arising out of
26 # indirect, special, incidental, or consequential damages arising out of
27 # the use of this software and its documentation, even if the authors
27 # the use of this software and its documentation, even if the authors
28 # have been advised of the possibility of such damage.
28 # have been advised of the possibility of such damage.
29 #
29 #
30 # The authors specifically disclaim any warranties, including, but not
30 # The authors specifically disclaim any warranties, including, but not
31 # limited to, the implied warranties of merchantability and fitness for
31 # limited to, the implied warranties of merchantability and fitness for
32 # a particular purpose. The software provided hereunder is on an "as
32 # a particular purpose. The software provided hereunder is on an "as
33 # is" basis, and the authors have no obligation to provide maintenance,
33 # is" basis, and the authors have no obligation to provide maintenance,
34 # support, updates, enhancements, or modifications.
34 # support, updates, enhancements, or modifications.
35
35
36 emulate -LR zsh
36 emulate -LR zsh
37 setopt extendedglob
37 setopt extendedglob
38
38
39 local curcontext="$curcontext" state line
39 local curcontext="$curcontext" state line
40 typeset -A _hg_cmd_globals
40 typeset -A _hg_cmd_globals
41
41
42 _hg() {
42 _hg() {
43 local cmd _hg_root
43 local cmd _hg_root
44 integer i=2
44 integer i=2
45 _hg_cmd_globals=()
45 _hg_cmd_globals=()
46
46
47 while (( i < $#words ))
47 while (( i < $#words ))
48 do
48 do
49 case "$words[$i]" in
49 case "$words[$i]" in
50 -R|--repository)
50 -R|--repository)
51 eval _hg_root="$words[$i+1]"
51 eval _hg_root="$words[$i+1]"
52 _hg_cmd_globals+=("$words[$i]" "$_hg_root")
52 _hg_cmd_globals+=("$words[$i]" "$_hg_root")
53 (( i += 2 ))
53 (( i += 2 ))
54 continue
54 continue
55 ;;
55 ;;
56 -R*)
56 -R*)
57 _hg_cmd_globals+="$words[$i]"
57 _hg_cmd_globals+="$words[$i]"
58 eval _hg_root="${words[$i]#-R}"
58 eval _hg_root="${words[$i]#-R}"
59 (( i++ ))
59 (( i++ ))
60 continue
60 continue
61 ;;
61 ;;
62 --cwd|--config)
62 --cwd|--config)
63 # pass along arguments to hg completer
63 # pass along arguments to hg completer
64 _hg_cmd_globals+=("$words[$i]" "$words[$i+1]")
64 _hg_cmd_globals+=("$words[$i]" "$words[$i+1]")
65 (( i += 2 ))
65 (( i += 2 ))
66 continue
66 continue
67 ;;
67 ;;
68 -*)
68 -*)
69 # skip option
69 # skip option
70 (( i++ ))
70 (( i++ ))
71 continue
71 continue
72 ;;
72 ;;
73 esac
73 esac
74 if [[ -z "$cmd" ]]
74 if [[ -z "$cmd" ]]
75 then
75 then
76 cmd="$words[$i]"
76 cmd="$words[$i]"
77 words[$i]=()
77 words[$i]=()
78 (( CURRENT-- ))
78 (( CURRENT-- ))
79 fi
79 fi
80 (( i++ ))
80 (( i++ ))
81 done
81 done
82
82
83 if [[ -z "$cmd" ]]
83 if [[ -z "$cmd" ]]
84 then
84 then
85 _arguments -s -w : $_hg_global_opts \
85 _arguments -s -w : $_hg_global_opts \
86 ':mercurial command:_hg_commands'
86 ':mercurial command:_hg_commands'
87 return
87 return
88 fi
88 fi
89
89
90 # resolve abbreviations and aliases
90 # resolve abbreviations and aliases
91 if ! (( $+functions[_hg_cmd_${cmd}] ))
91 if ! (( $+functions[_hg_cmd_${cmd}] ))
92 then
92 then
93 local cmdexp
93 local cmdexp
94 (( $#_hg_cmd_list )) || _hg_get_commands
94 (( $#_hg_cmd_list )) || _hg_get_commands
95
95
96 cmdexp=$_hg_cmd_list[(r)${cmd}*]
96 cmdexp=$_hg_cmd_list[(r)${cmd}*]
97 if [[ $cmdexp == $_hg_cmd_list[(R)${cmd}*] ]]
97 if [[ $cmdexp == $_hg_cmd_list[(R)${cmd}*] ]]
98 then
98 then
99 # might be nice to rewrite the command line with the expansion
99 # might be nice to rewrite the command line with the expansion
100 cmd="$cmdexp"
100 cmd="$cmdexp"
101 fi
101 fi
102 if [[ -n $_hg_alias_list[$cmd] ]]
102 if [[ -n $_hg_alias_list[$cmd] ]]
103 then
103 then
104 cmd=$_hg_alias_list[$cmd]
104 cmd=$_hg_alias_list[$cmd]
105 fi
105 fi
106 fi
106 fi
107
107
108 curcontext="${curcontext%:*:*}:hg-${cmd}:"
108 curcontext="${curcontext%:*:*}:hg-${cmd}:"
109
109
110 zstyle -s ":completion:$curcontext:" cache-policy update_policy
110 zstyle -s ":completion:$curcontext:" cache-policy update_policy
111
111
112 if [[ -z "$update_policy" ]]
112 if [[ -z "$update_policy" ]]
113 then
113 then
114 zstyle ":completion:$curcontext:" cache-policy _hg_cache_policy
114 zstyle ":completion:$curcontext:" cache-policy _hg_cache_policy
115 fi
115 fi
116
116
117 if (( $+functions[_hg_cmd_${cmd}] ))
117 if (( $+functions[_hg_cmd_${cmd}] ))
118 then
118 then
119 _hg_cmd_${cmd}
119 _hg_cmd_${cmd}
120 else
120 else
121 # complete unknown commands normally
121 # complete unknown commands normally
122 _arguments -s -w : $_hg_global_opts \
122 _arguments -s -w : $_hg_global_opts \
123 '*:files:_hg_files'
123 '*:files:_hg_files'
124 fi
124 fi
125 }
125 }
126
126
127 _hg_cache_policy() {
127 _hg_cache_policy() {
128 typeset -a old
128 typeset -a old
129
129
130 # cache for a minute
130 # cache for a minute
131 old=( "$1"(mm+10) )
131 old=( "$1"(mm+10) )
132 (( $#old )) && return 0
132 (( $#old )) && return 0
133
133
134 return 1
134 return 1
135 }
135 }
136
136
137 _hg_get_commands() {
137 _hg_get_commands() {
138 typeset -ga _hg_cmd_list
138 typeset -ga _hg_cmd_list
139 typeset -gA _hg_alias_list
139 typeset -gA _hg_alias_list
140 local hline cmd cmdalias
140 local hline cmd cmdalias
141
141
142 _call_program hg hg debugcomplete -v | while read -A hline
142 _call_program hg hg debugcomplete -v | while read -A hline
143 do
143 do
144 cmd=$hline[1]
144 cmd=$hline[1]
145 _hg_cmd_list+=($cmd)
145 _hg_cmd_list+=($cmd)
146
146
147 for cmdalias in $hline[2,-1]
147 for cmdalias in $hline[2,-1]
148 do
148 do
149 _hg_cmd_list+=($cmdalias)
149 _hg_cmd_list+=($cmdalias)
150 _hg_alias_list+=($cmdalias $cmd)
150 _hg_alias_list+=($cmdalias $cmd)
151 done
151 done
152 done
152 done
153 }
153 }
154
154
155 _hg_commands() {
155 _hg_commands() {
156 (( $#_hg_cmd_list )) || _hg_get_commands
156 (( $#_hg_cmd_list )) || _hg_get_commands
157 _describe -t commands 'mercurial command' _hg_cmd_list
157 _describe -t commands 'mercurial command' _hg_cmd_list
158 }
158 }
159
159
160 _hg_revrange() {
160 _hg_revrange() {
161 compset -P 1 '*:'
161 compset -P 1 '*:'
162 _hg_labels "$@"
162 _hg_labels "$@"
163 }
163 }
164
164
165 _hg_labels() {
165 _hg_labels() {
166 labels=("${(f)$(_hg_cmd debugnamecomplete)}")
166 labels=("${(f)$(_hg_cmd debugnamecomplete)}")
167 (( $#labels )) && _describe -t labels 'labels' labels
167 (( $#labels )) && _describe -t labels 'labels' labels
168 }
168 }
169
169
170 _hg_bookmarks() {
170 _hg_bookmarks() {
171 typeset -a bookmark bookmarks
171 typeset -a bookmark bookmarks
172
172
173 _hg_cmd bookmarks | while read -A bookmark
173 _hg_cmd bookmarks | while read -A bookmark
174 do
174 do
175 if test -z ${bookmark[-1]:#[0-9]*}
175 if test -z ${bookmark[-1]:#[0-9]*}
176 then
176 then
177 bookmarks+=($bookmark[-2])
177 bookmarks+=($bookmark[-2])
178 fi
178 fi
179 done
179 done
180 (( $#bookmarks )) && _describe -t bookmarks 'bookmarks' bookmarks
180 (( $#bookmarks )) && _describe -t bookmarks 'bookmarks' bookmarks
181 }
181 }
182
182
183 _hg_branches() {
183 _hg_branches() {
184 typeset -a branches
184 typeset -a branches
185 local branch
185 local branch
186
186
187 _hg_cmd branches | while read branch
187 _hg_cmd branches | while read branch
188 do
188 do
189 branches+=(${branch/ #[0-9]#:*})
189 branches+=(${branch/ #[0-9]#:*})
190 done
190 done
191 (( $#branches )) && _describe -t branches 'branches' branches
191 (( $#branches )) && _describe -t branches 'branches' branches
192 }
192 }
193
193
194 # likely merge candidates
194 # likely merge candidates
195 _hg_mergerevs() {
195 _hg_mergerevs() {
196 typeset -a heads
196 typeset -a heads
197 local myrev
197 local myrev
198
198
199 heads=(${(f)"$(_hg_cmd heads --template '{rev}:{branch}\\n')"})
199 heads=(${(f)"$(_hg_cmd heads --template '{rev}:{branch}\\n')"})
200 # exclude own revision
200 # exclude own revision
201 myrev=$(_hg_cmd log -r . --template '{rev}:{branch}\\n')
201 myrev=$(_hg_cmd log -r . --template '{rev}:{branch}\\n')
202 heads=(${heads:#$myrev})
202 heads=(${heads:#$myrev})
203
203
204 (( $#heads )) && _describe -t heads 'heads' heads
204 (( $#heads )) && _describe -t heads 'heads' heads
205
205
206 branches=(${(f)"$(_hg_cmd heads --template '{branch}\\n')"})
206 branches=(${(f)"$(_hg_cmd heads --template '{branch}\\n')"})
207 # exclude own revision
207 # exclude own revision
208 myrev=$(_hg_cmd log -r . --template '{branch}\\n')
208 myrev=$(_hg_cmd log -r . --template '{branch}\\n')
209 branches=(${branches:#$myrev})
209 branches=(${branches:#$myrev})
210
210
211 (( $#branches )) && _describe -t branches 'branches' branches
211 (( $#branches )) && _describe -t branches 'branches' branches
212 }
212 }
213
213
214 _hg_files() {
214 _hg_files() {
215 if [[ -n "$_hg_root" ]]
215 if [[ -n "$_hg_root" ]]
216 then
216 then
217 [[ -d "$_hg_root/.hg" ]] || return
217 [[ -d "$_hg_root/.hg" ]] || return
218 case "$_hg_root" in
218 case "$_hg_root" in
219 /*)
219 /*)
220 _files -W $_hg_root
220 _files -W $_hg_root
221 ;;
221 ;;
222 *)
222 *)
223 _files -W $PWD/$_hg_root
223 _files -W $PWD/$_hg_root
224 ;;
224 ;;
225 esac
225 esac
226 else
226 else
227 _files
227 _files
228 fi
228 fi
229 }
229 }
230
230
231 _hg_status() {
231 _hg_status() {
232 [[ -d $PREFIX ]] || PREFIX=$PREFIX:h
232 [[ -d $PREFIX ]] || PREFIX=$PREFIX:h
233 status_files=(${(ps:\0:)"$(_hg_cmd status -0n$1 ./$PREFIX)"})
233 status_files=(${(ps:\0:)"$(_hg_cmd status -0n$1 ./$PREFIX)"})
234 }
234 }
235
235
236 _hg_unknown() {
236 _hg_unknown() {
237 typeset -a status_files
237 typeset -a status_files
238 _hg_status u
238 _hg_status u
239 _wanted files expl 'unknown files' _multi_parts / status_files
239 _wanted files expl 'unknown files' _multi_parts / status_files
240 }
240 }
241
241
242 _hg_missing() {
242 _hg_missing() {
243 typeset -a status_files
243 typeset -a status_files
244 _hg_status d
244 _hg_status d
245 _wanted files expl 'missing files' _multi_parts / status_files
245 _wanted files expl 'missing files' _multi_parts / status_files
246 }
246 }
247
247
248 _hg_modified() {
248 _hg_modified() {
249 typeset -a status_files
249 typeset -a status_files
250 _hg_status m
250 _hg_status m
251 _wanted files expl 'modified files' _multi_parts / status_files
251 _wanted files expl 'modified files' _multi_parts / status_files
252 }
252 }
253
253
254 _hg_resolve() {
254 _hg_resolve() {
255 local rstate rpath
255 local rstate rpath
256
256
257 [[ -d $PREFIX ]] || PREFIX=$PREFIX:h
257 [[ -d $PREFIX ]] || PREFIX=$PREFIX:h
258
258
259 _hg_cmd resolve -l ./$PREFIX | while read rstate rpath
259 _hg_cmd resolve -l ./$PREFIX | while read rstate rpath
260 do
260 do
261 [[ $rstate == 'R' ]] && resolved_files+=($rpath)
261 [[ $rstate == 'R' ]] && resolved_files+=($rpath)
262 [[ $rstate == 'U' ]] && unresolved_files+=($rpath)
262 [[ $rstate == 'U' ]] && unresolved_files+=($rpath)
263 done
263 done
264 }
264 }
265
265
266 _hg_resolved() {
266 _hg_resolved() {
267 typeset -a resolved_files unresolved_files
267 typeset -a resolved_files unresolved_files
268 _hg_resolve
268 _hg_resolve
269 _wanted files expl 'resolved files' _multi_parts / resolved_files
269 _wanted files expl 'resolved files' _multi_parts / resolved_files
270 }
270 }
271
271
272 _hg_unresolved() {
272 _hg_unresolved() {
273 typeset -a resolved_files unresolved_files
273 typeset -a resolved_files unresolved_files
274 _hg_resolve
274 _hg_resolve
275 _wanted files expl 'unresolved files' _multi_parts / unresolved_files
275 _wanted files expl 'unresolved files' _multi_parts / unresolved_files
276 }
276 }
277
277
278 _hg_config() {
278 _hg_config() {
279 typeset -a items
279 typeset -a items
280 items=(${${(%f)"$(_call_program hg hg showconfig)"}%%\=*})
280 items=(${${(%f)"$(_call_program hg hg showconfig)"}%%\=*})
281 (( $#items )) && _describe -t config 'config item' items
281 (( $#items )) && _describe -t config 'config item' items
282 }
282 }
283
283
284 _hg_addremove() {
284 _hg_addremove() {
285 _alternative 'files:unknown files:_hg_unknown' \
285 _alternative 'files:unknown files:_hg_unknown' \
286 'files:missing files:_hg_missing'
286 'files:missing files:_hg_missing'
287 }
287 }
288
288
289 _hg_ssh_urls() {
289 _hg_ssh_urls() {
290 if [[ -prefix */ ]]
290 if [[ -prefix */ ]]
291 then
291 then
292 if zstyle -T ":completion:${curcontext}:files" remote-access
292 if zstyle -T ":completion:${curcontext}:files" remote-access
293 then
293 then
294 local host=${PREFIX%%/*}
294 local host=${PREFIX%%/*}
295 typeset -a remdirs
295 typeset -a remdirs
296 compset -p $(( $#host + 1 ))
296 compset -p $(( $#host + 1 ))
297 local rempath=${(M)PREFIX##*/}
297 local rempath=${(M)PREFIX##*/}
298 local cacheid="hg:${host}-${rempath//\//_}"
298 local cacheid="hg:${host}-${rempath//\//_}"
299 cacheid=${cacheid%[-_]}
299 cacheid=${cacheid%[-_]}
300 compset -P '*/'
300 compset -P '*/'
301 if _cache_invalid "$cacheid" || ! _retrieve_cache "$cacheid"
301 if _cache_invalid "$cacheid" || ! _retrieve_cache "$cacheid"
302 then
302 then
303 remdirs=(${${(M)${(f)"$(_call_program files ssh -a -x $host ls -1FL "${(q)rempath}")"}##*/}%/})
303 remdirs=(${${(M)${(f)"$(_call_program files ssh -a -x $host ls -1FL "${(q)rempath}")"}##*/}%/})
304 _store_cache "$cacheid" remdirs
304 _store_cache "$cacheid" remdirs
305 fi
305 fi
306 _describe -t directories 'remote directory' remdirs -S/
306 _describe -t directories 'remote directory' remdirs -S/
307 else
307 else
308 _message 'remote directory'
308 _message 'remote directory'
309 fi
309 fi
310 else
310 else
311 if compset -P '*@'
311 if compset -P '*@'
312 then
312 then
313 _hosts -S/
313 _hosts -S/
314 else
314 else
315 _alternative 'hosts:remote host name:_hosts -S/' \
315 _alternative 'hosts:remote host name:_hosts -S/' \
316 'users:user:_users -S@'
316 'users:user:_users -S@'
317 fi
317 fi
318 fi
318 fi
319 }
319 }
320
320
321 _hg_urls() {
321 _hg_urls() {
322 if compset -P bundle://
322 if compset -P bundle://
323 then
323 then
324 _files
324 _files
325 elif compset -P ssh://
325 elif compset -P ssh://
326 then
326 then
327 _hg_ssh_urls
327 _hg_ssh_urls
328 elif [[ -prefix *: ]]
328 elif [[ -prefix *: ]]
329 then
329 then
330 _urls
330 _urls
331 else
331 else
332 local expl
332 local expl
333 compset -S '[^:]*'
333 compset -S '[^:]*'
334 _wanted url-schemas expl 'URL schema' compadd -S '' - \
334 _wanted url-schemas expl 'URL schema' compadd -S '' - \
335 http:// https:// ssh:// bundle://
335 http:// https:// ssh:// bundle://
336 fi
336 fi
337 }
337 }
338
338
339 _hg_paths() {
339 _hg_paths() {
340 typeset -a paths pnames
340 typeset -a paths pnames
341 _hg_cmd paths | while read -A pnames
341 _hg_cmd paths | while read -A pnames
342 do
342 do
343 paths+=($pnames[1])
343 paths+=($pnames[1])
344 done
344 done
345 (( $#paths )) && _describe -t path-aliases 'repository alias' paths
345 (( $#paths )) && _describe -t path-aliases 'repository alias' paths
346 }
346 }
347
347
348 _hg_remote() {
348 _hg_remote() {
349 _alternative 'path-aliases:repository alias:_hg_paths' \
349 _alternative 'path-aliases:repository alias:_hg_paths' \
350 'directories:directory:_files -/' \
350 'directories:directory:_files -/' \
351 'urls:URL:_hg_urls'
351 'urls:URL:_hg_urls'
352 }
352 }
353
353
354 _hg_clone_dest() {
354 _hg_clone_dest() {
355 _alternative 'directories:directory:_files -/' \
355 _alternative 'directories:directory:_files -/' \
356 'urls:URL:_hg_urls'
356 'urls:URL:_hg_urls'
357 }
357 }
358
358
359 _hg_add_help_topics=(
359 _hg_add_help_topics=(
360 config dates diffs environment extensions filesets glossary hgignore hgweb
360 config dates diffs environment extensions filesets glossary hgignore hgweb
361 merge-tools multirevs obsolescence patterns phases revisions revsets
361 merge-tools multirevs obsolescence patterns phases revisions revsets
362 subrepos templating urls
362 subrepos templating urls
363 )
363 )
364
364
365 _hg_help_topics() {
365 _hg_help_topics() {
366 local topics
366 local topics
367 (( $#_hg_cmd_list )) || _hg_get_commands
367 (( $#_hg_cmd_list )) || _hg_get_commands
368 topics=($_hg_cmd_list $_hg_add_help_topics)
368 topics=($_hg_cmd_list $_hg_add_help_topics)
369 _describe -t help_topics 'help topics' topics
369 _describe -t help_topics 'help topics' topics
370 }
370 }
371
371
372 # Common options
372 # Common options
373 _hg_global_opts=(
373 _hg_global_opts=(
374 '(--repository -R)'{-R+,--repository}'[repository root directory]:repository:_files -/'
374 '(--repository -R)'{-R+,--repository=}'[repository root directory]:repository:_files -/'
375 '--cwd[change working directory]:new working directory:_files -/'
375 '--cwd[change working directory]:new working directory:_files -/'
376 '(--noninteractive -y)'{-y,--noninteractive}'[do not prompt, assume yes for any required answers]'
376 '(--noninteractive -y)'{-y,--noninteractive}'[do not prompt, assume yes for any required answers]'
377 '(--verbose -v)'{-v,--verbose}'[enable additional output]'
377 '(--verbose -v)'{-v,--verbose}'[enable additional output]'
378 '*--config[set/override config option]:defined config items:_hg_config'
378 '*--config[set/override config option]:defined config items:_hg_config'
379 '(--quiet -q)'{-q,--quiet}'[suppress output]'
379 '(--quiet -q)'{-q,--quiet}'[suppress output]'
380 '(--help -h)'{-h,--help}'[display help and exit]'
380 '(--help -h)'{-h,--help}'[display help and exit]'
381 '--debug[debug mode]'
381 '--debug[debug mode]'
382 '--debugger[start debugger]'
382 '--debugger[start debugger]'
383 '--encoding[set the charset encoding]'
383 '--encoding[set the charset encoding]'
384 '--encodingmode[set the charset encoding mode]'
384 '--encodingmode[set the charset encoding mode]'
385 '--lsprof[print improved command execution profile]'
385 '--lsprof[print improved command execution profile]'
386 '--traceback[print traceback on exception]'
386 '--traceback[print traceback on exception]'
387 '--time[time how long the command takes]'
387 '--time[time how long the command takes]'
388 '--profile[profile]'
388 '--profile[profile]'
389 '--version[output version information and exit]'
389 '--version[output version information and exit]'
390 )
390 )
391
391
392 _hg_pat_opts=(
392 _hg_pat_opts=(
393 '*'{-I+,--include}'[include names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/'
393 '*'{-I+,--include=}'[include names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/'
394 '*'{-X+,--exclude}'[exclude names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/')
394 '*'{-X+,--exclude=}'[exclude names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/')
395
395
396 _hg_clone_opts=(
396 _hg_clone_opts=(
397 $_hg_remote_opts
397 $_hg_remote_opts
398 '(--noupdate -U)'{-U,--noupdate}'[do not update the new working directory]'
398 '(--noupdate -U)'{-U,--noupdate}'[do not update the new working directory]'
399 '--pull[use pull protocol to copy metadata]'
399 '--pull[use pull protocol to copy metadata]'
400 '--uncompressed[use uncompressed transfer (fast over LAN)]')
400 '--uncompressed[use uncompressed transfer (fast over LAN)]')
401
401
402 _hg_date_user_opts=(
402 _hg_date_user_opts=(
403 '(--currentdate -D)'{-D,--currentdate}'[record the current date as commit date]'
403 '(--currentdate -D)'{-D,--currentdate}'[record the current date as commit date]'
404 '(--currentuser -U)'{-U,--currentuser}'[record the current user as committer]'
404 '(--currentuser -U)'{-U,--currentuser}'[record the current user as committer]'
405 '(--date -d)'{-d+,--date}'[record the specified date as commit date]:date:'
405 '(--date -d)'{-d+,--date=}'[record the specified date as commit date]:date:'
406 '(--user -u)'{-u+,--user}'[record the specified user as committer]:user:')
406 '(--user -u)'{-u+,--user=}'[record the specified user as committer]:user:')
407
407
408 _hg_gitlike_opts=(
408 _hg_gitlike_opts=(
409 '(--git -g)'{-g,--git}'[use git extended diff format]')
409 '(--git -g)'{-g,--git}'[use git extended diff format]')
410
410
411 _hg_diff_opts=(
411 _hg_diff_opts=(
412 $_hg_gitlike_opts
412 $_hg_gitlike_opts
413 '(--text -a)'{-a,--text}'[treat all files as text]'
413 '(--text -a)'{-a,--text}'[treat all files as text]'
414 '--nodates[omit dates from diff headers]')
414 '--nodates[omit dates from diff headers]')
415
415
416 _hg_mergetool_opts=(
416 _hg_mergetool_opts=(
417 '(--tool -t)'{-t+,--tool}'[specify merge tool]:tool:')
417 '(--tool -t)'{-t+,--tool=}'[specify merge tool]:tool:')
418
418
419 _hg_dryrun_opts=(
419 _hg_dryrun_opts=(
420 '(--dry-run -n)'{-n,--dry-run}'[do not perform actions, just print output]')
420 '(--dry-run -n)'{-n,--dry-run}'[do not perform actions, just print output]')
421
421
422 _hg_ignore_space_opts=(
422 _hg_ignore_space_opts=(
423 '(--ignore-all-space -w)'{-w,--ignore-all-space}'[ignore white space when comparing lines]'
423 '(--ignore-all-space -w)'{-w,--ignore-all-space}'[ignore white space when comparing lines]'
424 '(--ignore-space-change -b)'{-b,--ignore-space-change}'[ignore changes in the amount of white space]'
424 '(--ignore-space-change -b)'{-b,--ignore-space-change}'[ignore changes in the amount of white space]'
425 '(--ignore-blank-lines -B)'{-B,--ignore-blank-lines}'[ignore changes whose lines are all blank]')
425 '(--ignore-blank-lines -B)'{-B,--ignore-blank-lines}'[ignore changes whose lines are all blank]')
426
426
427 _hg_style_opts=(
427 _hg_style_opts=(
428 '--style[display using template map file]:'
428 '--style[display using template map file]:'
429 '--template[display with template]:')
429 '--template[display with template]:')
430
430
431 _hg_log_opts=(
431 _hg_log_opts=(
432 $_hg_global_opts $_hg_style_opts $_hg_gitlike_opts
432 $_hg_global_opts $_hg_style_opts $_hg_gitlike_opts
433 '(--limit -l)'{-l+,--limit}'[limit number of changes displayed]:'
433 '(--limit -l)'{-l+,--limit=}'[limit number of changes displayed]:'
434 '(--no-merges -M)'{-M,--no-merges}'[do not show merges]'
434 '(--no-merges -M)'{-M,--no-merges}'[do not show merges]'
435 '(--patch -p)'{-p,--patch}'[show patch]'
435 '(--patch -p)'{-p,--patch}'[show patch]'
436 '--stat[output diffstat-style summary of changes]'
436 '--stat[output diffstat-style summary of changes]'
437 )
437 )
438
438
439 _hg_commit_opts=(
439 _hg_commit_opts=(
440 '(-m --message -l --logfile --edit -e)'{-e,--edit}'[edit commit message]'
440 '(-m --message -l --logfile --edit -e)'{-e,--edit}'[edit commit message]'
441 '(-e --edit -l --logfile --message -m)'{-m+,--message}'[use <text> as commit message]:message:'
441 '(-e --edit -l --logfile --message -m)'{-m+,--message=}'[use <text> as commit message]:message:'
442 '(-e --edit -m --message --logfile -l)'{-l+,--logfile}'[read the commit message from <file>]:log file:_files')
442 '(-e --edit -m --message --logfile -l)'{-l+,--logfile=}'[read the commit message from <file>]:log file:_files')
443
443
444 _hg_remote_opts=(
444 _hg_remote_opts=(
445 '(--ssh -e)'{-e+,--ssh}'[specify ssh command to use]:'
445 '(--ssh -e)'{-e+,--ssh=}'[specify ssh command to use]:'
446 '--remotecmd[specify hg command to run on the remote side]:')
446 '--remotecmd[specify hg command to run on the remote side]:')
447
447
448 _hg_branch_bmark_opts=(
448 _hg_branch_bmark_opts=(
449 '(--bookmark -B)'{-B+,--bookmark}'[specify bookmark(s)]:bookmark:_hg_bookmarks'
449 '(--bookmark -B)'{-B+,--bookmark=}'[specify bookmark(s)]:bookmark:_hg_bookmarks'
450 '(--branch -b)'{-b+,--branch}'[specify branch(es)]:branch:_hg_branches'
450 '(--branch -b)'{-b+,--branch=}'[specify branch(es)]:branch:_hg_branches'
451 )
451 )
452
452
453 _hg_subrepos_opts=(
453 _hg_subrepos_opts=(
454 '(--subrepos -S)'{-S,--subrepos}'[recurse into subrepositories]')
454 '(--subrepos -S)'{-S,--subrepos}'[recurse into subrepositories]')
455
455
456 _hg_cmd() {
456 _hg_cmd() {
457 _call_program hg HGPLAIN=1 hg "$_hg_cmd_globals[@]" "$@" 2> /dev/null
457 _call_program hg HGPLAIN=1 hg "$_hg_cmd_globals[@]" "$@" 2> /dev/null
458 }
458 }
459
459
460 _hg_cmd_add() {
460 _hg_cmd_add() {
461 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts $_hg_subrepos_opts \
461 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts $_hg_subrepos_opts \
462 '*:unknown files:_hg_unknown'
462 '*:unknown files:_hg_unknown'
463 }
463 }
464
464
465 _hg_cmd_addremove() {
465 _hg_cmd_addremove() {
466 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
466 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
467 '(--similarity -s)'{-s+,--similarity}'[guess renamed files by similarity (0<=s<=100)]:' \
467 '(--similarity -s)'{-s+,--similarity=}'[guess renamed files by similarity (0<=s<=100)]:' \
468 '*:unknown or missing files:_hg_addremove'
468 '*:unknown or missing files:_hg_addremove'
469 }
469 }
470
470
471 _hg_cmd_annotate() {
471 _hg_cmd_annotate() {
472 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
472 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
473 '(--rev -r)'{-r+,--rev}'[annotate the specified revision]:revision:_hg_labels' \
473 '(--rev -r)'{-r+,--rev=}'[annotate the specified revision]:revision:_hg_labels' \
474 '(--follow -f)'{-f,--follow}'[follow file copies and renames]' \
474 '(--follow -f)'{-f,--follow}'[follow file copies and renames]' \
475 '(--text -a)'{-a,--text}'[treat all files as text]' \
475 '(--text -a)'{-a,--text}'[treat all files as text]' \
476 '(--user -u)'{-u,--user}'[list the author]' \
476 '(--user -u)'{-u,--user}'[list the author]' \
477 '(--date -d)'{-d,--date}'[list the date]' \
477 '(--date -d)'{-d,--date}'[list the date]' \
478 '(--number -n)'{-n,--number}'[list the revision number (default)]' \
478 '(--number -n)'{-n,--number}'[list the revision number (default)]' \
479 '(--changeset -c)'{-c,--changeset}'[list the changeset]' \
479 '(--changeset -c)'{-c,--changeset}'[list the changeset]' \
480 '*:files:_hg_files'
480 '*:files:_hg_files'
481 }
481 }
482
482
483 _hg_cmd_archive() {
483 _hg_cmd_archive() {
484 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
484 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
485 '--no-decode[do not pass files through decoders]' \
485 '--no-decode[do not pass files through decoders]' \
486 '(--prefix -p)'{-p+,--prefix}'[directory prefix for files in archive]:' \
486 '(--prefix -p)'{-p+,--prefix=}'[directory prefix for files in archive]:' \
487 '(--rev -r)'{-r+,--rev}'[revision to distribute]:revision:_hg_labels' \
487 '(--rev -r)'{-r+,--rev=}'[revision to distribute]:revision:_hg_labels' \
488 '(--type -t)'{-t+,--type}'[type of distribution to create]:archive type:(files tar tbz2 tgz uzip zip)' \
488 '(--type -t)'{-t+,--type=}'[type of distribution to create]:archive type:(files tar tbz2 tgz uzip zip)' \
489 '*:destination:_files'
489 '*:destination:_files'
490 }
490 }
491
491
492 _hg_cmd_backout() {
492 _hg_cmd_backout() {
493 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts $_hg_pat_opts \
493 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts $_hg_pat_opts \
494 '--merge[merge with old dirstate parent after backout]' \
494 '--merge[merge with old dirstate parent after backout]' \
495 '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \
495 '(--date -d)'{-d+,--date=}'[record datecode as commit date]:date code:' \
496 '--parent[parent to choose when backing out merge]' \
496 '--parent[parent to choose when backing out merge]' \
497 '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \
497 '(--user -u)'{-u+,--user=}'[record user as commiter]:user:' \
498 '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_labels' \
498 '(--rev -r)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
499 '(--message -m)'{-m+,--message}'[use <text> as commit message]:text:' \
499 '(--message -m)'{-m+,--message=}'[use <text> as commit message]:text:' \
500 '(--logfile -l)'{-l+,--logfile}'[read commit message from <file>]:log file:_files'
500 '(--logfile -l)'{-l+,--logfile=}'[read commit message from <file>]:log file:_files'
501 }
501 }
502
502
503 _hg_cmd_bisect() {
503 _hg_cmd_bisect() {
504 _arguments -s -w : $_hg_global_opts \
504 _arguments -s -w : $_hg_global_opts \
505 '(-)'{-r,--reset}'[reset bisect state]' \
505 '(-)'{-r,--reset}'[reset bisect state]' \
506 '(--extend -e)'{-e,--extend}'[extend the bisect range]' \
506 '(--extend -e)'{-e,--extend}'[extend the bisect range]' \
507 '(--good -g --bad -b --skip -s --reset -r)'{-g,--good}'[mark changeset good]'::revision:_hg_labels \
507 '(--good -g --bad -b --skip -s --reset -r)'{-g,--good}'[mark changeset good]'::revision:_hg_labels \
508 '(--good -g --bad -b --skip -s --reset -r)'{-b,--bad}'[mark changeset bad]'::revision:_hg_labels \
508 '(--good -g --bad -b --skip -s --reset -r)'{-b,--bad}'[mark changeset bad]'::revision:_hg_labels \
509 '(--good -g --bad -b --skip -s --reset -r)'{-s,--skip}'[skip testing changeset]' \
509 '(--good -g --bad -b --skip -s --reset -r)'{-s,--skip}'[skip testing changeset]' \
510 '(--command -c --noupdate -U)'{-c+,--command}'[use command to check changeset state]':commands:_command_names \
510 '(--command -c --noupdate -U)'{-c+,--command=}'[use command to check changeset state]':commands:_command_names \
511 '(--command -c --noupdate -U)'{-U,--noupdate}'[do not update to target]'
511 '(--command -c --noupdate -U)'{-U,--noupdate}'[do not update to target]'
512 }
512 }
513
513
514 _hg_cmd_bookmarks() {
514 _hg_cmd_bookmarks() {
515 _arguments -s -w : $_hg_global_opts \
515 _arguments -s -w : $_hg_global_opts \
516 '(--force -f)'{-f,--force}'[force]' \
516 '(--force -f)'{-f,--force}'[force]' \
517 '(--inactive -i)'{-i,--inactive}'[mark a bookmark inactive]' \
517 '(--inactive -i)'{-i,--inactive}'[mark a bookmark inactive]' \
518 '(--rev -r --delete -d --rename -m)'{-r+,--rev}'[revision]:revision:_hg_labels' \
518 '(--rev -r --delete -d --rename -m)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
519 '(--rev -r --delete -d --rename -m)'{-d,--delete}'[delete a given bookmark]' \
519 '(--rev -r --delete -d --rename -m)'{-d,--delete}'[delete a given bookmark]' \
520 '(--rev -r --delete -d --rename -m)'{-m+,--rename}'[rename a given bookmark]:bookmark:_hg_bookmarks' \
520 '(--rev -r --delete -d --rename -m)'{-m+,--rename=}'[rename a given bookmark]:bookmark:_hg_bookmarks' \
521 ':bookmark:_hg_bookmarks'
521 ':bookmark:_hg_bookmarks'
522 }
522 }
523
523
524 _hg_cmd_branch() {
524 _hg_cmd_branch() {
525 _arguments -s -w : $_hg_global_opts \
525 _arguments -s -w : $_hg_global_opts \
526 '(--force -f)'{-f,--force}'[set branch name even if it shadows an existing branch]' \
526 '(--force -f)'{-f,--force}'[set branch name even if it shadows an existing branch]' \
527 '(--clean -C)'{-C,--clean}'[reset branch name to parent branch name]'
527 '(--clean -C)'{-C,--clean}'[reset branch name to parent branch name]'
528 }
528 }
529
529
530 _hg_cmd_branches() {
530 _hg_cmd_branches() {
531 _arguments -s -w : $_hg_global_opts \
531 _arguments -s -w : $_hg_global_opts \
532 '(--active -a)'{-a,--active}'[show only branches that have unmerge heads]' \
532 '(--active -a)'{-a,--active}'[show only branches that have unmerge heads]' \
533 '(--closed -c)'{-c,--closed}'[show normal and closed branches]'
533 '(--closed -c)'{-c,--closed}'[show normal and closed branches]'
534 }
534 }
535
535
536 _hg_cmd_bundle() {
536 _hg_cmd_bundle() {
537 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
537 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
538 '(--force -f)'{-f,--force}'[run even when remote repository is unrelated]' \
538 '(--force -f)'{-f,--force}'[run even when remote repository is unrelated]' \
539 '(2)*--base[a base changeset to specify instead of a destination]:revision:_hg_labels' \
539 '(2)*--base[a base changeset to specify instead of a destination]:revision:_hg_labels' \
540 '(--branch -b)'{-b+,--branch}'[a specific branch to bundle]' \
540 '(--branch -b)'{-b+,--branch=}'[a specific branch to bundle]:' \
541 '(--rev -r)'{-r+,--rev}'[changeset(s) to bundle]:' \
541 '(--rev -r)'{-r+,--rev=}'[changeset(s) to bundle]:' \
542 '--all[bundle all changesets in the repository]' \
542 '--all[bundle all changesets in the repository]' \
543 ':output file:_files' \
543 ':output file:_files' \
544 ':destination repository:_files -/'
544 ':destination repository:_files -/'
545 }
545 }
546
546
547 _hg_cmd_cat() {
547 _hg_cmd_cat() {
548 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
548 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
549 '(--output -o)'{-o+,--output}'[print output to file with formatted name]:filespec:' \
549 '(--output -o)'{-o+,--output=}'[print output to file with formatted name]:filespec:' \
550 '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_labels' \
550 '(--rev -r)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
551 '--decode[apply any matching decode filter]' \
551 '--decode[apply any matching decode filter]' \
552 '*:file:_hg_files'
552 '*:file:_hg_files'
553 }
553 }
554
554
555 _hg_cmd_clone() {
555 _hg_cmd_clone() {
556 _arguments -s -w : $_hg_global_opts $_hg_clone_opts \
556 _arguments -s -w : $_hg_global_opts $_hg_clone_opts \
557 '(--rev -r)'{-r+,--rev}'[a changeset you would like to have after cloning]:' \
557 '(--rev -r)'{-r+,--rev=}'[a changeset you would like to have after cloning]:' \
558 '(--updaterev -u)'{-u+,--updaterev}'[revision, tag or branch to check out]' \
558 '(--updaterev -u)'{-u+,--updaterev=}'[revision, tag or branch to check out]:' \
559 '(--branch -b)'{-b+,--branch}'[clone only the specified branch]' \
559 '(--branch -b)'{-b+,--branch=}'[clone only the specified branch]:' \
560 ':source repository:_hg_remote' \
560 ':source repository:_hg_remote' \
561 ':destination:_hg_clone_dest'
561 ':destination:_hg_clone_dest'
562 }
562 }
563
563
564 _hg_cmd_commit() {
564 _hg_cmd_commit() {
565 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
565 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
566 '(--addremove -A)'{-A,--addremove}'[mark new/missing files as added/removed before committing]' \
566 '(--addremove -A)'{-A,--addremove}'[mark new/missing files as added/removed before committing]' \
567 '(--message -m)'{-m+,--message}'[use <text> as commit message]:text:' \
567 '(--message -m)'{-m+,--message=}'[use <text> as commit message]:text:' \
568 '(--logfile -l)'{-l+,--logfile}'[read commit message from <file>]:log file:_files' \
568 '(--logfile -l)'{-l+,--logfile=}'[read commit message from <file>]:log file:_files' \
569 '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \
569 '(--date -d)'{-d+,--date=}'[record datecode as commit date]:date code:' \
570 '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \
570 '(--user -u)'{-u+,--user=}'[record user as commiter]:user:' \
571 '--amend[amend the parent of the working dir]' \
571 '--amend[amend the parent of the working dir]' \
572 '--close-branch[mark a branch as closed]' \
572 '--close-branch[mark a branch as closed]' \
573 '*:file:_hg_files'
573 '*:file:_hg_files'
574 }
574 }
575
575
576 _hg_cmd_copy() {
576 _hg_cmd_copy() {
577 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
577 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
578 '(--after -A)'{-A,--after}'[record a copy that has already occurred]' \
578 '(--after -A)'{-A,--after}'[record a copy that has already occurred]' \
579 '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \
579 '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \
580 '*:file:_hg_files'
580 '*:file:_hg_files'
581 }
581 }
582
582
583 _hg_cmd_diff() {
583 _hg_cmd_diff() {
584 typeset -A opt_args
584 typeset -A opt_args
585 _arguments -s -w : $_hg_global_opts $_hg_diff_opts $_hg_ignore_space_opts \
585 _arguments -s -w : $_hg_global_opts $_hg_diff_opts $_hg_ignore_space_opts \
586 $_hg_pat_opts $_hg_subrepos_opts \
586 $_hg_pat_opts $_hg_subrepos_opts \
587 '*'{-r,--rev}'+[revision]:revision:_hg_revrange' \
587 '*'{-r+,--rev=}'[revision]:revision:_hg_revrange' \
588 '(--show-function -p)'{-p,--show-function}'[show which function each change is in]' \
588 '(--show-function -p)'{-p,--show-function}'[show which function each change is in]' \
589 '(--change -c)'{-c,--change}'[change made by revision]' \
589 '(--change -c)'{-c+,--change=}'[change made by revision]:' \
590 '(--text -a)'{-a,--text}'[treat all files as text]' \
590 '(--text -a)'{-a,--text}'[treat all files as text]' \
591 '--reverse[produce a diff that undoes the changes]' \
591 '--reverse[produce a diff that undoes the changes]' \
592 '(--unified -U)'{-U,--unified}'[number of lines of context to show]' \
592 '(--unified -U)'{-U+,--unified=}'[number of lines of context to show]:' \
593 '--stat[output diffstat-style summary of changes]' \
593 '--stat[output diffstat-style summary of changes]' \
594 '*:file:->diff_files'
594 '*:file:->diff_files'
595
595
596 if [[ $state == 'diff_files' ]]
596 if [[ $state == 'diff_files' ]]
597 then
597 then
598 if [[ -n $opt_args[-r] ]]
598 if [[ -n $opt_args[-r] ]]
599 then
599 then
600 _hg_files
600 _hg_files
601 else
601 else
602 _hg_modified
602 _hg_modified
603 fi
603 fi
604 fi
604 fi
605 }
605 }
606
606
607 _hg_cmd_export() {
607 _hg_cmd_export() {
608 _arguments -s -w : $_hg_global_opts $_hg_diff_opts \
608 _arguments -s -w : $_hg_global_opts $_hg_diff_opts \
609 '(--outout -o)'{-o+,--output}'[print output to file with formatted name]:filespec:' \
609 '(--outout -o)'{-o+,--output=}'[print output to file with formatted name]:filespec:' \
610 '--switch-parent[diff against the second parent]' \
610 '--switch-parent[diff against the second parent]' \
611 '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_labels' \
611 '(--rev -r)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
612 '*:revision:_hg_labels'
612 '*:revision:_hg_labels'
613 }
613 }
614
614
615 _hg_cmd_forget() {
615 _hg_cmd_forget() {
616 _arguments -s -w : $_hg_global_opts \
616 _arguments -s -w : $_hg_global_opts \
617 '*:file:_hg_files'
617 '*:file:_hg_files'
618 }
618 }
619
619
620 _hg_cmd_graft() {
620 _hg_cmd_graft() {
621 _arguments -s -w : $_hg_global_opts $_hg_dryrun_opts \
621 _arguments -s -w : $_hg_global_opts $_hg_dryrun_opts \
622 $_hg_date_user_opts $_hg_mergetool_opts \
622 $_hg_date_user_opts $_hg_mergetool_opts \
623 '(--continue -c)'{-c,--continue}'[resume interrupted graft]' \
623 '(--continue -c)'{-c,--continue}'[resume interrupted graft]' \
624 '(--edit -e)'{-e,--edit}'[invoke editor on commit messages]' \
624 '(--edit -e)'{-e,--edit}'[invoke editor on commit messages]' \
625 '--log[append graft info to log message]' \
625 '--log[append graft info to log message]' \
626 '*:revision:_hg_labels'
626 '*:revision:_hg_labels'
627 }
627 }
628
628
629 _hg_cmd_grep() {
629 _hg_cmd_grep() {
630 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
630 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
631 '(--print0 -0)'{-0,--print0}'[end filenames with NUL]' \
631 '(--print0 -0)'{-0,--print0}'[end filenames with NUL]' \
632 '--all[print all revisions with matches]' \
632 '--all[print all revisions with matches]' \
633 '(--follow -f)'{-f,--follow}'[follow changeset or file history]' \
633 '(--follow -f)'{-f,--follow}'[follow changeset or file history]' \
634 '(--ignore-case -i)'{-i,--ignore-case}'[ignore case when matching]' \
634 '(--ignore-case -i)'{-i,--ignore-case}'[ignore case when matching]' \
635 '(--files-with-matches -l)'{-l,--files-with-matches}'[print only filenames and revs that match]' \
635 '(--files-with-matches -l)'{-l,--files-with-matches}'[print only filenames and revs that match]' \
636 '(--line-number -n)'{-n,--line-number}'[print matching line numbers]' \
636 '(--line-number -n)'{-n,--line-number}'[print matching line numbers]' \
637 '*'{-r+,--rev}'[search in given revision range]:revision:_hg_revrange' \
637 '*'{-r+,--rev=}'[search in given revision range]:revision:_hg_revrange' \
638 '(--user -u)'{-u,--user}'[print user who committed change]' \
638 '(--user -u)'{-u,--user}'[print user who committed change]' \
639 '(--date -d)'{-d,--date}'[print date of a changeset]' \
639 '(--date -d)'{-d,--date}'[print date of a changeset]' \
640 '1:search pattern:' \
640 '1:search pattern:' \
641 '*:files:_hg_files'
641 '*:files:_hg_files'
642 }
642 }
643
643
644 _hg_cmd_heads() {
644 _hg_cmd_heads() {
645 _arguments -s -w : $_hg_global_opts $_hg_style_opts \
645 _arguments -s -w : $_hg_global_opts $_hg_style_opts \
646 '(--topo -t)'{-t,--topo}'[show topological heads only]' \
646 '(--topo -t)'{-t,--topo}'[show topological heads only]' \
647 '(--closed -c)'{-c,--closed}'[show normal and closed branch heads]' \
647 '(--closed -c)'{-c,--closed}'[show normal and closed branch heads]' \
648 '(--rev -r)'{-r+,--rev}'[show only heads which are descendants of rev]:revision:_hg_labels'
648 '(--rev -r)'{-r+,--rev=}'[show only heads which are descendants of rev]:revision:_hg_labels'
649 }
649 }
650
650
651 _hg_cmd_help() {
651 _hg_cmd_help() {
652 _arguments -s -w : $_hg_global_opts \
652 _arguments -s -w : $_hg_global_opts \
653 '(--extension -e)'{-e,--extension}'[show only help for extensions]' \
653 '(--extension -e)'{-e,--extension}'[show only help for extensions]' \
654 '(--command -c)'{-c,--command}'[show only help for commands]' \
654 '(--command -c)'{-c,--command}'[show only help for commands]' \
655 '(--keyword -k)'{-k+,--keyword}'[show topics matching keyword]' \
655 '(--keyword -k)'{-k+,--keyword}'[show topics matching keyword]' \
656 '*:mercurial help topic:_hg_help_topics'
656 '*:mercurial help topic:_hg_help_topics'
657 }
657 }
658
658
659 _hg_cmd_identify() {
659 _hg_cmd_identify() {
660 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
660 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
661 '(--rev -r)'{-r+,--rev}'[identify the specified rev]:revision:_hg_labels' \
661 '(--rev -r)'{-r+,--rev=}'[identify the specified rev]:revision:_hg_labels' \
662 '(--num -n)'{-n+,--num}'[show local revision number]' \
662 '(--num -n)'{-n,--num}'[show local revision number]' \
663 '(--id -i)'{-i+,--id}'[show global revision id]' \
663 '(--id -i)'{-i,--id}'[show global revision id]' \
664 '(--branch -b)'{-b+,--branch}'[show branch]' \
664 '(--branch -b)'{-b,--branch}'[show branch]' \
665 '(--bookmark -B)'{-B+,--bookmark}'[show bookmarks]' \
665 '(--bookmark -B)'{-B,--bookmark}'[show bookmarks]' \
666 '(--tags -t)'{-t+,--tags}'[show tags]'
666 '(--tags -t)'{-t,--tags}'[show tags]'
667 }
667 }
668
668
669 _hg_cmd_import() {
669 _hg_cmd_import() {
670 _arguments -s -w : $_hg_global_opts $_hg_commit_opts \
670 _arguments -s -w : $_hg_global_opts $_hg_commit_opts \
671 '(--strip -p)'{-p+,--strip}'[directory strip option for patch (default: 1)]:count:' \
671 '(--strip -p)'{-p+,--strip=}'[directory strip option for patch (default: 1)]:count:' \
672 '(--force -f)'{-f,--force}'[skip check for outstanding uncommitted changes]' \
672 '(--force -f)'{-f,--force}'[skip check for outstanding uncommitted changes]' \
673 '--bypass[apply patch without touching the working directory]' \
673 '--bypass[apply patch without touching the working directory]' \
674 '--no-commit[do not commit, just update the working directory]' \
674 '--no-commit[do not commit, just update the working directory]' \
675 '--exact[apply patch to the nodes from which it was generated]' \
675 '--exact[apply patch to the nodes from which it was generated]' \
676 '--import-branch[use any branch information in patch (implied by --exact)]' \
676 '--import-branch[use any branch information in patch (implied by --exact)]' \
677 '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \
677 '(--date -d)'{-d+,--date=}'[record datecode as commit date]:date code:' \
678 '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \
678 '(--user -u)'{-u+,--user=}'[record user as commiter]:user:' \
679 '(--similarity -s)'{-s+,--similarity}'[guess renamed files by similarity (0<=s<=100)]:' \
679 '(--similarity -s)'{-s+,--similarity=}'[guess renamed files by similarity (0<=s<=100)]:' \
680 '*:patch:_files'
680 '*:patch:_files'
681 }
681 }
682
682
683 _hg_cmd_incoming() {
683 _hg_cmd_incoming() {
684 _arguments -s -w : $_hg_log_opts $_hg_branch_bmark_opts $_hg_remote_opts \
684 _arguments -s -w : $_hg_log_opts $_hg_branch_bmark_opts $_hg_remote_opts \
685 $_hg_subrepos_opts \
685 $_hg_subrepos_opts \
686 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
686 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
687 '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]:revision:_hg_labels' \
687 '(--rev -r)'{-r+,--rev=}'[a specific revision up to which you would like to pull]:revision:_hg_labels' \
688 '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \
688 '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \
689 '--bundle[file to store the bundles into]:bundle file:_files' \
689 '--bundle[file to store the bundles into]:bundle file:_files' \
690 ':source:_hg_remote'
690 ':source:_hg_remote'
691 }
691 }
692
692
693 _hg_cmd_init() {
693 _hg_cmd_init() {
694 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
694 _arguments -s -w : $_hg_global_opts $_hg_remote_opts \
695 ':dir:_files -/'
695 ':dir:_files -/'
696 }
696 }
697
697
698 _hg_cmd_locate() {
698 _hg_cmd_locate() {
699 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
699 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
700 '(--rev -r)'{-r+,--rev}'[search repository as it stood at revision]:revision:_hg_labels' \
700 '(--rev -r)'{-r+,--rev=}'[search repository as it stood at revision]:revision:_hg_labels' \
701 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \
701 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \
702 '(--fullpath -f)'{-f,--fullpath}'[print complete paths]' \
702 '(--fullpath -f)'{-f,--fullpath}'[print complete paths]' \
703 '*:search pattern:_hg_files'
703 '*:search pattern:_hg_files'
704 }
704 }
705
705
706 _hg_cmd_log() {
706 _hg_cmd_log() {
707 _arguments -s -w : $_hg_log_opts $_hg_pat_opts \
707 _arguments -s -w : $_hg_log_opts $_hg_pat_opts \
708 '(--follow --follow-first -f)'{-f,--follow}'[follow changeset or history]' \
708 '(--follow --follow-first -f)'{-f,--follow}'[follow changeset or history]' \
709 '(-f --follow)--follow-first[only follow the first parent of merge changesets]' \
709 '(-f --follow)--follow-first[only follow the first parent of merge changesets]' \
710 '(--copies -C)'{-C,--copies}'[show copied files]' \
710 '(--copies -C)'{-C,--copies}'[show copied files]' \
711 '(--keyword -k)'{-k+,--keyword}'[search for a keyword]:' \
711 '(--keyword -k)'{-k+,--keyword}'[search for a keyword]:' \
712 '*'{-r,--rev}'[show the specified revision or revset]:revision:_hg_revrange' \
712 '*'{-r+,--rev=}'[show the specified revision or revset]:revision:_hg_revrange' \
713 '(--only-merges -m)'{-m,--only-merges}'[show only merges]' \
713 '(--only-merges -m)'{-m,--only-merges}'[show only merges]' \
714 '(--prune -P)'{-P+,--prune}'[do not display revision or any of its ancestors]:revision:_hg_labels' \
714 '(--prune -P)'{-P+,--prune=}'[do not display revision or any of its ancestors]:revision:_hg_labels' \
715 '(--graph -G)'{-G+,--graph}'[show the revision DAG]' \
715 '(--graph -G)'{-G,--graph}'[show the revision DAG]' \
716 '(--branch -b)'{-b+,--branch}'[show changesets within the given named branch]:branch:_hg_branches' \
716 '(--branch -b)'{-b+,--branch=}'[show changesets within the given named branch]:branch:_hg_branches' \
717 '(--user -u)'{-u+,--user}'[revisions committed by user]:user:' \
717 '(--user -u)'{-u+,--user=}'[revisions committed by user]:user:' \
718 '(--date -d)'{-d+,--date}'[show revisions matching date spec]:date:' \
718 '(--date -d)'{-d+,--date=}'[show revisions matching date spec]:date:' \
719 '*:files:_hg_files'
719 '*:files:_hg_files'
720 }
720 }
721
721
722 _hg_cmd_manifest() {
722 _hg_cmd_manifest() {
723 _arguments -s -w : $_hg_global_opts \
723 _arguments -s -w : $_hg_global_opts \
724 '--all[list files from all revisions]' \
724 '--all[list files from all revisions]' \
725 '(--rev -r)'{-r+,--rev}'[revision to display]:revision:_hg_labels' \
725 '(--rev -r)'{-r+,--rev=}'[revision to display]:revision:_hg_labels' \
726 ':revision:_hg_labels'
726 ':revision:_hg_labels'
727 }
727 }
728
728
729 _hg_cmd_merge() {
729 _hg_cmd_merge() {
730 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts \
730 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts \
731 '(--force -f)'{-f,--force}'[force a merge with outstanding changes]' \
731 '(--force -f)'{-f,--force}'[force a merge with outstanding changes]' \
732 '(--rev -r 1)'{-r,--rev}'[revision to merge]:revision:_hg_mergerevs' \
732 '(--rev -r 1)'{-r+,--rev=}'[revision to merge]:revision:_hg_mergerevs' \
733 '(--preview -P)'{-P,--preview}'[review revisions to merge (no merge is performed)]' \
733 '(--preview -P)'{-P,--preview}'[review revisions to merge (no merge is performed)]' \
734 ':revision:_hg_mergerevs'
734 ':revision:_hg_mergerevs'
735 }
735 }
736
736
737 _hg_cmd_outgoing() {
737 _hg_cmd_outgoing() {
738 _arguments -s -w : $_hg_log_opts $_hg_branch_bmark_opts $_hg_remote_opts \
738 _arguments -s -w : $_hg_log_opts $_hg_branch_bmark_opts $_hg_remote_opts \
739 $_hg_subrepos_opts \
739 $_hg_subrepos_opts \
740 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
740 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
741 '*'{-r,--rev}'[a specific revision you would like to push]:revision:_hg_revrange' \
741 '*'{-r+,--rev=}'[a specific revision you would like to push]:revision:_hg_revrange' \
742 '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \
742 '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \
743 ':destination:_hg_remote'
743 ':destination:_hg_remote'
744 }
744 }
745
745
746 _hg_cmd_parents() {
746 _hg_cmd_parents() {
747 _arguments -s -w : $_hg_global_opts $_hg_style_opts \
747 _arguments -s -w : $_hg_global_opts $_hg_style_opts \
748 '(--rev -r)'{-r+,--rev}'[show parents of the specified rev]:revision:_hg_labels' \
748 '(--rev -r)'{-r+,--rev=}'[show parents of the specified rev]:revision:_hg_labels' \
749 ':last modified file:_hg_files'
749 ':last modified file:_hg_files'
750 }
750 }
751
751
752 _hg_cmd_paths() {
752 _hg_cmd_paths() {
753 _arguments -s -w : $_hg_global_opts \
753 _arguments -s -w : $_hg_global_opts \
754 ':path:_hg_paths'
754 ':path:_hg_paths'
755 }
755 }
756
756
757 _hg_cmd_phase() {
757 _hg_cmd_phase() {
758 _arguments -s -w : $_hg_global_opts \
758 _arguments -s -w : $_hg_global_opts \
759 '(--public -p)'{-p,--public}'[set changeset phase to public]' \
759 '(--public -p)'{-p,--public}'[set changeset phase to public]' \
760 '(--draft -d)'{-d,--draft}'[set changeset phase to draft]' \
760 '(--draft -d)'{-d,--draft}'[set changeset phase to draft]' \
761 '(--secret -s)'{-s,--secret}'[set changeset phase to secret]' \
761 '(--secret -s)'{-s,--secret}'[set changeset phase to secret]' \
762 '(--force -f)'{-f,--force}'[allow to move boundary backward]' \
762 '(--force -f)'{-f,--force}'[allow to move boundary backward]' \
763 '(--rev -r)'{-r+,--rev}'[target revision]:revision:_hg_labels' \
763 '(--rev -r)'{-r+,--rev=}'[target revision]:revision:_hg_labels' \
764 ':revision:_hg_labels'
764 ':revision:_hg_labels'
765 }
765 }
766
766
767 _hg_cmd_pull() {
767 _hg_cmd_pull() {
768 _arguments -s -w : $_hg_global_opts $_hg_branch_bmark_opts $_hg_remote_opts \
768 _arguments -s -w : $_hg_global_opts $_hg_branch_bmark_opts $_hg_remote_opts \
769 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
769 '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \
770 '(--update -u)'{-u,--update}'[update to new tip if changesets were pulled]' \
770 '(--update -u)'{-u,--update}'[update to new tip if changesets were pulled]' \
771 '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]:revision:' \
771 '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]:revision:' \
772 ':source:_hg_remote'
772 ':source:_hg_remote'
773 }
773 }
774
774
775 _hg_cmd_push() {
775 _hg_cmd_push() {
776 _arguments -s -w : $_hg_global_opts $_hg_branch_bmark_opts $_hg_remote_opts \
776 _arguments -s -w : $_hg_global_opts $_hg_branch_bmark_opts $_hg_remote_opts \
777 '(--force -f)'{-f,--force}'[force push]' \
777 '(--force -f)'{-f,--force}'[force push]' \
778 '(--rev -r)'{-r+,--rev}'[a specific revision you would like to push]:revision:_hg_labels' \
778 '(--rev -r)'{-r+,--rev=}'[a specific revision you would like to push]:revision:_hg_labels' \
779 '--new-branch[allow pushing a new branch]' \
779 '--new-branch[allow pushing a new branch]' \
780 ':destination:_hg_remote'
780 ':destination:_hg_remote'
781 }
781 }
782
782
783 _hg_cmd_remove() {
783 _hg_cmd_remove() {
784 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
784 _arguments -s -w : $_hg_global_opts $_hg_pat_opts \
785 '(--after -A)'{-A,--after}'[record remove that has already occurred]' \
785 '(--after -A)'{-A,--after}'[record remove that has already occurred]' \
786 '(--force -f)'{-f,--force}'[remove file even if modified]' \
786 '(--force -f)'{-f,--force}'[remove file even if modified]' \
787 '*:file:_hg_files'
787 '*:file:_hg_files'
788 }
788 }
789
789
790 _hg_cmd_rename() {
790 _hg_cmd_rename() {
791 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
791 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
792 '(--after -A)'{-A,--after}'[record a rename that has already occurred]' \
792 '(--after -A)'{-A,--after}'[record a rename that has already occurred]' \
793 '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \
793 '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \
794 '*:file:_hg_files'
794 '*:file:_hg_files'
795 }
795 }
796
796
797 _hg_cmd_resolve() {
797 _hg_cmd_resolve() {
798 local context state line
798 local context state line
799 typeset -A opt_args
799 typeset -A opt_args
800
800
801 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts $_hg_pat_opts \
801 _arguments -s -w : $_hg_global_opts $_hg_mergetool_opts $_hg_pat_opts \
802 '(--all -a)'{-a,--all}'[select all unresolved files]' \
802 '(--all -a)'{-a,--all}'[select all unresolved files]' \
803 '(--no-status -n)'{-n,--no-status}'[hide status prefix]' \
803 '(--no-status -n)'{-n,--no-status}'[hide status prefix]' \
804 '(--list -l --mark -m --unmark -u)'{-l,--list}'[list state of files needing merge]:*:merged files:->resolve_files' \
804 '(--list -l --mark -m --unmark -u)'{-l,--list}'[list state of files needing merge]:*:merged files:->resolve_files' \
805 '(--mark -m --list -l --unmark -u)'{-m,--mark}'[mark files as resolved]:*:unresolved files:_hg_unresolved' \
805 '(--mark -m --list -l --unmark -u)'{-m,--mark}'[mark files as resolved]:*:unresolved files:_hg_unresolved' \
806 '(--unmark -u --list -l --mark -m)'{-u,--unmark}'[unmark files as resolved]:*:resolved files:_hg_resolved' \
806 '(--unmark -u --list -l --mark -m)'{-u,--unmark}'[unmark files as resolved]:*:resolved files:_hg_resolved' \
807 '*:file:_hg_unresolved'
807 '*:file:_hg_unresolved'
808
808
809 if [[ $state == 'resolve_files' ]]
809 if [[ $state == 'resolve_files' ]]
810 then
810 then
811 _alternative 'files:resolved files:_hg_resolved' \
811 _alternative 'files:resolved files:_hg_resolved' \
812 'files:unresolved files:_hg_unresolved'
812 'files:unresolved files:_hg_unresolved'
813 fi
813 fi
814 }
814 }
815
815
816 _hg_cmd_revert() {
816 _hg_cmd_revert() {
817 local context state line
817 local context state line
818 typeset -A opt_args
818 typeset -A opt_args
819
819
820 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
820 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \
821 '(--all -a :)'{-a,--all}'[revert all changes when no arguments given]' \
821 '(--all -a :)'{-a,--all}'[revert all changes when no arguments given]' \
822 '(--rev -r)'{-r+,--rev}'[revision to revert to]:revision:_hg_labels' \
822 '(--rev -r)'{-r+,--rev=}'[revision to revert to]:revision:_hg_labels' \
823 '(--no-backup -C)'{-C,--no-backup}'[do not save backup copies of files]' \
823 '(--no-backup -C)'{-C,--no-backup}'[do not save backup copies of files]' \
824 '(--date -d)'{-d+,--date}'[tipmost revision matching date]:date code:' \
824 '(--date -d)'{-d+,--date=}'[tipmost revision matching date]:date code:' \
825 '*:file:->diff_files'
825 '*:file:->diff_files'
826
826
827 if [[ $state == 'diff_files' ]]
827 if [[ $state == 'diff_files' ]]
828 then
828 then
829 if [[ -n $opt_args[-r] ]]
829 if [[ -n $opt_args[-r] ]]
830 then
830 then
831 _hg_files
831 _hg_files
832 else
832 else
833 typeset -a status_files
833 typeset -a status_files
834 _hg_status mard
834 _hg_status mard
835 _wanted files expl 'modified, added, removed or deleted file' _multi_parts / status_files
835 _wanted files expl 'modified, added, removed or deleted file' _multi_parts / status_files
836 fi
836 fi
837 fi
837 fi
838 }
838 }
839
839
840 _hg_cmd_rollback() {
840 _hg_cmd_rollback() {
841 _arguments -s -w : $_hg_global_opts $_hg_dryrun_opts \
841 _arguments -s -w : $_hg_global_opts $_hg_dryrun_opts \
842 '(--force -f)'{-f,--force}'[ignore safety measures]' \
842 '(--force -f)'{-f,--force}'[ignore safety measures]' \
843 }
843 }
844
844
845 _hg_cmd_serve() {
845 _hg_cmd_serve() {
846 _arguments -s -w : $_hg_global_opts \
846 _arguments -s -w : $_hg_global_opts \
847 '(--accesslog -A)'{-A+,--accesslog}'[name of access log file]:log file:_files' \
847 '(--accesslog -A)'{-A+,--accesslog=}'[name of access log file]:log file:_files' \
848 '(--errorlog -E)'{-E+,--errorlog}'[name of error log file]:log file:_files' \
848 '(--errorlog -E)'{-E+,--errorlog=}'[name of error log file]:log file:_files' \
849 '(--daemon -d)'{-d,--daemon}'[run server in background]' \
849 '(--daemon -d)'{-d,--daemon}'[run server in background]' \
850 '(--port -p)'{-p+,--port}'[listen port]:listen port:' \
850 '(--port -p)'{-p+,--port=}'[listen port]:listen port:' \
851 '(--address -a)'{-a+,--address}'[interface address]:interface address:' \
851 '(--address -a)'{-a+,--address=}'[interface address]:interface address:' \
852 '--prefix[prefix path to serve from]:directory:_files' \
852 '--prefix[prefix path to serve from]:directory:_files' \
853 '(--name -n)'{-n+,--name}'[name to show in web pages]:repository name:' \
853 '(--name -n)'{-n+,--name=}'[name to show in web pages]:repository name:' \
854 '--web-conf[name of the hgweb config file]:webconf_file:_files' \
854 '--web-conf[name of the hgweb config file]:webconf_file:_files' \
855 '--pid-file[name of file to write process ID to]:pid_file:_files' \
855 '--pid-file[name of file to write process ID to]:pid_file:_files' \
856 '--cmdserver[cmdserver mode]:mode:' \
856 '--cmdserver[cmdserver mode]:mode:' \
857 '(--templates -t)'{-t,--templates}'[web template directory]:template dir:_files -/' \
857 '(--templates -t)'{-t,--templates}'[web template directory]:template dir:_files -/' \
858 '--style[web template style]:style' \
858 '--style[web template style]:style' \
859 '--stdio[for remote clients]' \
859 '--stdio[for remote clients]' \
860 '--certificate[certificate file]:cert_file:_files' \
860 '--certificate[certificate file]:cert_file:_files' \
861 '(--ipv6 -6)'{-6,--ipv6}'[use IPv6 in addition to IPv4]'
861 '(--ipv6 -6)'{-6,--ipv6}'[use IPv6 in addition to IPv4]'
862 }
862 }
863
863
864 _hg_cmd_showconfig() {
864 _hg_cmd_showconfig() {
865 _arguments -s -w : $_hg_global_opts \
865 _arguments -s -w : $_hg_global_opts \
866 '(--untrusted -u)'{-u+,--untrusted}'[show untrusted configuration options]' \
866 '(--untrusted -u)'{-u,--untrusted}'[show untrusted configuration options]' \
867 ':config item:_hg_config'
867 ':config item:_hg_config'
868 }
868 }
869
869
870 _hg_cmd_status() {
870 _hg_cmd_status() {
871 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
871 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
872 '(--all -A)'{-A,--all}'[show status of all files]' \
872 '(--all -A)'{-A,--all}'[show status of all files]' \
873 '(--modified -m)'{-m,--modified}'[show only modified files]' \
873 '(--modified -m)'{-m,--modified}'[show only modified files]' \
874 '(--added -a)'{-a,--added}'[show only added files]' \
874 '(--added -a)'{-a,--added}'[show only added files]' \
875 '(--removed -r)'{-r,--removed}'[show only removed files]' \
875 '(--removed -r)'{-r,--removed}'[show only removed files]' \
876 '(--deleted -d)'{-d,--deleted}'[show only deleted (but tracked) files]' \
876 '(--deleted -d)'{-d,--deleted}'[show only deleted (but tracked) files]' \
877 '(--clean -c)'{-c,--clean}'[show only files without changes]' \
877 '(--clean -c)'{-c,--clean}'[show only files without changes]' \
878 '(--unknown -u)'{-u,--unknown}'[show only unknown files]' \
878 '(--unknown -u)'{-u,--unknown}'[show only unknown files]' \
879 '(--ignored -i)'{-i,--ignored}'[show ignored files]' \
879 '(--ignored -i)'{-i,--ignored}'[show ignored files]' \
880 '(--no-status -n)'{-n,--no-status}'[hide status prefix]' \
880 '(--no-status -n)'{-n,--no-status}'[hide status prefix]' \
881 '(--copies -C)'{-C,--copies}'[show source of copied files]' \
881 '(--copies -C)'{-C,--copies}'[show source of copied files]' \
882 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \
882 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \
883 '--rev[show difference from revision]:revision:_hg_labels' \
883 '--rev[show difference from revision]:revision:_hg_labels' \
884 '--change[list the changed files of a revision]:revision:_hg_labels' \
884 '--change[list the changed files of a revision]:revision:_hg_labels' \
885 '*:files:_files'
885 '*:files:_files'
886 }
886 }
887
887
888 _hg_cmd_summary() {
888 _hg_cmd_summary() {
889 _arguments -s -w : $_hg_global_opts \
889 _arguments -s -w : $_hg_global_opts \
890 '--remote[check for push and pull]'
890 '--remote[check for push and pull]'
891 }
891 }
892
892
893 _hg_cmd_tag() {
893 _hg_cmd_tag() {
894 _arguments -s -w : $_hg_global_opts \
894 _arguments -s -w : $_hg_global_opts \
895 '(--local -l)'{-l,--local}'[make the tag local]' \
895 '(--local -l)'{-l,--local}'[make the tag local]' \
896 '(--message -m)'{-m+,--message}'[message for tag commit log entry]:message:' \
896 '(--message -m)'{-m+,--message=}'[message for tag commit log entry]:message:' \
897 '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \
897 '(--date -d)'{-d+,--date=}'[record datecode as commit date]:date code:' \
898 '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \
898 '(--user -u)'{-u+,--user=}'[record user as commiter]:user:' \
899 '(--rev -r)'{-r+,--rev}'[revision to tag]:revision:_hg_labels' \
899 '(--rev -r)'{-r+,--rev=}'[revision to tag]:revision:_hg_labels' \
900 '(--force -f)'{-f,--force}'[force tag]' \
900 '(--force -f)'{-f,--force}'[force tag]' \
901 '--remove[remove a tag]' \
901 '--remove[remove a tag]' \
902 '(--edit -e)'{-e,--edit}'[edit commit message]' \
902 '(--edit -e)'{-e,--edit}'[edit commit message]' \
903 ':tag name:'
903 ':tag name:'
904 }
904 }
905
905
906 _hg_cmd_tip() {
906 _hg_cmd_tip() {
907 _arguments -s -w : $_hg_global_opts $_hg_gitlike_opts $_hg_style_opts \
907 _arguments -s -w : $_hg_global_opts $_hg_gitlike_opts $_hg_style_opts \
908 '(--patch -p)'{-p,--patch}'[show patch]'
908 '(--patch -p)'{-p,--patch}'[show patch]'
909 }
909 }
910
910
911 _hg_cmd_unbundle() {
911 _hg_cmd_unbundle() {
912 _arguments -s -w : $_hg_global_opts \
912 _arguments -s -w : $_hg_global_opts \
913 '(--update -u)'{-u,--update}'[update to new tip if changesets were unbundled]' \
913 '(--update -u)'{-u,--update}'[update to new tip if changesets were unbundled]' \
914 ':files:_files'
914 ':files:_files'
915 }
915 }
916
916
917 _hg_cmd_update() {
917 _hg_cmd_update() {
918 _arguments -s -w : $_hg_global_opts \
918 _arguments -s -w : $_hg_global_opts \
919 '(--clean -C)'{-C,--clean}'[overwrite locally modified files]' \
919 '(--clean -C)'{-C,--clean}'[overwrite locally modified files]' \
920 '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_labels' \
920 '(--rev -r)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
921 '(--check -c)'{-c,--check}'[update across branches if no uncommitted changes]' \
921 '(--check -c)'{-c,--check}'[update across branches if no uncommitted changes]' \
922 '(--date -d)'{-d+,--date}'[tipmost revision matching date]' \
922 '(--date -d)'{-d+,--date=}'[tipmost revision matching date]:' \
923 ':revision:_hg_labels'
923 ':revision:_hg_labels'
924 }
924 }
925
925
926 ## extensions ##
926 ## extensions ##
927
927
928 # HGK
928 # HGK
929 _hg_cmd_view() {
929 _hg_cmd_view() {
930 _arguments -s -w : $_hg_global_opts \
930 _arguments -s -w : $_hg_global_opts \
931 '(--limit -l)'{-l+,--limit}'[limit number of changes displayed]:' \
931 '(--limit -l)'{-l+,--limit=}'[limit number of changes displayed]:' \
932 ':revision range:_hg_labels'
932 ':revision range:_hg_labels'
933 }
933 }
934
934
935 # MQ
935 # MQ
936 _hg_qseries() {
936 _hg_qseries() {
937 typeset -a patches
937 typeset -a patches
938 patches=(${(f)"$(_hg_cmd qseries)"})
938 patches=(${(f)"$(_hg_cmd qseries)"})
939 (( $#patches )) && _describe -t hg-patches 'patches' patches
939 (( $#patches )) && _describe -t hg-patches 'patches' patches
940 }
940 }
941
941
942 _hg_qapplied() {
942 _hg_qapplied() {
943 typeset -a patches
943 typeset -a patches
944 patches=(${(f)"$(_hg_cmd qapplied)"})
944 patches=(${(f)"$(_hg_cmd qapplied)"})
945 if (( $#patches ))
945 if (( $#patches ))
946 then
946 then
947 patches+=(qbase qtip)
947 patches+=(qbase qtip)
948 _describe -t hg-applied-patches 'applied patches' patches
948 _describe -t hg-applied-patches 'applied patches' patches
949 fi
949 fi
950 }
950 }
951
951
952 _hg_qunapplied() {
952 _hg_qunapplied() {
953 typeset -a patches
953 typeset -a patches
954 patches=(${(f)"$(_hg_cmd qunapplied)"})
954 patches=(${(f)"$(_hg_cmd qunapplied)"})
955 (( $#patches )) && _describe -t hg-unapplied-patches 'unapplied patches' patches
955 (( $#patches )) && _describe -t hg-unapplied-patches 'unapplied patches' patches
956 }
956 }
957
957
958 # unapplied, including guarded patches
958 # unapplied, including guarded patches
959 _hg_qdeletable() {
959 _hg_qdeletable() {
960 typeset -a unapplied
960 typeset -a unapplied
961 unapplied=(${(f)"$(_hg_cmd qseries)"})
961 unapplied=(${(f)"$(_hg_cmd qseries)"})
962 for p in $(_hg_cmd qapplied)
962 for p in $(_hg_cmd qapplied)
963 do
963 do
964 unapplied=(${unapplied:#$p})
964 unapplied=(${unapplied:#$p})
965 done
965 done
966
966
967 (( $#unapplied )) && _describe -t hg-allunapplied-patches 'all unapplied patches' unapplied
967 (( $#unapplied )) && _describe -t hg-allunapplied-patches 'all unapplied patches' unapplied
968 }
968 }
969
969
970 _hg_qguards() {
970 _hg_qguards() {
971 typeset -a guards
971 typeset -a guards
972 local guard
972 local guard
973 compset -P "+|-"
973 compset -P "+|-"
974 _hg_cmd qselect -s | while read guard
974 _hg_cmd qselect -s | while read guard
975 do
975 do
976 guards+=(${guard#(+|-)})
976 guards+=(${guard#(+|-)})
977 done
977 done
978 (( $#guards )) && _describe -t hg-guards 'guards' guards
978 (( $#guards )) && _describe -t hg-guards 'guards' guards
979 }
979 }
980
980
981 _hg_qseries_opts=(
981 _hg_qseries_opts=(
982 '(--summary -s)'{-s,--summary}'[print first line of patch header]')
982 '(--summary -s)'{-s,--summary}'[print first line of patch header]')
983
983
984 _hg_cmd_qapplied() {
984 _hg_cmd_qapplied() {
985 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
985 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
986 '(--last -1)'{-1,--last}'[show only the preceding applied patch]' \
986 '(--last -1)'{-1,--last}'[show only the preceding applied patch]' \
987 '*:patch:_hg_qapplied'
987 '*:patch:_hg_qapplied'
988 }
988 }
989
989
990 _hg_cmd_qclone() {
990 _hg_cmd_qclone() {
991 _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_clone_opts \
991 _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_clone_opts \
992 '(--patches -p)'{-p+,--patches}'[location of source patch repository]' \
992 '(--patches -p)'{-p+,--patches=}'[location of source patch repository]:' \
993 ':source repository:_hg_remote' \
993 ':source repository:_hg_remote' \
994 ':destination:_hg_clone_dest'
994 ':destination:_hg_clone_dest'
995 }
995 }
996
996
997 _hg_cmd_qdelete() {
997 _hg_cmd_qdelete() {
998 _arguments -s -w : $_hg_global_opts \
998 _arguments -s -w : $_hg_global_opts \
999 '(--keep -k)'{-k,--keep}'[keep patch file]' \
999 '(--keep -k)'{-k,--keep}'[keep patch file]' \
1000 '*'{-r+,--rev}'[stop managing a revision]:applied patch:_hg_revrange' \
1000 '*'{-r+,--rev=}'[stop managing a revision]:applied patch:_hg_revrange' \
1001 '*:unapplied patch:_hg_qdeletable'
1001 '*:unapplied patch:_hg_qdeletable'
1002 }
1002 }
1003
1003
1004 _hg_cmd_qdiff() {
1004 _hg_cmd_qdiff() {
1005 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_diff_opts \
1005 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_diff_opts \
1006 $_hg_ignore_space_opts \
1006 $_hg_ignore_space_opts \
1007 '*:pattern:_hg_files'
1007 '*:pattern:_hg_files'
1008 }
1008 }
1009
1009
1010 _hg_cmd_qfinish() {
1010 _hg_cmd_qfinish() {
1011 _arguments -s -w : $_hg_global_opts \
1011 _arguments -s -w : $_hg_global_opts \
1012 '(--applied -a)'{-a,--applied}'[finish all applied patches]' \
1012 '(--applied -a)'{-a,--applied}'[finish all applied patches]' \
1013 '*:patch:_hg_qapplied'
1013 '*:patch:_hg_qapplied'
1014 }
1014 }
1015
1015
1016 _hg_cmd_qfold() {
1016 _hg_cmd_qfold() {
1017 _arguments -s -w : $_hg_global_opts $_h_commit_opts \
1017 _arguments -s -w : $_hg_global_opts $_h_commit_opts \
1018 '(--keep,-k)'{-k,--keep}'[keep folded patch files]' \
1018 '(--keep,-k)'{-k,--keep}'[keep folded patch files]' \
1019 '(--force -f)'{-f,--force}'[overwrite any local changes]' \
1019 '(--force -f)'{-f,--force}'[overwrite any local changes]' \
1020 '--no-backup[do not save backup copies of files]' \
1020 '--no-backup[do not save backup copies of files]' \
1021 '*:unapplied patch:_hg_qunapplied'
1021 '*:unapplied patch:_hg_qunapplied'
1022 }
1022 }
1023
1023
1024 _hg_cmd_qgoto() {
1024 _hg_cmd_qgoto() {
1025 _arguments -s -w : $_hg_global_opts \
1025 _arguments -s -w : $_hg_global_opts \
1026 '(--force -f)'{-f,--force}'[overwrite any local changes]' \
1026 '(--force -f)'{-f,--force}'[overwrite any local changes]' \
1027 '--keep-changes[tolerate non-conflicting local changes]' \
1027 '--keep-changes[tolerate non-conflicting local changes]' \
1028 ':patch:_hg_qseries'
1028 ':patch:_hg_qseries'
1029 }
1029 }
1030
1030
1031 _hg_cmd_qguard() {
1031 _hg_cmd_qguard() {
1032 _arguments -s -w : $_hg_global_opts \
1032 _arguments -s -w : $_hg_global_opts \
1033 '(--list -l)'{-l,--list}'[list all patches and guards]' \
1033 '(--list -l)'{-l,--list}'[list all patches and guards]' \
1034 '(--none -n)'{-n,--none}'[drop all guards]' \
1034 '(--none -n)'{-n,--none}'[drop all guards]' \
1035 ':patch:_hg_qseries' \
1035 ':patch:_hg_qseries' \
1036 '*:guards:_hg_qguards'
1036 '*:guards:_hg_qguards'
1037 }
1037 }
1038
1038
1039 _hg_cmd_qheader() {
1039 _hg_cmd_qheader() {
1040 _arguments -s -w : $_hg_global_opts \
1040 _arguments -s -w : $_hg_global_opts \
1041 ':patch:_hg_qseries'
1041 ':patch:_hg_qseries'
1042 }
1042 }
1043
1043
1044 _hg_cmd_qimport() {
1044 _hg_cmd_qimport() {
1045 _arguments -s -w : $_hg_global_opts $_hg_gitlike_opts \
1045 _arguments -s -w : $_hg_global_opts $_hg_gitlike_opts \
1046 '(--existing -e)'{-e,--existing}'[import file in patch dir]' \
1046 '(--existing -e)'{-e,--existing}'[import file in patch dir]' \
1047 '(--name -n 2)'{-n+,--name}'[patch file name]:name:' \
1047 '(--name -n 2)'{-n+,--name}'[patch file name]:name:' \
1048 '(--force -f)'{-f,--force}'[overwrite existing files]' \
1048 '(--force -f)'{-f,--force}'[overwrite existing files]' \
1049 '*'{-r+,--rev}'[place existing revisions under mq control]:revision:_hg_revrange' \
1049 '*'{-r+,--rev=}'[place existing revisions under mq control]:revision:_hg_revrange' \
1050 '(--push -P)'{-P,--push}'[qpush after importing]' \
1050 '(--push -P)'{-P,--push}'[qpush after importing]' \
1051 '*:patch:_files'
1051 '*:patch:_files'
1052 }
1052 }
1053
1053
1054 _hg_cmd_qnew() {
1054 _hg_cmd_qnew() {
1055 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_date_user_opts $_hg_gitlike_opts \
1055 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_date_user_opts $_hg_gitlike_opts \
1056 ':patch:'
1056 ':patch:'
1057 }
1057 }
1058
1058
1059 _hg_cmd_qnext() {
1059 _hg_cmd_qnext() {
1060 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1060 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1061 }
1061 }
1062
1062
1063 _hg_cmd_qpop() {
1063 _hg_cmd_qpop() {
1064 _arguments -s -w : $_hg_global_opts \
1064 _arguments -s -w : $_hg_global_opts \
1065 '(--all -a :)'{-a,--all}'[pop all patches]' \
1065 '(--all -a :)'{-a,--all}'[pop all patches]' \
1066 '(--force -f)'{-f,--force}'[forget any local changes]' \
1066 '(--force -f)'{-f,--force}'[forget any local changes]' \
1067 '--keep-changes[tolerate non-conflicting local changes]' \
1067 '--keep-changes[tolerate non-conflicting local changes]' \
1068 '--no-backup[do not save backup copies of files]' \
1068 '--no-backup[do not save backup copies of files]' \
1069 ':patch:_hg_qapplied'
1069 ':patch:_hg_qapplied'
1070 }
1070 }
1071
1071
1072 _hg_cmd_qprev() {
1072 _hg_cmd_qprev() {
1073 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1073 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1074 }
1074 }
1075
1075
1076 _hg_cmd_qpush() {
1076 _hg_cmd_qpush() {
1077 _arguments -s -w : $_hg_global_opts \
1077 _arguments -s -w : $_hg_global_opts \
1078 '(--all -a :)'{-a,--all}'[apply all patches]' \
1078 '(--all -a :)'{-a,--all}'[apply all patches]' \
1079 '(--list -l)'{-l,--list}'[list patch name in commit text]' \
1079 '(--list -l)'{-l,--list}'[list patch name in commit text]' \
1080 '(--force -f)'{-f,--force}'[apply if the patch has rejects]' \
1080 '(--force -f)'{-f,--force}'[apply if the patch has rejects]' \
1081 '(--exact -e)'{-e,--exact}'[apply the target patch to its recorded parent]' \
1081 '(--exact -e)'{-e,--exact}'[apply the target patch to its recorded parent]' \
1082 '--move[reorder patch series and apply only the patch]' \
1082 '--move[reorder patch series and apply only the patch]' \
1083 '--keep-changes[tolerate non-conflicting local changes]' \
1083 '--keep-changes[tolerate non-conflicting local changes]' \
1084 '--no-backup[do not save backup copies of files]' \
1084 '--no-backup[do not save backup copies of files]' \
1085 ':patch:_hg_qunapplied'
1085 ':patch:_hg_qunapplied'
1086 }
1086 }
1087
1087
1088 _hg_cmd_qrefresh() {
1088 _hg_cmd_qrefresh() {
1089 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_commit_opts $_hg_gitlike_opts \
1089 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_commit_opts $_hg_gitlike_opts \
1090 '(--short -s)'{-s,--short}'[short refresh]' \
1090 '(--short -s)'{-s,--short}'[short refresh]' \
1091 '*:files:_hg_files'
1091 '*:files:_hg_files'
1092 }
1092 }
1093
1093
1094 _hg_cmd_qrename() {
1094 _hg_cmd_qrename() {
1095 _arguments -s -w : $_hg_global_opts \
1095 _arguments -s -w : $_hg_global_opts \
1096 ':patch:_hg_qunapplied' \
1096 ':patch:_hg_qunapplied' \
1097 ':destination:'
1097 ':destination:'
1098 }
1098 }
1099
1099
1100 _hg_cmd_qselect() {
1100 _hg_cmd_qselect() {
1101 _arguments -s -w : $_hg_global_opts \
1101 _arguments -s -w : $_hg_global_opts \
1102 '(--none -n :)'{-n,--none}'[disable all guards]' \
1102 '(--none -n :)'{-n,--none}'[disable all guards]' \
1103 '(--series -s :)'{-s,--series}'[list all guards in series file]' \
1103 '(--series -s :)'{-s,--series}'[list all guards in series file]' \
1104 '--pop[pop to before first guarded applied patch]' \
1104 '--pop[pop to before first guarded applied patch]' \
1105 '--reapply[pop and reapply patches]' \
1105 '--reapply[pop and reapply patches]' \
1106 '*:guards:_hg_qguards'
1106 '*:guards:_hg_qguards'
1107 }
1107 }
1108
1108
1109 _hg_cmd_qseries() {
1109 _hg_cmd_qseries() {
1110 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
1110 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
1111 '(--missing -m)'{-m,--missing}'[print patches not in series]'
1111 '(--missing -m)'{-m,--missing}'[print patches not in series]'
1112 }
1112 }
1113
1113
1114 _hg_cmd_qunapplied() {
1114 _hg_cmd_qunapplied() {
1115 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
1115 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \
1116 '(--first -1)'{-1,--first}'[show only the first patch]'
1116 '(--first -1)'{-1,--first}'[show only the first patch]'
1117 }
1117 }
1118
1118
1119 _hg_cmd_qtop() {
1119 _hg_cmd_qtop() {
1120 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1120 _arguments -s -w : $_hg_global_opts $_hg_qseries_opts
1121 }
1121 }
1122
1122
1123 _hg_cmd_strip() {
1123 _hg_cmd_strip() {
1124 _arguments -s -w : $_hg_global_opts \
1124 _arguments -s -w : $_hg_global_opts \
1125 '(--force -f)'{-f,--force}'[force removal, discard uncommitted changes, no backup]' \
1125 '(--force -f)'{-f,--force}'[force removal, discard uncommitted changes, no backup]' \
1126 '(--no-backup -n)'{-n,--no-backup}'[no backups]' \
1126 '(--no-backup -n)'{-n,--no-backup}'[no backups]' \
1127 '(--keep -k)'{-k,--keep}'[do not modify working copy during strip]' \
1127 '(--keep -k)'{-k,--keep}'[do not modify working copy during strip]' \
1128 '(--bookmark -B)'{-B+,--bookmark}'[remove revs only reachable from given bookmark]:bookmark:_hg_bookmarks' \
1128 '(--bookmark -B)'{-B+,--bookmark=}'[remove revs only reachable from given bookmark]:bookmark:_hg_bookmarks' \
1129 '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_labels' \
1129 '(--rev -r)'{-r+,--rev=}'[revision]:revision:_hg_labels' \
1130 ':revision:_hg_labels'
1130 ':revision:_hg_labels'
1131 }
1131 }
1132
1132
1133 # Patchbomb
1133 # Patchbomb
1134 _hg_cmd_email() {
1134 _hg_cmd_email() {
1135 _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_gitlike_opts \
1135 _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_gitlike_opts \
1136 '--plain[omit hg patch header]' \
1136 '--plain[omit hg patch header]' \
1137 '--body[send patches as inline message text (default)]' \
1137 '--body[send patches as inline message text (default)]' \
1138 '(--outgoing -o)'{-o,--outgoing}'[send changes not found in the target repository]' \
1138 '(--outgoing -o)'{-o,--outgoing}'[send changes not found in the target repository]' \
1139 '(--bundle -b)'{-b,--bundle}'[send changes not in target as a binary bundle]' \
1139 '(--bundle -b)'{-b,--bundle}'[send changes not in target as a binary bundle]' \
1140 '--bundlename[name of the bundle attachment file (default: bundle)]:' \
1140 '--bundlename[name of the bundle attachment file (default: bundle)]:' \
1141 '*'{-r+,--rev}'[search in given revision range]:revision:_hg_revrange' \
1141 '*'{-r+,--rev=}'[search in given revision range]:revision:_hg_revrange' \
1142 '--force[run even when remote repository is unrelated (with -b/--bundle)]' \
1142 '--force[run even when remote repository is unrelated (with -b/--bundle)]' \
1143 '*--base[a base changeset to specify instead of a destination (with -b/--bundle)]:revision:_hg_labels' \
1143 '*--base[a base changeset to specify instead of a destination (with -b/--bundle)]:revision:_hg_labels' \
1144 '--intro[send an introduction email for a single patch]' \
1144 '--intro[send an introduction email for a single patch]' \
1145 '(--inline -i --attach -a)'{-a,--attach}'[send patches as attachments]' \
1145 '(--inline -i --attach -a)'{-a,--attach}'[send patches as attachments]' \
1146 '(--attach -a --inline -i)'{-i,--inline}'[send patches as inline attachments]' \
1146 '(--attach -a --inline -i)'{-i,--inline}'[send patches as inline attachments]' \
1147 '*--bcc[email addresses of blind carbon copy recipients]:email:' \
1147 '*--bcc[email addresses of blind carbon copy recipients]:email:' \
1148 '*'{-c+,--cc}'[email addresses of copy recipients]:email:' \
1148 '*'{-c+,--cc}'[email addresses of copy recipients]:email:' \
1149 '(--diffstat -d)'{-d,--diffstat}'[add diffstat output to messages]' \
1149 '(--diffstat -d)'{-d,--diffstat}'[add diffstat output to messages]' \
1150 '--date[use the given date as the sending date]:date:' \
1150 '--date[use the given date as the sending date]:date:' \
1151 '--desc[use the given file as the series description]:files:_files' \
1151 '--desc[use the given file as the series description]:files:_files' \
1152 '(--from -f)'{-f,--from}'[email address of sender]:email:' \
1152 '(--from -f)'{-f,--from}'[email address of sender]:email:' \
1153 '(--test -n)'{-n,--test}'[print messages that would be sent]' \
1153 '(--test -n)'{-n,--test}'[print messages that would be sent]' \
1154 '(--mbox -m)'{-m,--mbox}'[write messages to mbox file instead of sending them]:file:' \
1154 '(--mbox -m)'{-m,--mbox}'[write messages to mbox file instead of sending them]:file:' \
1155 '*--reply-to[email addresses replies should be sent to]:email:' \
1155 '*--reply-to[email addresses replies should be sent to]:email:' \
1156 '(--subject -s)'{-s,--subject}'[subject of first message (intro or single patch)]:subject:' \
1156 '(--subject -s)'{-s,--subject}'[subject of first message (intro or single patch)]:subject:' \
1157 '--in-reply-to[message identifier to reply to]:msgid:' \
1157 '--in-reply-to[message identifier to reply to]:msgid:' \
1158 '*--flag[flags to add in subject prefixes]:flag:' \
1158 '*--flag[flags to add in subject prefixes]:flag:' \
1159 '*'{-t,--to}'[email addresses of recipients]:email:' \
1159 '*'{-t,--to}'[email addresses of recipients]:email:' \
1160 ':revision:_hg_revrange'
1160 ':revision:_hg_revrange'
1161 }
1161 }
1162
1162
1163 # Rebase
1163 # Rebase
1164 _hg_cmd_rebase() {
1164 _hg_cmd_rebase() {
1165 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_mergetool_opts \
1165 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_mergetool_opts \
1166 '*'{-r,--rev}'[rebase these revisions]:revision:_hg_revrange' \
1166 '*'{-r+,--rev=}'[rebase these revisions]:revision:_hg_revrange' \
1167 '(--source -s)'{-s+,--source}'[rebase from the specified changeset]:revision:_hg_labels' \
1167 '(--source -s)'{-s+,--source=}'[rebase from the specified changeset]:revision:_hg_labels' \
1168 '(--base -b)'{-b+,--base}'[rebase from the base of the specified changeset]:revision:_hg_labels' \
1168 '(--base -b)'{-b+,--base=}'[rebase from the base of the specified changeset]:revision:_hg_labels' \
1169 '(--dest -d)'{-d+,--dest}'[rebase onto the specified changeset]:revision:_hg_labels' \
1169 '(--dest -d)'{-d+,--dest=}'[rebase onto the specified changeset]:revision:_hg_labels' \
1170 '--collapse[collapse the rebased changeset]' \
1170 '--collapse[collapse the rebased changeset]' \
1171 '--keep[keep original changeset]' \
1171 '--keep[keep original changeset]' \
1172 '--keepbranches[keep original branch name]' \
1172 '--keepbranches[keep original branch name]' \
1173 '(--continue -c)'{-c,--continue}'[continue an interrupted rebase]' \
1173 '(--continue -c)'{-c,--continue}'[continue an interrupted rebase]' \
1174 '(--abort -a)'{-a,--abort}'[abort an interrupted rebase]' \
1174 '(--abort -a)'{-a,--abort}'[abort an interrupted rebase]' \
1175 }
1175 }
1176
1176
1177 # Record
1177 # Record
1178 _hg_cmd_record() {
1178 _hg_cmd_record() {
1179 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_pat_opts \
1179 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_pat_opts \
1180 $_hg_ignore_space_opts $_hg_subrepos_opts \
1180 $_hg_ignore_space_opts $_hg_subrepos_opts \
1181 '(--addremove -A)'{-A,--addremove}'[mark new/missing files as added/removed before committing]' \
1181 '(--addremove -A)'{-A,--addremove}'[mark new/missing files as added/removed before committing]' \
1182 '--close-branch[mark a branch as closed, hiding it from the branch list]' \
1182 '--close-branch[mark a branch as closed, hiding it from the branch list]' \
1183 '--amend[amend the parent of the working dir]' \
1183 '--amend[amend the parent of the working dir]' \
1184 '(--date -d)'{-d+,--date}'[record the specified date as commit date]:date:' \
1184 '(--date -d)'{-d+,--date=}'[record the specified date as commit date]:date:' \
1185 '(--user -u)'{-u+,--user}'[record the specified user as committer]:user:'
1185 '(--user -u)'{-u+,--user=}'[record the specified user as committer]:user:'
1186 }
1186 }
1187
1187
1188 _hg_cmd_qrecord() {
1188 _hg_cmd_qrecord() {
1189 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_date_user_opts $_hg_gitlike_opts \
1189 _arguments -s -w : $_hg_global_opts $_hg_commit_opts $_hg_date_user_opts $_hg_gitlike_opts \
1190 $_hg_pat_opts $_hg_ignore_space_opts $_hg_subrepos_opts
1190 $_hg_pat_opts $_hg_ignore_space_opts $_hg_subrepos_opts
1191 }
1191 }
1192
1192
1193 # Convert
1193 # Convert
1194 _hg_cmd_convert() {
1194 _hg_cmd_convert() {
1195 _arguments -s -w : $_hg_global_opts \
1195 _arguments -s -w : $_hg_global_opts \
1196 '(--source-type -s)'{-s,--source-type}'[source repository type]' \
1196 '(--source-type -s)'{-s,--source-type}'[source repository type]' \
1197 '(--dest-type -d)'{-d,--dest-type}'[destination repository type]' \
1197 '(--dest-type -d)'{-d,--dest-type}'[destination repository type]' \
1198 '(--rev -r)'{-r+,--rev}'[import up to target revision]:revision:' \
1198 '(--rev -r)'{-r+,--rev=}'[import up to target revision]:revision:' \
1199 '(--authormap -A)'{-A+,--authormap}'[remap usernames using this file]:file:_files' \
1199 '(--authormap -A)'{-A+,--authormap=}'[remap usernames using this file]:file:_files' \
1200 '--filemap[remap file names using contents of file]:file:_files' \
1200 '--filemap[remap file names using contents of file]:file:_files' \
1201 '--splicemap[splice synthesized history into place]:file:_files' \
1201 '--splicemap[splice synthesized history into place]:file:_files' \
1202 '--branchmap[change branch names while converting]:file:_files' \
1202 '--branchmap[change branch names while converting]:file:_files' \
1203 '--branchsort[try to sort changesets by branches]' \
1203 '--branchsort[try to sort changesets by branches]' \
1204 '--datesort[try to sort changesets by date]' \
1204 '--datesort[try to sort changesets by date]' \
1205 '--sourcesort[preserve source changesets order]'
1205 '--sourcesort[preserve source changesets order]'
1206 }
1206 }
1207
1207
1208 # Graphlog
1208 # Graphlog
1209 _hg_cmd_glog() {
1209 _hg_cmd_glog() {
1210 _hg_cmd_log $@
1210 _hg_cmd_log $@
1211 }
1211 }
1212
1212
1213 # Purge
1213 # Purge
1214 _hg_cmd_purge() {
1214 _hg_cmd_purge() {
1215 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
1215 _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_subrepos_opts \
1216 '(--abort-on-err -a)'{-a,--abort-on-err}'[abort if an error occurs]' \
1216 '(--abort-on-err -a)'{-a,--abort-on-err}'[abort if an error occurs]' \
1217 '--all[purge ignored files too]' \
1217 '--all[purge ignored files too]' \
1218 '(--print -p)'{-p,--print}'[print filenames instead of deleting them]' \
1218 '(--print -p)'{-p,--print}'[print filenames instead of deleting them]' \
1219 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs (implies -p/--print)]'
1219 '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs (implies -p/--print)]'
1220 }
1220 }
1221
1221
1222 _hg "$@"
1222 _hg "$@"
@@ -1,1115 +1,1114 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # $Id: manpage.py 6110 2009-08-31 14:40:33Z grubert $
2 # $Id: manpage.py 6110 2009-08-31 14:40:33Z grubert $
3 # Author: Engelbert Gruber <grubert@users.sourceforge.net>
3 # Author: Engelbert Gruber <grubert@users.sourceforge.net>
4 # Copyright: This module is put into the public domain.
4 # Copyright: This module is put into the public domain.
5
5
6 """
6 """
7 Simple man page writer for reStructuredText.
7 Simple man page writer for reStructuredText.
8
8
9 Man pages (short for "manual pages") contain system documentation on unix-like
9 Man pages (short for "manual pages") contain system documentation on unix-like
10 systems. The pages are grouped in numbered sections:
10 systems. The pages are grouped in numbered sections:
11
11
12 1 executable programs and shell commands
12 1 executable programs and shell commands
13 2 system calls
13 2 system calls
14 3 library functions
14 3 library functions
15 4 special files
15 4 special files
16 5 file formats
16 5 file formats
17 6 games
17 6 games
18 7 miscellaneous
18 7 miscellaneous
19 8 system administration
19 8 system administration
20
20
21 Man pages are written in *troff*, a text file formatting system.
21 Man pages are written in *troff*, a text file formatting system.
22
22
23 See http://www.tldp.org/HOWTO/Man-Page for a start.
23 See http://www.tldp.org/HOWTO/Man-Page for a start.
24
24
25 Man pages have no subsections only parts.
25 Man pages have no subsections only parts.
26 Standard parts
26 Standard parts
27
27
28 NAME ,
28 NAME ,
29 SYNOPSIS ,
29 SYNOPSIS ,
30 DESCRIPTION ,
30 DESCRIPTION ,
31 OPTIONS ,
31 OPTIONS ,
32 FILES ,
32 FILES ,
33 SEE ALSO ,
33 SEE ALSO ,
34 BUGS ,
34 BUGS ,
35
35
36 and
36 and
37
37
38 AUTHOR .
38 AUTHOR .
39
39
40 A unix-like system keeps an index of the DESCRIPTIONs, which is accesable
40 A unix-like system keeps an index of the DESCRIPTIONs, which is accesable
41 by the command whatis or apropos.
41 by the command whatis or apropos.
42
42
43 """
43 """
44 from __future__ import absolute_import
44 from __future__ import absolute_import
45
45
46 __docformat__ = 'reStructuredText'
46 __docformat__ = 'reStructuredText'
47
47
48 import inspect
48 import inspect
49 import re
49 import re
50
50
51 from docutils import (
51 from docutils import (
52 languages,
52 languages,
53 nodes,
53 nodes,
54 writers,
54 writers,
55 )
55 )
56 try:
56 try:
57 import roman
57 import roman
58 except ImportError:
58 except ImportError:
59 from docutils.utils import roman
59 from docutils.utils import roman
60 import inspect
61
60
62 FIELD_LIST_INDENT = 7
61 FIELD_LIST_INDENT = 7
63 DEFINITION_LIST_INDENT = 7
62 DEFINITION_LIST_INDENT = 7
64 OPTION_LIST_INDENT = 7
63 OPTION_LIST_INDENT = 7
65 BLOCKQOUTE_INDENT = 3.5
64 BLOCKQOUTE_INDENT = 3.5
66
65
67 # Define two macros so man/roff can calculate the
66 # Define two macros so man/roff can calculate the
68 # indent/unindent margins by itself
67 # indent/unindent margins by itself
69 MACRO_DEF = (r""".
68 MACRO_DEF = (r""".
70 .nr rst2man-indent-level 0
69 .nr rst2man-indent-level 0
71 .
70 .
72 .de1 rstReportMargin
71 .de1 rstReportMargin
73 \\$1 \\n[an-margin]
72 \\$1 \\n[an-margin]
74 level \\n[rst2man-indent-level]
73 level \\n[rst2man-indent-level]
75 level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
74 level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
76 -
75 -
77 \\n[rst2man-indent0]
76 \\n[rst2man-indent0]
78 \\n[rst2man-indent1]
77 \\n[rst2man-indent1]
79 \\n[rst2man-indent2]
78 \\n[rst2man-indent2]
80 ..
79 ..
81 .de1 INDENT
80 .de1 INDENT
82 .\" .rstReportMargin pre:
81 .\" .rstReportMargin pre:
83 . RS \\$1
82 . RS \\$1
84 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
83 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
85 . nr rst2man-indent-level +1
84 . nr rst2man-indent-level +1
86 .\" .rstReportMargin post:
85 .\" .rstReportMargin post:
87 ..
86 ..
88 .de UNINDENT
87 .de UNINDENT
89 . RE
88 . RE
90 .\" indent \\n[an-margin]
89 .\" indent \\n[an-margin]
91 .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
90 .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
92 .nr rst2man-indent-level -1
91 .nr rst2man-indent-level -1
93 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
92 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
94 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
93 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
95 ..
94 ..
96 """)
95 """)
97
96
98 class Writer(writers.Writer):
97 class Writer(writers.Writer):
99
98
100 supported = ('manpage')
99 supported = ('manpage')
101 """Formats this writer supports."""
100 """Formats this writer supports."""
102
101
103 output = None
102 output = None
104 """Final translated form of `document`."""
103 """Final translated form of `document`."""
105
104
106 def __init__(self):
105 def __init__(self):
107 writers.Writer.__init__(self)
106 writers.Writer.__init__(self)
108 self.translator_class = Translator
107 self.translator_class = Translator
109
108
110 def translate(self):
109 def translate(self):
111 visitor = self.translator_class(self.document)
110 visitor = self.translator_class(self.document)
112 self.document.walkabout(visitor)
111 self.document.walkabout(visitor)
113 self.output = visitor.astext()
112 self.output = visitor.astext()
114
113
115
114
116 class Table(object):
115 class Table(object):
117 def __init__(self):
116 def __init__(self):
118 self._rows = []
117 self._rows = []
119 self._options = ['center']
118 self._options = ['center']
120 self._tab_char = '\t'
119 self._tab_char = '\t'
121 self._coldefs = []
120 self._coldefs = []
122 def new_row(self):
121 def new_row(self):
123 self._rows.append([])
122 self._rows.append([])
124 def append_separator(self, separator):
123 def append_separator(self, separator):
125 """Append the separator for table head."""
124 """Append the separator for table head."""
126 self._rows.append([separator])
125 self._rows.append([separator])
127 def append_cell(self, cell_lines):
126 def append_cell(self, cell_lines):
128 """cell_lines is an array of lines"""
127 """cell_lines is an array of lines"""
129 start = 0
128 start = 0
130 if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
129 if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
131 start = 1
130 start = 1
132 self._rows[-1].append(cell_lines[start:])
131 self._rows[-1].append(cell_lines[start:])
133 if len(self._coldefs) < len(self._rows[-1]):
132 if len(self._coldefs) < len(self._rows[-1]):
134 self._coldefs.append('l')
133 self._coldefs.append('l')
135 def _minimize_cell(self, cell_lines):
134 def _minimize_cell(self, cell_lines):
136 """Remove leading and trailing blank and ``.sp`` lines"""
135 """Remove leading and trailing blank and ``.sp`` lines"""
137 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
136 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
138 del cell_lines[0]
137 del cell_lines[0]
139 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
138 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
140 del cell_lines[-1]
139 del cell_lines[-1]
141 def as_list(self):
140 def as_list(self):
142 text = ['.TS\n']
141 text = ['.TS\n']
143 text.append(' '.join(self._options) + ';\n')
142 text.append(' '.join(self._options) + ';\n')
144 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
143 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
145 for row in self._rows:
144 for row in self._rows:
146 # row = array of cells. cell = array of lines.
145 # row = array of cells. cell = array of lines.
147 text.append('_\n') # line above
146 text.append('_\n') # line above
148 text.append('T{\n')
147 text.append('T{\n')
149 for i in range(len(row)):
148 for i in range(len(row)):
150 cell = row[i]
149 cell = row[i]
151 self._minimize_cell(cell)
150 self._minimize_cell(cell)
152 text.extend(cell)
151 text.extend(cell)
153 if not text[-1].endswith('\n'):
152 if not text[-1].endswith('\n'):
154 text[-1] += '\n'
153 text[-1] += '\n'
155 if i < len(row) - 1:
154 if i < len(row) - 1:
156 text.append('T}'+self._tab_char+'T{\n')
155 text.append('T}'+self._tab_char+'T{\n')
157 else:
156 else:
158 text.append('T}\n')
157 text.append('T}\n')
159 text.append('_\n')
158 text.append('_\n')
160 text.append('.TE\n')
159 text.append('.TE\n')
161 return text
160 return text
162
161
163 class Translator(nodes.NodeVisitor):
162 class Translator(nodes.NodeVisitor):
164 """"""
163 """"""
165
164
166 words_and_spaces = re.compile(r'\S+| +|\n')
165 words_and_spaces = re.compile(r'\S+| +|\n')
167 document_start = """Man page generated from reStructuredText."""
166 document_start = """Man page generated from reStructuredText."""
168
167
169 def __init__(self, document):
168 def __init__(self, document):
170 nodes.NodeVisitor.__init__(self, document)
169 nodes.NodeVisitor.__init__(self, document)
171 self.settings = settings = document.settings
170 self.settings = settings = document.settings
172 lcode = settings.language_code
171 lcode = settings.language_code
173 arglen = len(inspect.getargspec(languages.get_language)[0])
172 arglen = len(inspect.getargspec(languages.get_language)[0])
174 if arglen == 2:
173 if arglen == 2:
175 self.language = languages.get_language(lcode,
174 self.language = languages.get_language(lcode,
176 self.document.reporter)
175 self.document.reporter)
177 else:
176 else:
178 self.language = languages.get_language(lcode)
177 self.language = languages.get_language(lcode)
179 self.head = []
178 self.head = []
180 self.body = []
179 self.body = []
181 self.foot = []
180 self.foot = []
182 self.section_level = 0
181 self.section_level = 0
183 self.context = []
182 self.context = []
184 self.topic_class = ''
183 self.topic_class = ''
185 self.colspecs = []
184 self.colspecs = []
186 self.compact_p = 1
185 self.compact_p = 1
187 self.compact_simple = None
186 self.compact_simple = None
188 # the list style "*" bullet or "#" numbered
187 # the list style "*" bullet or "#" numbered
189 self._list_char = []
188 self._list_char = []
190 # writing the header .TH and .SH NAME is postboned after
189 # writing the header .TH and .SH NAME is postboned after
191 # docinfo.
190 # docinfo.
192 self._docinfo = {
191 self._docinfo = {
193 "title" : "", "title_upper": "",
192 "title" : "", "title_upper": "",
194 "subtitle" : "",
193 "subtitle" : "",
195 "manual_section" : "", "manual_group" : "",
194 "manual_section" : "", "manual_group" : "",
196 "author" : [],
195 "author" : [],
197 "date" : "",
196 "date" : "",
198 "copyright" : "",
197 "copyright" : "",
199 "version" : "",
198 "version" : "",
200 }
199 }
201 self._docinfo_keys = [] # a list to keep the sequence as in source.
200 self._docinfo_keys = [] # a list to keep the sequence as in source.
202 self._docinfo_names = {} # to get name from text not normalized.
201 self._docinfo_names = {} # to get name from text not normalized.
203 self._in_docinfo = None
202 self._in_docinfo = None
204 self._active_table = None
203 self._active_table = None
205 self._in_literal = False
204 self._in_literal = False
206 self.header_written = 0
205 self.header_written = 0
207 self._line_block = 0
206 self._line_block = 0
208 self.authors = []
207 self.authors = []
209 self.section_level = 0
208 self.section_level = 0
210 self._indent = [0]
209 self._indent = [0]
211 # central definition of simple processing rules
210 # central definition of simple processing rules
212 # what to output on : visit, depart
211 # what to output on : visit, depart
213 # Do not use paragraph requests ``.PP`` because these set indentation.
212 # Do not use paragraph requests ``.PP`` because these set indentation.
214 # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
213 # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
215 #
214 #
216 # Fonts are put on a stack, the top one is used.
215 # Fonts are put on a stack, the top one is used.
217 # ``.ft P`` or ``\\fP`` pop from stack.
216 # ``.ft P`` or ``\\fP`` pop from stack.
218 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
217 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
219 # Hopefully ``C`` courier too.
218 # Hopefully ``C`` courier too.
220 self.defs = {
219 self.defs = {
221 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
220 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
222 'definition_list_item' : ('.TP', ''),
221 'definition_list_item' : ('.TP', ''),
223 'field_name' : ('.TP\n.B ', '\n'),
222 'field_name' : ('.TP\n.B ', '\n'),
224 'literal' : ('\\fB', '\\fP'),
223 'literal' : ('\\fB', '\\fP'),
225 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
224 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
226
225
227 'option_list_item' : ('.TP\n', ''),
226 'option_list_item' : ('.TP\n', ''),
228
227
229 'reference' : (r'\%', r'\:'),
228 'reference' : (r'\%', r'\:'),
230 'emphasis': ('\\fI', '\\fP'),
229 'emphasis': ('\\fI', '\\fP'),
231 'strong' : ('\\fB', '\\fP'),
230 'strong' : ('\\fB', '\\fP'),
232 'term' : ('\n.B ', '\n'),
231 'term' : ('\n.B ', '\n'),
233 'title_reference' : ('\\fI', '\\fP'),
232 'title_reference' : ('\\fI', '\\fP'),
234
233
235 'topic-title' : ('.SS ',),
234 'topic-title' : ('.SS ',),
236 'sidebar-title' : ('.SS ',),
235 'sidebar-title' : ('.SS ',),
237
236
238 'problematic' : ('\n.nf\n', '\n.fi\n'),
237 'problematic' : ('\n.nf\n', '\n.fi\n'),
239 }
238 }
240 # NOTE don't specify the newline before a dot-command, but ensure
239 # NOTE don't specify the newline before a dot-command, but ensure
241 # it is there.
240 # it is there.
242
241
243 def comment_begin(self, text):
242 def comment_begin(self, text):
244 """Return commented version of the passed text WITHOUT end of
243 """Return commented version of the passed text WITHOUT end of
245 line/comment."""
244 line/comment."""
246 prefix = '.\\" '
245 prefix = '.\\" '
247 out_text = ''.join(
246 out_text = ''.join(
248 [(prefix + in_line + '\n')
247 [(prefix + in_line + '\n')
249 for in_line in text.split('\n')])
248 for in_line in text.split('\n')])
250 return out_text
249 return out_text
251
250
252 def comment(self, text):
251 def comment(self, text):
253 """Return commented version of the passed text."""
252 """Return commented version of the passed text."""
254 return self.comment_begin(text)+'.\n'
253 return self.comment_begin(text)+'.\n'
255
254
256 def ensure_eol(self):
255 def ensure_eol(self):
257 """Ensure the last line in body is terminated by new line."""
256 """Ensure the last line in body is terminated by new line."""
258 if self.body[-1][-1] != '\n':
257 if self.body[-1][-1] != '\n':
259 self.body.append('\n')
258 self.body.append('\n')
260
259
261 def astext(self):
260 def astext(self):
262 """Return the final formatted document as a string."""
261 """Return the final formatted document as a string."""
263 if not self.header_written:
262 if not self.header_written:
264 # ensure we get a ".TH" as viewers require it.
263 # ensure we get a ".TH" as viewers require it.
265 self.head.append(self.header())
264 self.head.append(self.header())
266 # filter body
265 # filter body
267 for i in xrange(len(self.body) - 1, 0, -1):
266 for i in xrange(len(self.body) - 1, 0, -1):
268 # remove superfluous vertical gaps.
267 # remove superfluous vertical gaps.
269 if self.body[i] == '.sp\n':
268 if self.body[i] == '.sp\n':
270 if self.body[i - 1][:4] in ('.BI ','.IP '):
269 if self.body[i - 1][:4] in ('.BI ','.IP '):
271 self.body[i] = '.\n'
270 self.body[i] = '.\n'
272 elif (self.body[i - 1][:3] == '.B ' and
271 elif (self.body[i - 1][:3] == '.B ' and
273 self.body[i - 2][:4] == '.TP\n'):
272 self.body[i - 2][:4] == '.TP\n'):
274 self.body[i] = '.\n'
273 self.body[i] = '.\n'
275 elif (self.body[i - 1] == '\n' and
274 elif (self.body[i - 1] == '\n' and
276 self.body[i - 2][0] != '.' and
275 self.body[i - 2][0] != '.' and
277 (self.body[i - 3][:7] == '.TP\n.B '
276 (self.body[i - 3][:7] == '.TP\n.B '
278 or self.body[i - 3][:4] == '\n.B ')
277 or self.body[i - 3][:4] == '\n.B ')
279 ):
278 ):
280 self.body[i] = '.\n'
279 self.body[i] = '.\n'
281 return ''.join(self.head + self.body + self.foot)
280 return ''.join(self.head + self.body + self.foot)
282
281
283 def deunicode(self, text):
282 def deunicode(self, text):
284 text = text.replace(u'\xa0', '\\ ')
283 text = text.replace(u'\xa0', '\\ ')
285 text = text.replace(u'\u2020', '\\(dg')
284 text = text.replace(u'\u2020', '\\(dg')
286 return text
285 return text
287
286
288 def visit_Text(self, node):
287 def visit_Text(self, node):
289 text = node.astext()
288 text = node.astext()
290 text = text.replace('\\','\\e')
289 text = text.replace('\\','\\e')
291 replace_pairs = [
290 replace_pairs = [
292 (u'-', ur'\-'),
291 (u'-', u'\\-'),
293 (u'\'', ur'\(aq'),
292 (u"'", u'\\(aq'),
294 (u'´', ur'\''),
293 (u'´', u"\\'"),
295 (u'`', ur'\(ga'),
294 (u'`', u'\\(ga'),
296 ]
295 ]
297 for (in_char, out_markup) in replace_pairs:
296 for (in_char, out_markup) in replace_pairs:
298 text = text.replace(in_char, out_markup)
297 text = text.replace(in_char, out_markup)
299 # unicode
298 # unicode
300 text = self.deunicode(text)
299 text = self.deunicode(text)
301 if self._in_literal:
300 if self._in_literal:
302 # prevent interpretation of "." at line start
301 # prevent interpretation of "." at line start
303 if text[0] == '.':
302 if text[0] == '.':
304 text = '\\&' + text
303 text = '\\&' + text
305 text = text.replace('\n.', '\n\\&.')
304 text = text.replace('\n.', '\n\\&.')
306 self.body.append(text)
305 self.body.append(text)
307
306
308 def depart_Text(self, node):
307 def depart_Text(self, node):
309 pass
308 pass
310
309
311 def list_start(self, node):
310 def list_start(self, node):
312 class enum_char(object):
311 class enum_char(object):
313 enum_style = {
312 enum_style = {
314 'bullet' : '\\(bu',
313 'bullet' : '\\(bu',
315 'emdash' : '\\(em',
314 'emdash' : '\\(em',
316 }
315 }
317
316
318 def __init__(self, style):
317 def __init__(self, style):
319 self._style = style
318 self._style = style
320 if 'start' in node:
319 if 'start' in node:
321 self._cnt = node['start'] - 1
320 self._cnt = node['start'] - 1
322 else:
321 else:
323 self._cnt = 0
322 self._cnt = 0
324 self._indent = 2
323 self._indent = 2
325 if style == 'arabic':
324 if style == 'arabic':
326 # indentation depends on number of children
325 # indentation depends on number of children
327 # and start value.
326 # and start value.
328 self._indent = len(str(len(node.children)))
327 self._indent = len(str(len(node.children)))
329 self._indent += len(str(self._cnt)) + 1
328 self._indent += len(str(self._cnt)) + 1
330 elif style == 'loweralpha':
329 elif style == 'loweralpha':
331 self._cnt += ord('a') - 1
330 self._cnt += ord('a') - 1
332 self._indent = 3
331 self._indent = 3
333 elif style == 'upperalpha':
332 elif style == 'upperalpha':
334 self._cnt += ord('A') - 1
333 self._cnt += ord('A') - 1
335 self._indent = 3
334 self._indent = 3
336 elif style.endswith('roman'):
335 elif style.endswith('roman'):
337 self._indent = 5
336 self._indent = 5
338
337
339 def next(self):
338 def next(self):
340 if self._style == 'bullet':
339 if self._style == 'bullet':
341 return self.enum_style[self._style]
340 return self.enum_style[self._style]
342 elif self._style == 'emdash':
341 elif self._style == 'emdash':
343 return self.enum_style[self._style]
342 return self.enum_style[self._style]
344 self._cnt += 1
343 self._cnt += 1
345 # TODO add prefix postfix
344 # TODO add prefix postfix
346 if self._style == 'arabic':
345 if self._style == 'arabic':
347 return "%d." % self._cnt
346 return "%d." % self._cnt
348 elif self._style in ('loweralpha', 'upperalpha'):
347 elif self._style in ('loweralpha', 'upperalpha'):
349 return "%c." % self._cnt
348 return "%c." % self._cnt
350 elif self._style.endswith('roman'):
349 elif self._style.endswith('roman'):
351 res = roman.toRoman(self._cnt) + '.'
350 res = roman.toRoman(self._cnt) + '.'
352 if self._style.startswith('upper'):
351 if self._style.startswith('upper'):
353 return res.upper()
352 return res.upper()
354 return res.lower()
353 return res.lower()
355 else:
354 else:
356 return "%d." % self._cnt
355 return "%d." % self._cnt
357 def get_width(self):
356 def get_width(self):
358 return self._indent
357 return self._indent
359 def __repr__(self):
358 def __repr__(self):
360 return 'enum_style-%s' % list(self._style)
359 return 'enum_style-%s' % list(self._style)
361
360
362 if 'enumtype' in node:
361 if 'enumtype' in node:
363 self._list_char.append(enum_char(node['enumtype']))
362 self._list_char.append(enum_char(node['enumtype']))
364 else:
363 else:
365 self._list_char.append(enum_char('bullet'))
364 self._list_char.append(enum_char('bullet'))
366 if len(self._list_char) > 1:
365 if len(self._list_char) > 1:
367 # indent nested lists
366 # indent nested lists
368 self.indent(self._list_char[-2].get_width())
367 self.indent(self._list_char[-2].get_width())
369 else:
368 else:
370 self.indent(self._list_char[-1].get_width())
369 self.indent(self._list_char[-1].get_width())
371
370
372 def list_end(self):
371 def list_end(self):
373 self.dedent()
372 self.dedent()
374 self._list_char.pop()
373 self._list_char.pop()
375
374
376 def header(self):
375 def header(self):
377 tmpl = (".TH %(title_upper)s %(manual_section)s"
376 tmpl = (".TH %(title_upper)s %(manual_section)s"
378 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
377 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
379 ".SH NAME\n"
378 ".SH NAME\n"
380 "%(title)s \- %(subtitle)s\n")
379 "%(title)s \- %(subtitle)s\n")
381 return tmpl % self._docinfo
380 return tmpl % self._docinfo
382
381
383 def append_header(self):
382 def append_header(self):
384 """append header with .TH and .SH NAME"""
383 """append header with .TH and .SH NAME"""
385 # NOTE before everything
384 # NOTE before everything
386 # .TH title_upper section date source manual
385 # .TH title_upper section date source manual
387 if self.header_written:
386 if self.header_written:
388 return
387 return
389 self.body.append(self.header())
388 self.body.append(self.header())
390 self.body.append(MACRO_DEF)
389 self.body.append(MACRO_DEF)
391 self.header_written = 1
390 self.header_written = 1
392
391
393 def visit_address(self, node):
392 def visit_address(self, node):
394 self.visit_docinfo_item(node, 'address')
393 self.visit_docinfo_item(node, 'address')
395
394
396 def depart_address(self, node):
395 def depart_address(self, node):
397 pass
396 pass
398
397
399 def visit_admonition(self, node, name=None):
398 def visit_admonition(self, node, name=None):
400 if name:
399 if name:
401 self.body.append('.IP %s\n' %
400 self.body.append('.IP %s\n' %
402 self.language.labels.get(name, name))
401 self.language.labels.get(name, name))
403
402
404 def depart_admonition(self, node):
403 def depart_admonition(self, node):
405 self.body.append('.RE\n')
404 self.body.append('.RE\n')
406
405
407 def visit_attention(self, node):
406 def visit_attention(self, node):
408 self.visit_admonition(node, 'attention')
407 self.visit_admonition(node, 'attention')
409
408
410 depart_attention = depart_admonition
409 depart_attention = depart_admonition
411
410
412 def visit_docinfo_item(self, node, name):
411 def visit_docinfo_item(self, node, name):
413 if name == 'author':
412 if name == 'author':
414 self._docinfo[name].append(node.astext())
413 self._docinfo[name].append(node.astext())
415 else:
414 else:
416 self._docinfo[name] = node.astext()
415 self._docinfo[name] = node.astext()
417 self._docinfo_keys.append(name)
416 self._docinfo_keys.append(name)
418 raise nodes.SkipNode()
417 raise nodes.SkipNode()
419
418
420 def depart_docinfo_item(self, node):
419 def depart_docinfo_item(self, node):
421 pass
420 pass
422
421
423 def visit_author(self, node):
422 def visit_author(self, node):
424 self.visit_docinfo_item(node, 'author')
423 self.visit_docinfo_item(node, 'author')
425
424
426 depart_author = depart_docinfo_item
425 depart_author = depart_docinfo_item
427
426
428 def visit_authors(self, node):
427 def visit_authors(self, node):
429 # _author is called anyway.
428 # _author is called anyway.
430 pass
429 pass
431
430
432 def depart_authors(self, node):
431 def depart_authors(self, node):
433 pass
432 pass
434
433
435 def visit_block_quote(self, node):
434 def visit_block_quote(self, node):
436 # BUG/HACK: indent always uses the _last_ indentation,
435 # BUG/HACK: indent always uses the _last_ indentation,
437 # thus we need two of them.
436 # thus we need two of them.
438 self.indent(BLOCKQOUTE_INDENT)
437 self.indent(BLOCKQOUTE_INDENT)
439 self.indent(0)
438 self.indent(0)
440
439
441 def depart_block_quote(self, node):
440 def depart_block_quote(self, node):
442 self.dedent()
441 self.dedent()
443 self.dedent()
442 self.dedent()
444
443
445 def visit_bullet_list(self, node):
444 def visit_bullet_list(self, node):
446 self.list_start(node)
445 self.list_start(node)
447
446
448 def depart_bullet_list(self, node):
447 def depart_bullet_list(self, node):
449 self.list_end()
448 self.list_end()
450
449
451 def visit_caption(self, node):
450 def visit_caption(self, node):
452 pass
451 pass
453
452
454 def depart_caption(self, node):
453 def depart_caption(self, node):
455 pass
454 pass
456
455
457 def visit_caution(self, node):
456 def visit_caution(self, node):
458 self.visit_admonition(node, 'caution')
457 self.visit_admonition(node, 'caution')
459
458
460 depart_caution = depart_admonition
459 depart_caution = depart_admonition
461
460
462 def visit_citation(self, node):
461 def visit_citation(self, node):
463 num, text = node.astext().split(None, 1)
462 num, text = node.astext().split(None, 1)
464 num = num.strip()
463 num = num.strip()
465 self.body.append('.IP [%s] 5\n' % num)
464 self.body.append('.IP [%s] 5\n' % num)
466
465
467 def depart_citation(self, node):
466 def depart_citation(self, node):
468 pass
467 pass
469
468
470 def visit_citation_reference(self, node):
469 def visit_citation_reference(self, node):
471 self.body.append('['+node.astext()+']')
470 self.body.append('['+node.astext()+']')
472 raise nodes.SkipNode()
471 raise nodes.SkipNode()
473
472
474 def visit_classifier(self, node):
473 def visit_classifier(self, node):
475 pass
474 pass
476
475
477 def depart_classifier(self, node):
476 def depart_classifier(self, node):
478 pass
477 pass
479
478
480 def visit_colspec(self, node):
479 def visit_colspec(self, node):
481 self.colspecs.append(node)
480 self.colspecs.append(node)
482
481
483 def depart_colspec(self, node):
482 def depart_colspec(self, node):
484 pass
483 pass
485
484
486 def write_colspecs(self):
485 def write_colspecs(self):
487 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
486 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
488
487
489 def visit_comment(self, node,
488 def visit_comment(self, node,
490 sub=re.compile('-(?=-)').sub):
489 sub=re.compile('-(?=-)').sub):
491 self.body.append(self.comment(node.astext()))
490 self.body.append(self.comment(node.astext()))
492 raise nodes.SkipNode()
491 raise nodes.SkipNode()
493
492
494 def visit_contact(self, node):
493 def visit_contact(self, node):
495 self.visit_docinfo_item(node, 'contact')
494 self.visit_docinfo_item(node, 'contact')
496
495
497 depart_contact = depart_docinfo_item
496 depart_contact = depart_docinfo_item
498
497
499 def visit_container(self, node):
498 def visit_container(self, node):
500 pass
499 pass
501
500
502 def depart_container(self, node):
501 def depart_container(self, node):
503 pass
502 pass
504
503
505 def visit_compound(self, node):
504 def visit_compound(self, node):
506 pass
505 pass
507
506
508 def depart_compound(self, node):
507 def depart_compound(self, node):
509 pass
508 pass
510
509
511 def visit_copyright(self, node):
510 def visit_copyright(self, node):
512 self.visit_docinfo_item(node, 'copyright')
511 self.visit_docinfo_item(node, 'copyright')
513
512
514 def visit_danger(self, node):
513 def visit_danger(self, node):
515 self.visit_admonition(node, 'danger')
514 self.visit_admonition(node, 'danger')
516
515
517 depart_danger = depart_admonition
516 depart_danger = depart_admonition
518
517
519 def visit_date(self, node):
518 def visit_date(self, node):
520 self.visit_docinfo_item(node, 'date')
519 self.visit_docinfo_item(node, 'date')
521
520
522 def visit_decoration(self, node):
521 def visit_decoration(self, node):
523 pass
522 pass
524
523
525 def depart_decoration(self, node):
524 def depart_decoration(self, node):
526 pass
525 pass
527
526
528 def visit_definition(self, node):
527 def visit_definition(self, node):
529 pass
528 pass
530
529
531 def depart_definition(self, node):
530 def depart_definition(self, node):
532 pass
531 pass
533
532
534 def visit_definition_list(self, node):
533 def visit_definition_list(self, node):
535 self.indent(DEFINITION_LIST_INDENT)
534 self.indent(DEFINITION_LIST_INDENT)
536
535
537 def depart_definition_list(self, node):
536 def depart_definition_list(self, node):
538 self.dedent()
537 self.dedent()
539
538
540 def visit_definition_list_item(self, node):
539 def visit_definition_list_item(self, node):
541 self.body.append(self.defs['definition_list_item'][0])
540 self.body.append(self.defs['definition_list_item'][0])
542
541
543 def depart_definition_list_item(self, node):
542 def depart_definition_list_item(self, node):
544 self.body.append(self.defs['definition_list_item'][1])
543 self.body.append(self.defs['definition_list_item'][1])
545
544
546 def visit_description(self, node):
545 def visit_description(self, node):
547 pass
546 pass
548
547
549 def depart_description(self, node):
548 def depart_description(self, node):
550 pass
549 pass
551
550
552 def visit_docinfo(self, node):
551 def visit_docinfo(self, node):
553 self._in_docinfo = 1
552 self._in_docinfo = 1
554
553
555 def depart_docinfo(self, node):
554 def depart_docinfo(self, node):
556 self._in_docinfo = None
555 self._in_docinfo = None
557 # NOTE nothing should be written before this
556 # NOTE nothing should be written before this
558 self.append_header()
557 self.append_header()
559
558
560 def visit_doctest_block(self, node):
559 def visit_doctest_block(self, node):
561 self.body.append(self.defs['literal_block'][0])
560 self.body.append(self.defs['literal_block'][0])
562 self._in_literal = True
561 self._in_literal = True
563
562
564 def depart_doctest_block(self, node):
563 def depart_doctest_block(self, node):
565 self._in_literal = False
564 self._in_literal = False
566 self.body.append(self.defs['literal_block'][1])
565 self.body.append(self.defs['literal_block'][1])
567
566
568 def visit_document(self, node):
567 def visit_document(self, node):
569 # no blank line between comment and header.
568 # no blank line between comment and header.
570 self.body.append(self.comment(self.document_start).rstrip()+'\n')
569 self.body.append(self.comment(self.document_start).rstrip()+'\n')
571 # writing header is postboned
570 # writing header is postboned
572 self.header_written = 0
571 self.header_written = 0
573
572
574 def depart_document(self, node):
573 def depart_document(self, node):
575 if self._docinfo['author']:
574 if self._docinfo['author']:
576 self.body.append('.SH AUTHOR\n%s\n'
575 self.body.append('.SH AUTHOR\n%s\n'
577 % ', '.join(self._docinfo['author']))
576 % ', '.join(self._docinfo['author']))
578 skip = ('author', 'copyright', 'date',
577 skip = ('author', 'copyright', 'date',
579 'manual_group', 'manual_section',
578 'manual_group', 'manual_section',
580 'subtitle',
579 'subtitle',
581 'title', 'title_upper', 'version')
580 'title', 'title_upper', 'version')
582 for name in self._docinfo_keys:
581 for name in self._docinfo_keys:
583 if name == 'address':
582 if name == 'address':
584 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
583 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
585 self.language.labels.get(name, name),
584 self.language.labels.get(name, name),
586 self.defs['indent'][0] % 0,
585 self.defs['indent'][0] % 0,
587 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
586 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
588 self._docinfo[name],
587 self._docinfo[name],
589 self.defs['indent'][1],
588 self.defs['indent'][1],
590 self.defs['indent'][1]))
589 self.defs['indent'][1]))
591 elif name not in skip:
590 elif name not in skip:
592 if name in self._docinfo_names:
591 if name in self._docinfo_names:
593 label = self._docinfo_names[name]
592 label = self._docinfo_names[name]
594 else:
593 else:
595 label = self.language.labels.get(name, name)
594 label = self.language.labels.get(name, name)
596 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
595 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
597 if self._docinfo['copyright']:
596 if self._docinfo['copyright']:
598 self.body.append('.SH COPYRIGHT\n%s\n'
597 self.body.append('.SH COPYRIGHT\n%s\n'
599 % self._docinfo['copyright'])
598 % self._docinfo['copyright'])
600 self.body.append(self.comment(
599 self.body.append(self.comment(
601 'Generated by docutils manpage writer.\n'))
600 'Generated by docutils manpage writer.\n'))
602
601
603 def visit_emphasis(self, node):
602 def visit_emphasis(self, node):
604 self.body.append(self.defs['emphasis'][0])
603 self.body.append(self.defs['emphasis'][0])
605
604
606 def depart_emphasis(self, node):
605 def depart_emphasis(self, node):
607 self.body.append(self.defs['emphasis'][1])
606 self.body.append(self.defs['emphasis'][1])
608
607
609 def visit_entry(self, node):
608 def visit_entry(self, node):
610 # a cell in a table row
609 # a cell in a table row
611 if 'morerows' in node:
610 if 'morerows' in node:
612 self.document.reporter.warning('"table row spanning" not supported',
611 self.document.reporter.warning('"table row spanning" not supported',
613 base_node=node)
612 base_node=node)
614 if 'morecols' in node:
613 if 'morecols' in node:
615 self.document.reporter.warning(
614 self.document.reporter.warning(
616 '"table cell spanning" not supported', base_node=node)
615 '"table cell spanning" not supported', base_node=node)
617 self.context.append(len(self.body))
616 self.context.append(len(self.body))
618
617
619 def depart_entry(self, node):
618 def depart_entry(self, node):
620 start = self.context.pop()
619 start = self.context.pop()
621 self._active_table.append_cell(self.body[start:])
620 self._active_table.append_cell(self.body[start:])
622 del self.body[start:]
621 del self.body[start:]
623
622
624 def visit_enumerated_list(self, node):
623 def visit_enumerated_list(self, node):
625 self.list_start(node)
624 self.list_start(node)
626
625
627 def depart_enumerated_list(self, node):
626 def depart_enumerated_list(self, node):
628 self.list_end()
627 self.list_end()
629
628
630 def visit_error(self, node):
629 def visit_error(self, node):
631 self.visit_admonition(node, 'error')
630 self.visit_admonition(node, 'error')
632
631
633 depart_error = depart_admonition
632 depart_error = depart_admonition
634
633
635 def visit_field(self, node):
634 def visit_field(self, node):
636 pass
635 pass
637
636
638 def depart_field(self, node):
637 def depart_field(self, node):
639 pass
638 pass
640
639
641 def visit_field_body(self, node):
640 def visit_field_body(self, node):
642 if self._in_docinfo:
641 if self._in_docinfo:
643 name_normalized = self._field_name.lower().replace(" ","_")
642 name_normalized = self._field_name.lower().replace(" ","_")
644 self._docinfo_names[name_normalized] = self._field_name
643 self._docinfo_names[name_normalized] = self._field_name
645 self.visit_docinfo_item(node, name_normalized)
644 self.visit_docinfo_item(node, name_normalized)
646 raise nodes.SkipNode()
645 raise nodes.SkipNode()
647
646
648 def depart_field_body(self, node):
647 def depart_field_body(self, node):
649 pass
648 pass
650
649
651 def visit_field_list(self, node):
650 def visit_field_list(self, node):
652 self.indent(FIELD_LIST_INDENT)
651 self.indent(FIELD_LIST_INDENT)
653
652
654 def depart_field_list(self, node):
653 def depart_field_list(self, node):
655 self.dedent()
654 self.dedent()
656
655
657 def visit_field_name(self, node):
656 def visit_field_name(self, node):
658 if self._in_docinfo:
657 if self._in_docinfo:
659 self._field_name = node.astext()
658 self._field_name = node.astext()
660 raise nodes.SkipNode()
659 raise nodes.SkipNode()
661 else:
660 else:
662 self.body.append(self.defs['field_name'][0])
661 self.body.append(self.defs['field_name'][0])
663
662
664 def depart_field_name(self, node):
663 def depart_field_name(self, node):
665 self.body.append(self.defs['field_name'][1])
664 self.body.append(self.defs['field_name'][1])
666
665
667 def visit_figure(self, node):
666 def visit_figure(self, node):
668 self.indent(2.5)
667 self.indent(2.5)
669 self.indent(0)
668 self.indent(0)
670
669
671 def depart_figure(self, node):
670 def depart_figure(self, node):
672 self.dedent()
671 self.dedent()
673 self.dedent()
672 self.dedent()
674
673
675 def visit_footer(self, node):
674 def visit_footer(self, node):
676 self.document.reporter.warning('"footer" not supported',
675 self.document.reporter.warning('"footer" not supported',
677 base_node=node)
676 base_node=node)
678
677
679 def depart_footer(self, node):
678 def depart_footer(self, node):
680 pass
679 pass
681
680
682 def visit_footnote(self, node):
681 def visit_footnote(self, node):
683 num, text = node.astext().split(None, 1)
682 num, text = node.astext().split(None, 1)
684 num = num.strip()
683 num = num.strip()
685 self.body.append('.IP [%s] 5\n' % self.deunicode(num))
684 self.body.append('.IP [%s] 5\n' % self.deunicode(num))
686
685
687 def depart_footnote(self, node):
686 def depart_footnote(self, node):
688 pass
687 pass
689
688
690 def footnote_backrefs(self, node):
689 def footnote_backrefs(self, node):
691 self.document.reporter.warning('"footnote_backrefs" not supported',
690 self.document.reporter.warning('"footnote_backrefs" not supported',
692 base_node=node)
691 base_node=node)
693
692
694 def visit_footnote_reference(self, node):
693 def visit_footnote_reference(self, node):
695 self.body.append('['+self.deunicode(node.astext())+']')
694 self.body.append('['+self.deunicode(node.astext())+']')
696 raise nodes.SkipNode()
695 raise nodes.SkipNode()
697
696
698 def depart_footnote_reference(self, node):
697 def depart_footnote_reference(self, node):
699 pass
698 pass
700
699
701 def visit_generated(self, node):
700 def visit_generated(self, node):
702 pass
701 pass
703
702
704 def depart_generated(self, node):
703 def depart_generated(self, node):
705 pass
704 pass
706
705
707 def visit_header(self, node):
706 def visit_header(self, node):
708 raise NotImplementedError(node.astext())
707 raise NotImplementedError(node.astext())
709
708
710 def depart_header(self, node):
709 def depart_header(self, node):
711 pass
710 pass
712
711
713 def visit_hint(self, node):
712 def visit_hint(self, node):
714 self.visit_admonition(node, 'hint')
713 self.visit_admonition(node, 'hint')
715
714
716 depart_hint = depart_admonition
715 depart_hint = depart_admonition
717
716
718 def visit_subscript(self, node):
717 def visit_subscript(self, node):
719 self.body.append('\\s-2\\d')
718 self.body.append('\\s-2\\d')
720
719
721 def depart_subscript(self, node):
720 def depart_subscript(self, node):
722 self.body.append('\\u\\s0')
721 self.body.append('\\u\\s0')
723
722
724 def visit_superscript(self, node):
723 def visit_superscript(self, node):
725 self.body.append('\\s-2\\u')
724 self.body.append('\\s-2\\u')
726
725
727 def depart_superscript(self, node):
726 def depart_superscript(self, node):
728 self.body.append('\\d\\s0')
727 self.body.append('\\d\\s0')
729
728
730 def visit_attribution(self, node):
729 def visit_attribution(self, node):
731 self.body.append('\\(em ')
730 self.body.append('\\(em ')
732
731
733 def depart_attribution(self, node):
732 def depart_attribution(self, node):
734 self.body.append('\n')
733 self.body.append('\n')
735
734
736 def visit_image(self, node):
735 def visit_image(self, node):
737 self.document.reporter.warning('"image" not supported',
736 self.document.reporter.warning('"image" not supported',
738 base_node=node)
737 base_node=node)
739 text = []
738 text = []
740 if 'alt' in node.attributes:
739 if 'alt' in node.attributes:
741 text.append(node.attributes['alt'])
740 text.append(node.attributes['alt'])
742 if 'uri' in node.attributes:
741 if 'uri' in node.attributes:
743 text.append(node.attributes['uri'])
742 text.append(node.attributes['uri'])
744 self.body.append('[image: %s]\n' % ('/'.join(text)))
743 self.body.append('[image: %s]\n' % ('/'.join(text)))
745 raise nodes.SkipNode()
744 raise nodes.SkipNode()
746
745
747 def visit_important(self, node):
746 def visit_important(self, node):
748 self.visit_admonition(node, 'important')
747 self.visit_admonition(node, 'important')
749
748
750 depart_important = depart_admonition
749 depart_important = depart_admonition
751
750
752 def visit_label(self, node):
751 def visit_label(self, node):
753 # footnote and citation
752 # footnote and citation
754 if (isinstance(node.parent, nodes.footnote)
753 if (isinstance(node.parent, nodes.footnote)
755 or isinstance(node.parent, nodes.citation)):
754 or isinstance(node.parent, nodes.citation)):
756 raise nodes.SkipNode()
755 raise nodes.SkipNode()
757 self.document.reporter.warning('"unsupported "label"',
756 self.document.reporter.warning('"unsupported "label"',
758 base_node=node)
757 base_node=node)
759 self.body.append('[')
758 self.body.append('[')
760
759
761 def depart_label(self, node):
760 def depart_label(self, node):
762 self.body.append(']\n')
761 self.body.append(']\n')
763
762
764 def visit_legend(self, node):
763 def visit_legend(self, node):
765 pass
764 pass
766
765
767 def depart_legend(self, node):
766 def depart_legend(self, node):
768 pass
767 pass
769
768
770 # WHAT should we use .INDENT, .UNINDENT ?
769 # WHAT should we use .INDENT, .UNINDENT ?
771 def visit_line_block(self, node):
770 def visit_line_block(self, node):
772 self._line_block += 1
771 self._line_block += 1
773 if self._line_block == 1:
772 if self._line_block == 1:
774 self.body.append('.sp\n')
773 self.body.append('.sp\n')
775 self.body.append('.nf\n')
774 self.body.append('.nf\n')
776 else:
775 else:
777 self.body.append('.in +2\n')
776 self.body.append('.in +2\n')
778
777
779 def depart_line_block(self, node):
778 def depart_line_block(self, node):
780 self._line_block -= 1
779 self._line_block -= 1
781 if self._line_block == 0:
780 if self._line_block == 0:
782 self.body.append('.fi\n')
781 self.body.append('.fi\n')
783 self.body.append('.sp\n')
782 self.body.append('.sp\n')
784 else:
783 else:
785 self.body.append('.in -2\n')
784 self.body.append('.in -2\n')
786
785
787 def visit_line(self, node):
786 def visit_line(self, node):
788 pass
787 pass
789
788
790 def depart_line(self, node):
789 def depart_line(self, node):
791 self.body.append('\n')
790 self.body.append('\n')
792
791
793 def visit_list_item(self, node):
792 def visit_list_item(self, node):
794 # man 7 man argues to use ".IP" instead of ".TP"
793 # man 7 man argues to use ".IP" instead of ".TP"
795 self.body.append('.IP %s %d\n' % (
794 self.body.append('.IP %s %d\n' % (
796 next(self._list_char[-1]),
795 next(self._list_char[-1]),
797 self._list_char[-1].get_width(),))
796 self._list_char[-1].get_width(),))
798
797
799 def depart_list_item(self, node):
798 def depart_list_item(self, node):
800 pass
799 pass
801
800
802 def visit_literal(self, node):
801 def visit_literal(self, node):
803 self.body.append(self.defs['literal'][0])
802 self.body.append(self.defs['literal'][0])
804
803
805 def depart_literal(self, node):
804 def depart_literal(self, node):
806 self.body.append(self.defs['literal'][1])
805 self.body.append(self.defs['literal'][1])
807
806
808 def visit_literal_block(self, node):
807 def visit_literal_block(self, node):
809 self.body.append(self.defs['literal_block'][0])
808 self.body.append(self.defs['literal_block'][0])
810 self._in_literal = True
809 self._in_literal = True
811
810
812 def depart_literal_block(self, node):
811 def depart_literal_block(self, node):
813 self._in_literal = False
812 self._in_literal = False
814 self.body.append(self.defs['literal_block'][1])
813 self.body.append(self.defs['literal_block'][1])
815
814
816 def visit_meta(self, node):
815 def visit_meta(self, node):
817 raise NotImplementedError(node.astext())
816 raise NotImplementedError(node.astext())
818
817
819 def depart_meta(self, node):
818 def depart_meta(self, node):
820 pass
819 pass
821
820
822 def visit_note(self, node):
821 def visit_note(self, node):
823 self.visit_admonition(node, 'note')
822 self.visit_admonition(node, 'note')
824
823
825 depart_note = depart_admonition
824 depart_note = depart_admonition
826
825
827 def indent(self, by=0.5):
826 def indent(self, by=0.5):
828 # if we are in a section ".SH" there already is a .RS
827 # if we are in a section ".SH" there already is a .RS
829 step = self._indent[-1]
828 step = self._indent[-1]
830 self._indent.append(by)
829 self._indent.append(by)
831 self.body.append(self.defs['indent'][0] % step)
830 self.body.append(self.defs['indent'][0] % step)
832
831
833 def dedent(self):
832 def dedent(self):
834 self._indent.pop()
833 self._indent.pop()
835 self.body.append(self.defs['indent'][1])
834 self.body.append(self.defs['indent'][1])
836
835
837 def visit_option_list(self, node):
836 def visit_option_list(self, node):
838 self.indent(OPTION_LIST_INDENT)
837 self.indent(OPTION_LIST_INDENT)
839
838
840 def depart_option_list(self, node):
839 def depart_option_list(self, node):
841 self.dedent()
840 self.dedent()
842
841
843 def visit_option_list_item(self, node):
842 def visit_option_list_item(self, node):
844 # one item of the list
843 # one item of the list
845 self.body.append(self.defs['option_list_item'][0])
844 self.body.append(self.defs['option_list_item'][0])
846
845
847 def depart_option_list_item(self, node):
846 def depart_option_list_item(self, node):
848 self.body.append(self.defs['option_list_item'][1])
847 self.body.append(self.defs['option_list_item'][1])
849
848
850 def visit_option_group(self, node):
849 def visit_option_group(self, node):
851 # as one option could have several forms it is a group
850 # as one option could have several forms it is a group
852 # options without parameter bold only, .B, -v
851 # options without parameter bold only, .B, -v
853 # options with parameter bold italic, .BI, -f file
852 # options with parameter bold italic, .BI, -f file
854 #
853 #
855 # we do not know if .B or .BI
854 # we do not know if .B or .BI
856 self.context.append('.B') # blind guess
855 self.context.append('.B') # blind guess
857 self.context.append(len(self.body)) # to be able to insert later
856 self.context.append(len(self.body)) # to be able to insert later
858 self.context.append(0) # option counter
857 self.context.append(0) # option counter
859
858
860 def depart_option_group(self, node):
859 def depart_option_group(self, node):
861 self.context.pop() # the counter
860 self.context.pop() # the counter
862 start_position = self.context.pop()
861 start_position = self.context.pop()
863 text = self.body[start_position:]
862 text = self.body[start_position:]
864 del self.body[start_position:]
863 del self.body[start_position:]
865 self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
864 self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
866
865
867 def visit_option(self, node):
866 def visit_option(self, node):
868 # each form of the option will be presented separately
867 # each form of the option will be presented separately
869 if self.context[-1] > 0:
868 if self.context[-1] > 0:
870 self.body.append(', ')
869 self.body.append(', ')
871 if self.context[-3] == '.BI':
870 if self.context[-3] == '.BI':
872 self.body.append('\\')
871 self.body.append('\\')
873 self.body.append(' ')
872 self.body.append(' ')
874
873
875 def depart_option(self, node):
874 def depart_option(self, node):
876 self.context[-1] += 1
875 self.context[-1] += 1
877
876
878 def visit_option_string(self, node):
877 def visit_option_string(self, node):
879 # do not know if .B or .BI
878 # do not know if .B or .BI
880 pass
879 pass
881
880
882 def depart_option_string(self, node):
881 def depart_option_string(self, node):
883 pass
882 pass
884
883
885 def visit_option_argument(self, node):
884 def visit_option_argument(self, node):
886 self.context[-3] = '.BI' # bold/italic alternate
885 self.context[-3] = '.BI' # bold/italic alternate
887 if node['delimiter'] != ' ':
886 if node['delimiter'] != ' ':
888 self.body.append('\\fB%s ' % node['delimiter'])
887 self.body.append('\\fB%s ' % node['delimiter'])
889 elif self.body[len(self.body) - 1].endswith('='):
888 elif self.body[len(self.body) - 1].endswith('='):
890 # a blank only means no blank in output, just changing font
889 # a blank only means no blank in output, just changing font
891 self.body.append(' ')
890 self.body.append(' ')
892 else:
891 else:
893 # blank backslash blank, switch font then a blank
892 # blank backslash blank, switch font then a blank
894 self.body.append(' \\ ')
893 self.body.append(' \\ ')
895
894
896 def depart_option_argument(self, node):
895 def depart_option_argument(self, node):
897 pass
896 pass
898
897
899 def visit_organization(self, node):
898 def visit_organization(self, node):
900 self.visit_docinfo_item(node, 'organization')
899 self.visit_docinfo_item(node, 'organization')
901
900
902 def depart_organization(self, node):
901 def depart_organization(self, node):
903 pass
902 pass
904
903
905 def visit_paragraph(self, node):
904 def visit_paragraph(self, node):
906 # ``.PP`` : Start standard indented paragraph.
905 # ``.PP`` : Start standard indented paragraph.
907 # ``.LP`` : Start block paragraph, all except the first.
906 # ``.LP`` : Start block paragraph, all except the first.
908 # ``.P [type]`` : Start paragraph type.
907 # ``.P [type]`` : Start paragraph type.
909 # NOTE don't use paragraph starts because they reset indentation.
908 # NOTE don't use paragraph starts because they reset indentation.
910 # ``.sp`` is only vertical space
909 # ``.sp`` is only vertical space
911 self.ensure_eol()
910 self.ensure_eol()
912 self.body.append('.sp\n')
911 self.body.append('.sp\n')
913
912
914 def depart_paragraph(self, node):
913 def depart_paragraph(self, node):
915 self.body.append('\n')
914 self.body.append('\n')
916
915
917 def visit_problematic(self, node):
916 def visit_problematic(self, node):
918 self.body.append(self.defs['problematic'][0])
917 self.body.append(self.defs['problematic'][0])
919
918
920 def depart_problematic(self, node):
919 def depart_problematic(self, node):
921 self.body.append(self.defs['problematic'][1])
920 self.body.append(self.defs['problematic'][1])
922
921
923 def visit_raw(self, node):
922 def visit_raw(self, node):
924 if node.get('format') == 'manpage':
923 if node.get('format') == 'manpage':
925 self.body.append(node.astext() + "\n")
924 self.body.append(node.astext() + "\n")
926 # Keep non-manpage raw text out of output:
925 # Keep non-manpage raw text out of output:
927 raise nodes.SkipNode()
926 raise nodes.SkipNode()
928
927
929 def visit_reference(self, node):
928 def visit_reference(self, node):
930 """E.g. link or email address."""
929 """E.g. link or email address."""
931 self.body.append(self.defs['reference'][0])
930 self.body.append(self.defs['reference'][0])
932
931
933 def depart_reference(self, node):
932 def depart_reference(self, node):
934 self.body.append(self.defs['reference'][1])
933 self.body.append(self.defs['reference'][1])
935
934
936 def visit_revision(self, node):
935 def visit_revision(self, node):
937 self.visit_docinfo_item(node, 'revision')
936 self.visit_docinfo_item(node, 'revision')
938
937
939 depart_revision = depart_docinfo_item
938 depart_revision = depart_docinfo_item
940
939
941 def visit_row(self, node):
940 def visit_row(self, node):
942 self._active_table.new_row()
941 self._active_table.new_row()
943
942
944 def depart_row(self, node):
943 def depart_row(self, node):
945 pass
944 pass
946
945
947 def visit_section(self, node):
946 def visit_section(self, node):
948 self.section_level += 1
947 self.section_level += 1
949
948
950 def depart_section(self, node):
949 def depart_section(self, node):
951 self.section_level -= 1
950 self.section_level -= 1
952
951
953 def visit_status(self, node):
952 def visit_status(self, node):
954 self.visit_docinfo_item(node, 'status')
953 self.visit_docinfo_item(node, 'status')
955
954
956 depart_status = depart_docinfo_item
955 depart_status = depart_docinfo_item
957
956
958 def visit_strong(self, node):
957 def visit_strong(self, node):
959 self.body.append(self.defs['strong'][0])
958 self.body.append(self.defs['strong'][0])
960
959
961 def depart_strong(self, node):
960 def depart_strong(self, node):
962 self.body.append(self.defs['strong'][1])
961 self.body.append(self.defs['strong'][1])
963
962
964 def visit_substitution_definition(self, node):
963 def visit_substitution_definition(self, node):
965 """Internal only."""
964 """Internal only."""
966 raise nodes.SkipNode()
965 raise nodes.SkipNode()
967
966
968 def visit_substitution_reference(self, node):
967 def visit_substitution_reference(self, node):
969 self.document.reporter.warning('"substitution_reference" not supported',
968 self.document.reporter.warning('"substitution_reference" not supported',
970 base_node=node)
969 base_node=node)
971
970
972 def visit_subtitle(self, node):
971 def visit_subtitle(self, node):
973 if isinstance(node.parent, nodes.sidebar):
972 if isinstance(node.parent, nodes.sidebar):
974 self.body.append(self.defs['strong'][0])
973 self.body.append(self.defs['strong'][0])
975 elif isinstance(node.parent, nodes.document):
974 elif isinstance(node.parent, nodes.document):
976 self.visit_docinfo_item(node, 'subtitle')
975 self.visit_docinfo_item(node, 'subtitle')
977 elif isinstance(node.parent, nodes.section):
976 elif isinstance(node.parent, nodes.section):
978 self.body.append(self.defs['strong'][0])
977 self.body.append(self.defs['strong'][0])
979
978
980 def depart_subtitle(self, node):
979 def depart_subtitle(self, node):
981 # document subtitle calls SkipNode
980 # document subtitle calls SkipNode
982 self.body.append(self.defs['strong'][1]+'\n.PP\n')
981 self.body.append(self.defs['strong'][1]+'\n.PP\n')
983
982
984 def visit_system_message(self, node):
983 def visit_system_message(self, node):
985 # TODO add report_level
984 # TODO add report_level
986 #if node['level'] < self.document.reporter['writer'].report_level:
985 #if node['level'] < self.document.reporter['writer'].report_level:
987 # Level is too low to display:
986 # Level is too low to display:
988 # raise nodes.SkipNode
987 # raise nodes.SkipNode
989 attr = {}
988 attr = {}
990 if node.hasattr('id'):
989 if node.hasattr('id'):
991 attr['name'] = node['id']
990 attr['name'] = node['id']
992 if node.hasattr('line'):
991 if node.hasattr('line'):
993 line = ', line %s' % node['line']
992 line = ', line %s' % node['line']
994 else:
993 else:
995 line = ''
994 line = ''
996 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
995 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
997 % (node['type'], node['level'], node['source'], line))
996 % (node['type'], node['level'], node['source'], line))
998
997
999 def depart_system_message(self, node):
998 def depart_system_message(self, node):
1000 pass
999 pass
1001
1000
1002 def visit_table(self, node):
1001 def visit_table(self, node):
1003 self._active_table = Table()
1002 self._active_table = Table()
1004
1003
1005 def depart_table(self, node):
1004 def depart_table(self, node):
1006 self.ensure_eol()
1005 self.ensure_eol()
1007 self.body.extend(self._active_table.as_list())
1006 self.body.extend(self._active_table.as_list())
1008 self._active_table = None
1007 self._active_table = None
1009
1008
1010 def visit_target(self, node):
1009 def visit_target(self, node):
1011 # targets are in-document hyper targets, without any use for man-pages.
1010 # targets are in-document hyper targets, without any use for man-pages.
1012 raise nodes.SkipNode()
1011 raise nodes.SkipNode()
1013
1012
1014 def visit_tbody(self, node):
1013 def visit_tbody(self, node):
1015 pass
1014 pass
1016
1015
1017 def depart_tbody(self, node):
1016 def depart_tbody(self, node):
1018 pass
1017 pass
1019
1018
1020 def visit_term(self, node):
1019 def visit_term(self, node):
1021 self.body.append(self.defs['term'][0])
1020 self.body.append(self.defs['term'][0])
1022
1021
1023 def depart_term(self, node):
1022 def depart_term(self, node):
1024 self.body.append(self.defs['term'][1])
1023 self.body.append(self.defs['term'][1])
1025
1024
1026 def visit_tgroup(self, node):
1025 def visit_tgroup(self, node):
1027 pass
1026 pass
1028
1027
1029 def depart_tgroup(self, node):
1028 def depart_tgroup(self, node):
1030 pass
1029 pass
1031
1030
1032 def visit_thead(self, node):
1031 def visit_thead(self, node):
1033 # MAYBE double line '='
1032 # MAYBE double line '='
1034 pass
1033 pass
1035
1034
1036 def depart_thead(self, node):
1035 def depart_thead(self, node):
1037 # MAYBE double line '='
1036 # MAYBE double line '='
1038 pass
1037 pass
1039
1038
1040 def visit_tip(self, node):
1039 def visit_tip(self, node):
1041 self.visit_admonition(node, 'tip')
1040 self.visit_admonition(node, 'tip')
1042
1041
1043 depart_tip = depart_admonition
1042 depart_tip = depart_admonition
1044
1043
1045 def visit_title(self, node):
1044 def visit_title(self, node):
1046 if isinstance(node.parent, nodes.topic):
1045 if isinstance(node.parent, nodes.topic):
1047 self.body.append(self.defs['topic-title'][0])
1046 self.body.append(self.defs['topic-title'][0])
1048 elif isinstance(node.parent, nodes.sidebar):
1047 elif isinstance(node.parent, nodes.sidebar):
1049 self.body.append(self.defs['sidebar-title'][0])
1048 self.body.append(self.defs['sidebar-title'][0])
1050 elif isinstance(node.parent, nodes.admonition):
1049 elif isinstance(node.parent, nodes.admonition):
1051 self.body.append('.IP "')
1050 self.body.append('.IP "')
1052 elif self.section_level == 0:
1051 elif self.section_level == 0:
1053 self._docinfo['title'] = node.astext()
1052 self._docinfo['title'] = node.astext()
1054 # document title for .TH
1053 # document title for .TH
1055 self._docinfo['title_upper'] = node.astext().upper()
1054 self._docinfo['title_upper'] = node.astext().upper()
1056 raise nodes.SkipNode()
1055 raise nodes.SkipNode()
1057 elif self.section_level == 1:
1056 elif self.section_level == 1:
1058 self.body.append('.SH ')
1057 self.body.append('.SH ')
1059 for n in node.traverse(nodes.Text):
1058 for n in node.traverse(nodes.Text):
1060 n.parent.replace(n, nodes.Text(n.astext().upper()))
1059 n.parent.replace(n, nodes.Text(n.astext().upper()))
1061 else:
1060 else:
1062 self.body.append('.SS ')
1061 self.body.append('.SS ')
1063
1062
1064 def depart_title(self, node):
1063 def depart_title(self, node):
1065 if isinstance(node.parent, nodes.admonition):
1064 if isinstance(node.parent, nodes.admonition):
1066 self.body.append('"')
1065 self.body.append('"')
1067 self.body.append('\n')
1066 self.body.append('\n')
1068
1067
1069 def visit_title_reference(self, node):
1068 def visit_title_reference(self, node):
1070 """inline citation reference"""
1069 """inline citation reference"""
1071 self.body.append(self.defs['title_reference'][0])
1070 self.body.append(self.defs['title_reference'][0])
1072
1071
1073 def depart_title_reference(self, node):
1072 def depart_title_reference(self, node):
1074 self.body.append(self.defs['title_reference'][1])
1073 self.body.append(self.defs['title_reference'][1])
1075
1074
1076 def visit_topic(self, node):
1075 def visit_topic(self, node):
1077 pass
1076 pass
1078
1077
1079 def depart_topic(self, node):
1078 def depart_topic(self, node):
1080 pass
1079 pass
1081
1080
1082 def visit_sidebar(self, node):
1081 def visit_sidebar(self, node):
1083 pass
1082 pass
1084
1083
1085 def depart_sidebar(self, node):
1084 def depart_sidebar(self, node):
1086 pass
1085 pass
1087
1086
1088 def visit_rubric(self, node):
1087 def visit_rubric(self, node):
1089 pass
1088 pass
1090
1089
1091 def depart_rubric(self, node):
1090 def depart_rubric(self, node):
1092 pass
1091 pass
1093
1092
1094 def visit_transition(self, node):
1093 def visit_transition(self, node):
1095 # .PP Begin a new paragraph and reset prevailing indent.
1094 # .PP Begin a new paragraph and reset prevailing indent.
1096 # .sp N leaves N lines of blank space.
1095 # .sp N leaves N lines of blank space.
1097 # .ce centers the next line
1096 # .ce centers the next line
1098 self.body.append('\n.sp\n.ce\n----\n')
1097 self.body.append('\n.sp\n.ce\n----\n')
1099
1098
1100 def depart_transition(self, node):
1099 def depart_transition(self, node):
1101 self.body.append('\n.ce 0\n.sp\n')
1100 self.body.append('\n.ce 0\n.sp\n')
1102
1101
1103 def visit_version(self, node):
1102 def visit_version(self, node):
1104 self.visit_docinfo_item(node, 'version')
1103 self.visit_docinfo_item(node, 'version')
1105
1104
1106 def visit_warning(self, node):
1105 def visit_warning(self, node):
1107 self.visit_admonition(node, 'warning')
1106 self.visit_admonition(node, 'warning')
1108
1107
1109 depart_warning = depart_admonition
1108 depart_warning = depart_admonition
1110
1109
1111 def unimplemented_visit(self, node):
1110 def unimplemented_visit(self, node):
1112 raise NotImplementedError('visiting unimplemented node type: %s'
1111 raise NotImplementedError('visiting unimplemented node type: %s'
1113 % node.__class__.__name__)
1112 % node.__class__.__name__)
1114
1113
1115 # vim: set fileencoding=utf-8 et ts=4 ai :
1114 # vim: set fileencoding=utf-8 et ts=4 ai :
@@ -1,330 +1,330 b''
1 # acl.py - changeset access control for mercurial
1 # acl.py - changeset access control for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 '''hooks for controlling repository access
8 '''hooks for controlling repository access
9
9
10 This hook makes it possible to allow or deny write access to given
10 This hook makes it possible to allow or deny write access to given
11 branches and paths of a repository when receiving incoming changesets
11 branches and paths of a repository when receiving incoming changesets
12 via pretxnchangegroup and pretxncommit.
12 via pretxnchangegroup and pretxncommit.
13
13
14 The authorization is matched based on the local user name on the
14 The authorization is matched based on the local user name on the
15 system where the hook runs, and not the committer of the original
15 system where the hook runs, and not the committer of the original
16 changeset (since the latter is merely informative).
16 changeset (since the latter is merely informative).
17
17
18 The acl hook is best used along with a restricted shell like hgsh,
18 The acl hook is best used along with a restricted shell like hgsh,
19 preventing authenticating users from doing anything other than pushing
19 preventing authenticating users from doing anything other than pushing
20 or pulling. The hook is not safe to use if users have interactive
20 or pulling. The hook is not safe to use if users have interactive
21 shell access, as they can then disable the hook. Nor is it safe if
21 shell access, as they can then disable the hook. Nor is it safe if
22 remote users share an account, because then there is no way to
22 remote users share an account, because then there is no way to
23 distinguish them.
23 distinguish them.
24
24
25 The order in which access checks are performed is:
25 The order in which access checks are performed is:
26
26
27 1) Deny list for branches (section ``acl.deny.branches``)
27 1) Deny list for branches (section ``acl.deny.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
29 3) Deny list for paths (section ``acl.deny``)
29 3) Deny list for paths (section ``acl.deny``)
30 4) Allow list for paths (section ``acl.allow``)
30 4) Allow list for paths (section ``acl.allow``)
31
31
32 The allow and deny sections take key-value pairs.
32 The allow and deny sections take key-value pairs.
33
33
34 Branch-based Access Control
34 Branch-based Access Control
35 ---------------------------
35 ---------------------------
36
36
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
38 have branch-based access control. Keys in these sections can be
38 have branch-based access control. Keys in these sections can be
39 either:
39 either:
40
40
41 - a branch name, or
41 - a branch name, or
42 - an asterisk, to match any branch;
42 - an asterisk, to match any branch;
43
43
44 The corresponding values can be either:
44 The corresponding values can be either:
45
45
46 - a comma-separated list containing users and groups, or
46 - a comma-separated list containing users and groups, or
47 - an asterisk, to match anyone;
47 - an asterisk, to match anyone;
48
48
49 You can add the "!" prefix to a user or group name to invert the sense
49 You can add the "!" prefix to a user or group name to invert the sense
50 of the match.
50 of the match.
51
51
52 Path-based Access Control
52 Path-based Access Control
53 -------------------------
53 -------------------------
54
54
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
56 access control. Keys in these sections accept a subtree pattern (with
56 access control. Keys in these sections accept a subtree pattern (with
57 a glob syntax by default). The corresponding values follow the same
57 a glob syntax by default). The corresponding values follow the same
58 syntax as the other sections above.
58 syntax as the other sections above.
59
59
60 Groups
60 Groups
61 ------
61 ------
62
62
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
64 name has the same effect as specifying all the users in that group.
64 name has the same effect as specifying all the users in that group.
65
65
66 You can define group members in the ``acl.groups`` section.
66 You can define group members in the ``acl.groups`` section.
67 If a group name is not defined there, and Mercurial is running under
67 If a group name is not defined there, and Mercurial is running under
68 a Unix-like system, the list of users will be taken from the OS.
68 a Unix-like system, the list of users will be taken from the OS.
69 Otherwise, an exception will be raised.
69 Otherwise, an exception will be raised.
70
70
71 Example Configuration
71 Example Configuration
72 ---------------------
72 ---------------------
73
73
74 ::
74 ::
75
75
76 [hooks]
76 [hooks]
77
77
78 # Use this if you want to check access restrictions at commit time
78 # Use this if you want to check access restrictions at commit time
79 pretxncommit.acl = python:hgext.acl.hook
79 pretxncommit.acl = python:hgext.acl.hook
80
80
81 # Use this if you want to check access restrictions for pull, push,
81 # Use this if you want to check access restrictions for pull, push,
82 # bundle and serve.
82 # bundle and serve.
83 pretxnchangegroup.acl = python:hgext.acl.hook
83 pretxnchangegroup.acl = python:hgext.acl.hook
84
84
85 [acl]
85 [acl]
86 # Allow or deny access for incoming changes only if their source is
86 # Allow or deny access for incoming changes only if their source is
87 # listed here, let them pass otherwise. Source is "serve" for all
87 # listed here, let them pass otherwise. Source is "serve" for all
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
89 # related commands are run locally.
89 # related commands are run locally.
90 # Default: serve
90 # Default: serve
91 sources = serve
91 sources = serve
92
92
93 [acl.deny.branches]
93 [acl.deny.branches]
94
94
95 # Everyone is denied to the frozen branch:
95 # Everyone is denied to the frozen branch:
96 frozen-branch = *
96 frozen-branch = *
97
97
98 # A bad user is denied on all branches:
98 # A bad user is denied on all branches:
99 * = bad-user
99 * = bad-user
100
100
101 [acl.allow.branches]
101 [acl.allow.branches]
102
102
103 # A few users are allowed on branch-a:
103 # A few users are allowed on branch-a:
104 branch-a = user-1, user-2, user-3
104 branch-a = user-1, user-2, user-3
105
105
106 # Only one user is allowed on branch-b:
106 # Only one user is allowed on branch-b:
107 branch-b = user-1
107 branch-b = user-1
108
108
109 # The super user is allowed on any branch:
109 # The super user is allowed on any branch:
110 * = super-user
110 * = super-user
111
111
112 # Everyone is allowed on branch-for-tests:
112 # Everyone is allowed on branch-for-tests:
113 branch-for-tests = *
113 branch-for-tests = *
114
114
115 [acl.deny]
115 [acl.deny]
116 # This list is checked first. If a match is found, acl.allow is not
116 # This list is checked first. If a match is found, acl.allow is not
117 # checked. All users are granted access if acl.deny is not present.
117 # checked. All users are granted access if acl.deny is not present.
118 # Format for both lists: glob pattern = user, ..., @group, ...
118 # Format for both lists: glob pattern = user, ..., @group, ...
119
119
120 # To match everyone, use an asterisk for the user:
120 # To match everyone, use an asterisk for the user:
121 # my/glob/pattern = *
121 # my/glob/pattern = *
122
122
123 # user6 will not have write access to any file:
123 # user6 will not have write access to any file:
124 ** = user6
124 ** = user6
125
125
126 # Group "hg-denied" will not have write access to any file:
126 # Group "hg-denied" will not have write access to any file:
127 ** = @hg-denied
127 ** = @hg-denied
128
128
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
130 # everyone being able to change all other files. See below.
130 # everyone being able to change all other files. See below.
131 src/main/resources/DONT-TOUCH-THIS.txt = *
131 src/main/resources/DONT-TOUCH-THIS.txt = *
132
132
133 [acl.allow]
133 [acl.allow]
134 # if acl.allow is not present, all users are allowed by default
134 # if acl.allow is not present, all users are allowed by default
135 # empty acl.allow = no users allowed
135 # empty acl.allow = no users allowed
136
136
137 # User "doc_writer" has write access to any file under the "docs"
137 # User "doc_writer" has write access to any file under the "docs"
138 # folder:
138 # folder:
139 docs/** = doc_writer
139 docs/** = doc_writer
140
140
141 # User "jack" and group "designers" have write access to any file
141 # User "jack" and group "designers" have write access to any file
142 # under the "images" folder:
142 # under the "images" folder:
143 images/** = jack, @designers
143 images/** = jack, @designers
144
144
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
146 # will have write access to any file under the "resources" folder
146 # will have write access to any file under the "resources" folder
147 # (except for 1 file. See acl.deny):
147 # (except for 1 file. See acl.deny):
148 src/main/resources/** = *
148 src/main/resources/** = *
149
149
150 .hgtags = release_engineer
150 .hgtags = release_engineer
151
151
152 Examples using the "!" prefix
152 Examples using the "!" prefix
153 .............................
153 .............................
154
154
155 Suppose there's a branch that only a given user (or group) should be able to
155 Suppose there's a branch that only a given user (or group) should be able to
156 push to, and you don't want to restrict access to any other branch that may
156 push to, and you don't want to restrict access to any other branch that may
157 be created.
157 be created.
158
158
159 The "!" prefix allows you to prevent anyone except a given user or group to
159 The "!" prefix allows you to prevent anyone except a given user or group to
160 push changesets in a given branch or path.
160 push changesets in a given branch or path.
161
161
162 In the examples below, we will:
162 In the examples below, we will:
163 1) Deny access to branch "ring" to anyone but user "gollum"
163 1) Deny access to branch "ring" to anyone but user "gollum"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
165 3) Deny access to a file to anyone but user "gollum"
165 3) Deny access to a file to anyone but user "gollum"
166
166
167 ::
167 ::
168
168
169 [acl.allow.branches]
169 [acl.allow.branches]
170 # Empty
170 # Empty
171
171
172 [acl.deny.branches]
172 [acl.deny.branches]
173
173
174 # 1) only 'gollum' can commit to branch 'ring';
174 # 1) only 'gollum' can commit to branch 'ring';
175 # 'gollum' and anyone else can still commit to any other branch.
175 # 'gollum' and anyone else can still commit to any other branch.
176 ring = !gollum
176 ring = !gollum
177
177
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
179 # 'hobbit' members and anyone else can still commit to any other branch.
179 # 'hobbit' members and anyone else can still commit to any other branch.
180 lake = !@hobbit
180 lake = !@hobbit
181
181
182 # You can also deny access based on file paths:
182 # You can also deny access based on file paths:
183
183
184 [acl.allow]
184 [acl.allow]
185 # Empty
185 # Empty
186
186
187 [acl.deny]
187 [acl.deny]
188 # 3) only 'gollum' can change the file below;
188 # 3) only 'gollum' can change the file below;
189 # 'gollum' and anyone else can still change any other file.
189 # 'gollum' and anyone else can still change any other file.
190 /misty/mountains/cave/ring = !gollum
190 /misty/mountains/cave/ring = !gollum
191
191
192 '''
192 '''
193
193
194 from __future__ import absolute_import
194 from __future__ import absolute_import
195
195
196 import getpass
196 import getpass
197
197
198 from mercurial.i18n import _
198 from mercurial.i18n import _
199 from mercurial import (
199 from mercurial import (
200 error,
200 error,
201 match,
201 match,
202 util,
202 util,
203 )
203 )
204
204
205 urlreq = util.urlreq
205 urlreq = util.urlreq
206
206
207 # Note for extension authors: ONLY specify testedwith = 'internal' for
207 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
208 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
208 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
209 # be specifying the version(s) of Mercurial they are tested with, or
209 # be specifying the version(s) of Mercurial they are tested with, or
210 # leave the attribute unspecified.
210 # leave the attribute unspecified.
211 testedwith = 'internal'
211 testedwith = 'ships-with-hg-core'
212
212
213 def _getusers(ui, group):
213 def _getusers(ui, group):
214
214
215 # First, try to use group definition from section [acl.groups]
215 # First, try to use group definition from section [acl.groups]
216 hgrcusers = ui.configlist('acl.groups', group)
216 hgrcusers = ui.configlist('acl.groups', group)
217 if hgrcusers:
217 if hgrcusers:
218 return hgrcusers
218 return hgrcusers
219
219
220 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
220 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
221 # If no users found in group definition, get users from OS-level group
221 # If no users found in group definition, get users from OS-level group
222 try:
222 try:
223 return util.groupmembers(group)
223 return util.groupmembers(group)
224 except KeyError:
224 except KeyError:
225 raise error.Abort(_("group '%s' is undefined") % group)
225 raise error.Abort(_("group '%s' is undefined") % group)
226
226
227 def _usermatch(ui, user, usersorgroups):
227 def _usermatch(ui, user, usersorgroups):
228
228
229 if usersorgroups == '*':
229 if usersorgroups == '*':
230 return True
230 return True
231
231
232 for ug in usersorgroups.replace(',', ' ').split():
232 for ug in usersorgroups.replace(',', ' ').split():
233
233
234 if ug.startswith('!'):
234 if ug.startswith('!'):
235 # Test for excluded user or group. Format:
235 # Test for excluded user or group. Format:
236 # if ug is a user name: !username
236 # if ug is a user name: !username
237 # if ug is a group name: !@groupname
237 # if ug is a group name: !@groupname
238 ug = ug[1:]
238 ug = ug[1:]
239 if not ug.startswith('@') and user != ug \
239 if not ug.startswith('@') and user != ug \
240 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
240 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
241 return True
241 return True
242
242
243 # Test for user or group. Format:
243 # Test for user or group. Format:
244 # if ug is a user name: username
244 # if ug is a user name: username
245 # if ug is a group name: @groupname
245 # if ug is a group name: @groupname
246 elif user == ug \
246 elif user == ug \
247 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
247 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
248 return True
248 return True
249
249
250 return False
250 return False
251
251
252 def buildmatch(ui, repo, user, key):
252 def buildmatch(ui, repo, user, key):
253 '''return tuple of (match function, list enabled).'''
253 '''return tuple of (match function, list enabled).'''
254 if not ui.has_section(key):
254 if not ui.has_section(key):
255 ui.debug('acl: %s not enabled\n' % key)
255 ui.debug('acl: %s not enabled\n' % key)
256 return None
256 return None
257
257
258 pats = [pat for pat, users in ui.configitems(key)
258 pats = [pat for pat, users in ui.configitems(key)
259 if _usermatch(ui, user, users)]
259 if _usermatch(ui, user, users)]
260 ui.debug('acl: %s enabled, %d entries for user %s\n' %
260 ui.debug('acl: %s enabled, %d entries for user %s\n' %
261 (key, len(pats), user))
261 (key, len(pats), user))
262
262
263 # Branch-based ACL
263 # Branch-based ACL
264 if not repo:
264 if not repo:
265 if pats:
265 if pats:
266 # If there's an asterisk (meaning "any branch"), always return True;
266 # If there's an asterisk (meaning "any branch"), always return True;
267 # Otherwise, test if b is in pats
267 # Otherwise, test if b is in pats
268 if '*' in pats:
268 if '*' in pats:
269 return util.always
269 return util.always
270 return lambda b: b in pats
270 return lambda b: b in pats
271 return util.never
271 return util.never
272
272
273 # Path-based ACL
273 # Path-based ACL
274 if pats:
274 if pats:
275 return match.match(repo.root, '', pats)
275 return match.match(repo.root, '', pats)
276 return util.never
276 return util.never
277
277
278 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
278 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
279 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
279 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
280 raise error.Abort(_('config error - hook type "%s" cannot stop '
280 raise error.Abort(_('config error - hook type "%s" cannot stop '
281 'incoming changesets nor commits') % hooktype)
281 'incoming changesets nor commits') % hooktype)
282 if (hooktype == 'pretxnchangegroup' and
282 if (hooktype == 'pretxnchangegroup' and
283 source not in ui.config('acl', 'sources', 'serve').split()):
283 source not in ui.config('acl', 'sources', 'serve').split()):
284 ui.debug('acl: changes have source "%s" - skipping\n' % source)
284 ui.debug('acl: changes have source "%s" - skipping\n' % source)
285 return
285 return
286
286
287 user = None
287 user = None
288 if source == 'serve' and 'url' in kwargs:
288 if source == 'serve' and 'url' in kwargs:
289 url = kwargs['url'].split(':')
289 url = kwargs['url'].split(':')
290 if url[0] == 'remote' and url[1].startswith('http'):
290 if url[0] == 'remote' and url[1].startswith('http'):
291 user = urlreq.unquote(url[3])
291 user = urlreq.unquote(url[3])
292
292
293 if user is None:
293 if user is None:
294 user = getpass.getuser()
294 user = getpass.getuser()
295
295
296 ui.debug('acl: checking access for user "%s"\n' % user)
296 ui.debug('acl: checking access for user "%s"\n' % user)
297
297
298 # deprecated config: acl.config
298 # deprecated config: acl.config
299 cfg = ui.config('acl', 'config')
299 cfg = ui.config('acl', 'config')
300 if cfg:
300 if cfg:
301 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
301 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
302 'acl.deny.branches', 'acl.allow', 'acl.deny'])
302 'acl.deny.branches', 'acl.allow', 'acl.deny'])
303
303
304 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
304 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
305 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
305 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
306 allow = buildmatch(ui, repo, user, 'acl.allow')
306 allow = buildmatch(ui, repo, user, 'acl.allow')
307 deny = buildmatch(ui, repo, user, 'acl.deny')
307 deny = buildmatch(ui, repo, user, 'acl.deny')
308
308
309 for rev in xrange(repo[node], len(repo)):
309 for rev in xrange(repo[node], len(repo)):
310 ctx = repo[rev]
310 ctx = repo[rev]
311 branch = ctx.branch()
311 branch = ctx.branch()
312 if denybranches and denybranches(branch):
312 if denybranches and denybranches(branch):
313 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
313 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
314 ' (changeset "%s")')
314 ' (changeset "%s")')
315 % (user, branch, ctx))
315 % (user, branch, ctx))
316 if allowbranches and not allowbranches(branch):
316 if allowbranches and not allowbranches(branch):
317 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
317 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
318 ' (changeset "%s")')
318 ' (changeset "%s")')
319 % (user, branch, ctx))
319 % (user, branch, ctx))
320 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
320 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
321 % (ctx, branch))
321 % (ctx, branch))
322
322
323 for f in ctx.files():
323 for f in ctx.files():
324 if deny and deny(f):
324 if deny and deny(f):
325 raise error.Abort(_('acl: user "%s" denied on "%s"'
325 raise error.Abort(_('acl: user "%s" denied on "%s"'
326 ' (changeset "%s")') % (user, f, ctx))
326 ' (changeset "%s")') % (user, f, ctx))
327 if allow and not allow(f):
327 if allow and not allow(f):
328 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
328 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
329 ' (changeset "%s")') % (user, f, ctx))
329 ' (changeset "%s")') % (user, f, ctx))
330 ui.debug('acl: path access granted: "%s"\n' % ctx)
330 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -1,250 +1,250 b''
1 # blackbox.py - log repository events to a file for post-mortem debugging
1 # blackbox.py - log repository events to a file for post-mortem debugging
2 #
2 #
3 # Copyright 2010 Nicolas Dumazet
3 # Copyright 2010 Nicolas Dumazet
4 # Copyright 2013 Facebook, Inc.
4 # Copyright 2013 Facebook, Inc.
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 """log repository events to a blackbox for debugging
9 """log repository events to a blackbox for debugging
10
10
11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
12 The events that get logged can be configured via the blackbox.track config key.
12 The events that get logged can be configured via the blackbox.track config key.
13
13
14 Examples::
14 Examples::
15
15
16 [blackbox]
16 [blackbox]
17 track = *
17 track = *
18 # dirty is *EXPENSIVE* (slow);
18 # dirty is *EXPENSIVE* (slow);
19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
20 dirty = True
20 dirty = True
21 # record the source of log messages
21 # record the source of log messages
22 logsource = True
22 logsource = True
23
23
24 [blackbox]
24 [blackbox]
25 track = command, commandfinish, commandexception, exthook, pythonhook
25 track = command, commandfinish, commandexception, exthook, pythonhook
26
26
27 [blackbox]
27 [blackbox]
28 track = incoming
28 track = incoming
29
29
30 [blackbox]
30 [blackbox]
31 # limit the size of a log file
31 # limit the size of a log file
32 maxsize = 1.5 MB
32 maxsize = 1.5 MB
33 # rotate up to N log files when the current one gets too big
33 # rotate up to N log files when the current one gets too big
34 maxfiles = 3
34 maxfiles = 3
35
35
36 """
36 """
37
37
38 from __future__ import absolute_import
38 from __future__ import absolute_import
39
39
40 import errno
40 import errno
41 import re
41 import re
42
42
43 from mercurial.i18n import _
43 from mercurial.i18n import _
44 from mercurial.node import hex
44 from mercurial.node import hex
45
45
46 from mercurial import (
46 from mercurial import (
47 cmdutil,
47 cmdutil,
48 ui as uimod,
48 ui as uimod,
49 util,
49 util,
50 )
50 )
51
51
52 cmdtable = {}
52 cmdtable = {}
53 command = cmdutil.command(cmdtable)
53 command = cmdutil.command(cmdtable)
54 # Note for extension authors: ONLY specify testedwith = 'internal' for
54 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
56 # be specifying the version(s) of Mercurial they are tested with, or
56 # be specifying the version(s) of Mercurial they are tested with, or
57 # leave the attribute unspecified.
57 # leave the attribute unspecified.
58 testedwith = 'internal'
58 testedwith = 'ships-with-hg-core'
59 lastui = None
59 lastui = None
60
60
61 filehandles = {}
61 filehandles = {}
62
62
63 def _openlog(vfs):
63 def _openlog(vfs):
64 path = vfs.join('blackbox.log')
64 path = vfs.join('blackbox.log')
65 if path in filehandles:
65 if path in filehandles:
66 return filehandles[path]
66 return filehandles[path]
67 filehandles[path] = fp = vfs('blackbox.log', 'a')
67 filehandles[path] = fp = vfs('blackbox.log', 'a')
68 return fp
68 return fp
69
69
70 def _closelog(vfs):
70 def _closelog(vfs):
71 path = vfs.join('blackbox.log')
71 path = vfs.join('blackbox.log')
72 fp = filehandles[path]
72 fp = filehandles[path]
73 del filehandles[path]
73 del filehandles[path]
74 fp.close()
74 fp.close()
75
75
76 def wrapui(ui):
76 def wrapui(ui):
77 class blackboxui(ui.__class__):
77 class blackboxui(ui.__class__):
78 def __init__(self, src=None):
78 def __init__(self, src=None):
79 super(blackboxui, self).__init__(src)
79 super(blackboxui, self).__init__(src)
80 if src is None:
80 if src is None:
81 self._partialinit()
81 self._partialinit()
82 else:
82 else:
83 self._bbfp = getattr(src, '_bbfp', None)
83 self._bbfp = getattr(src, '_bbfp', None)
84 self._bbinlog = False
84 self._bbinlog = False
85 self._bbrepo = getattr(src, '_bbrepo', None)
85 self._bbrepo = getattr(src, '_bbrepo', None)
86 self._bbvfs = getattr(src, '_bbvfs', None)
86 self._bbvfs = getattr(src, '_bbvfs', None)
87
87
88 def _partialinit(self):
88 def _partialinit(self):
89 if util.safehasattr(self, '_bbvfs'):
89 if util.safehasattr(self, '_bbvfs'):
90 return
90 return
91 self._bbfp = None
91 self._bbfp = None
92 self._bbinlog = False
92 self._bbinlog = False
93 self._bbrepo = None
93 self._bbrepo = None
94 self._bbvfs = None
94 self._bbvfs = None
95
95
96 def copy(self):
96 def copy(self):
97 self._partialinit()
97 self._partialinit()
98 return self.__class__(self)
98 return self.__class__(self)
99
99
100 @util.propertycache
100 @util.propertycache
101 def track(self):
101 def track(self):
102 return self.configlist('blackbox', 'track', ['*'])
102 return self.configlist('blackbox', 'track', ['*'])
103
103
104 def _openlogfile(self):
104 def _openlogfile(self):
105 def rotate(oldpath, newpath):
105 def rotate(oldpath, newpath):
106 try:
106 try:
107 self._bbvfs.unlink(newpath)
107 self._bbvfs.unlink(newpath)
108 except OSError as err:
108 except OSError as err:
109 if err.errno != errno.ENOENT:
109 if err.errno != errno.ENOENT:
110 self.debug("warning: cannot remove '%s': %s\n" %
110 self.debug("warning: cannot remove '%s': %s\n" %
111 (newpath, err.strerror))
111 (newpath, err.strerror))
112 try:
112 try:
113 if newpath:
113 if newpath:
114 self._bbvfs.rename(oldpath, newpath)
114 self._bbvfs.rename(oldpath, newpath)
115 except OSError as err:
115 except OSError as err:
116 if err.errno != errno.ENOENT:
116 if err.errno != errno.ENOENT:
117 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
117 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
118 (newpath, oldpath, err.strerror))
118 (newpath, oldpath, err.strerror))
119
119
120 fp = _openlog(self._bbvfs)
120 fp = _openlog(self._bbvfs)
121 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
121 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
122 if maxsize > 0:
122 if maxsize > 0:
123 st = self._bbvfs.fstat(fp)
123 st = self._bbvfs.fstat(fp)
124 if st.st_size >= maxsize:
124 if st.st_size >= maxsize:
125 path = fp.name
125 path = fp.name
126 _closelog(self._bbvfs)
126 _closelog(self._bbvfs)
127 maxfiles = self.configint('blackbox', 'maxfiles', 7)
127 maxfiles = self.configint('blackbox', 'maxfiles', 7)
128 for i in xrange(maxfiles - 1, 1, -1):
128 for i in xrange(maxfiles - 1, 1, -1):
129 rotate(oldpath='%s.%d' % (path, i - 1),
129 rotate(oldpath='%s.%d' % (path, i - 1),
130 newpath='%s.%d' % (path, i))
130 newpath='%s.%d' % (path, i))
131 rotate(oldpath=path,
131 rotate(oldpath=path,
132 newpath=maxfiles > 0 and path + '.1')
132 newpath=maxfiles > 0 and path + '.1')
133 fp = _openlog(self._bbvfs)
133 fp = _openlog(self._bbvfs)
134 return fp
134 return fp
135
135
136 def _bbwrite(self, fmt, *args):
136 def _bbwrite(self, fmt, *args):
137 self._bbfp.write(fmt % args)
137 self._bbfp.write(fmt % args)
138 self._bbfp.flush()
138 self._bbfp.flush()
139
139
140 def log(self, event, *msg, **opts):
140 def log(self, event, *msg, **opts):
141 global lastui
141 global lastui
142 super(blackboxui, self).log(event, *msg, **opts)
142 super(blackboxui, self).log(event, *msg, **opts)
143 self._partialinit()
143 self._partialinit()
144
144
145 if not '*' in self.track and not event in self.track:
145 if not '*' in self.track and not event in self.track:
146 return
146 return
147
147
148 if self._bbfp:
148 if self._bbfp:
149 ui = self
149 ui = self
150 elif self._bbvfs:
150 elif self._bbvfs:
151 try:
151 try:
152 self._bbfp = self._openlogfile()
152 self._bbfp = self._openlogfile()
153 except (IOError, OSError) as err:
153 except (IOError, OSError) as err:
154 self.debug('warning: cannot write to blackbox.log: %s\n' %
154 self.debug('warning: cannot write to blackbox.log: %s\n' %
155 err.strerror)
155 err.strerror)
156 del self._bbvfs
156 del self._bbvfs
157 self._bbfp = None
157 self._bbfp = None
158 ui = self
158 ui = self
159 else:
159 else:
160 # certain ui instances exist outside the context of
160 # certain ui instances exist outside the context of
161 # a repo, so just default to the last blackbox that
161 # a repo, so just default to the last blackbox that
162 # was seen.
162 # was seen.
163 ui = lastui
163 ui = lastui
164
164
165 if not ui or not ui._bbfp:
165 if not ui or not ui._bbfp:
166 return
166 return
167 if not lastui or ui._bbrepo:
167 if not lastui or ui._bbrepo:
168 lastui = ui
168 lastui = ui
169 if ui._bbinlog:
169 if ui._bbinlog:
170 # recursion guard
170 # recursion guard
171 return
171 return
172 try:
172 try:
173 ui._bbinlog = True
173 ui._bbinlog = True
174 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
174 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
175 user = util.getuser()
175 user = util.getuser()
176 pid = str(util.getpid())
176 pid = str(util.getpid())
177 formattedmsg = msg[0] % msg[1:]
177 formattedmsg = msg[0] % msg[1:]
178 rev = '(unknown)'
178 rev = '(unknown)'
179 changed = ''
179 changed = ''
180 if ui._bbrepo:
180 if ui._bbrepo:
181 ctx = ui._bbrepo[None]
181 ctx = ui._bbrepo[None]
182 parents = ctx.parents()
182 parents = ctx.parents()
183 rev = ('+'.join([hex(p.node()) for p in parents]))
183 rev = ('+'.join([hex(p.node()) for p in parents]))
184 if (ui.configbool('blackbox', 'dirty', False) and (
184 if (ui.configbool('blackbox', 'dirty', False) and (
185 any(ui._bbrepo.status()) or
185 any(ui._bbrepo.status()) or
186 any(ctx.sub(s).dirty() for s in ctx.substate)
186 any(ctx.sub(s).dirty() for s in ctx.substate)
187 )):
187 )):
188 changed = '+'
188 changed = '+'
189 if ui.configbool('blackbox', 'logsource', False):
189 if ui.configbool('blackbox', 'logsource', False):
190 src = ' [%s]' % event
190 src = ' [%s]' % event
191 else:
191 else:
192 src = ''
192 src = ''
193 try:
193 try:
194 ui._bbwrite('%s %s @%s%s (%s)%s> %s',
194 ui._bbwrite('%s %s @%s%s (%s)%s> %s',
195 date, user, rev, changed, pid, src, formattedmsg)
195 date, user, rev, changed, pid, src, formattedmsg)
196 except IOError as err:
196 except IOError as err:
197 self.debug('warning: cannot write to blackbox.log: %s\n' %
197 self.debug('warning: cannot write to blackbox.log: %s\n' %
198 err.strerror)
198 err.strerror)
199 finally:
199 finally:
200 ui._bbinlog = False
200 ui._bbinlog = False
201
201
202 def setrepo(self, repo):
202 def setrepo(self, repo):
203 self._bbfp = None
203 self._bbfp = None
204 self._bbinlog = False
204 self._bbinlog = False
205 self._bbrepo = repo
205 self._bbrepo = repo
206 self._bbvfs = repo.vfs
206 self._bbvfs = repo.vfs
207
207
208 ui.__class__ = blackboxui
208 ui.__class__ = blackboxui
209 uimod.ui = blackboxui
209 uimod.ui = blackboxui
210
210
211 def uisetup(ui):
211 def uisetup(ui):
212 wrapui(ui)
212 wrapui(ui)
213
213
214 def reposetup(ui, repo):
214 def reposetup(ui, repo):
215 # During 'hg pull' a httppeer repo is created to represent the remote repo.
215 # During 'hg pull' a httppeer repo is created to represent the remote repo.
216 # It doesn't have a .hg directory to put a blackbox in, so we don't do
216 # It doesn't have a .hg directory to put a blackbox in, so we don't do
217 # the blackbox setup for it.
217 # the blackbox setup for it.
218 if not repo.local():
218 if not repo.local():
219 return
219 return
220
220
221 if util.safehasattr(ui, 'setrepo'):
221 if util.safehasattr(ui, 'setrepo'):
222 ui.setrepo(repo)
222 ui.setrepo(repo)
223
223
224 @command('^blackbox',
224 @command('^blackbox',
225 [('l', 'limit', 10, _('the number of events to show')),
225 [('l', 'limit', 10, _('the number of events to show')),
226 ],
226 ],
227 _('hg blackbox [OPTION]...'))
227 _('hg blackbox [OPTION]...'))
228 def blackbox(ui, repo, *revs, **opts):
228 def blackbox(ui, repo, *revs, **opts):
229 '''view the recent repository events
229 '''view the recent repository events
230 '''
230 '''
231
231
232 if not repo.vfs.exists('blackbox.log'):
232 if not repo.vfs.exists('blackbox.log'):
233 return
233 return
234
234
235 limit = opts.get('limit')
235 limit = opts.get('limit')
236 fp = repo.vfs('blackbox.log', 'r')
236 fp = repo.vfs('blackbox.log', 'r')
237 lines = fp.read().split('\n')
237 lines = fp.read().split('\n')
238
238
239 count = 0
239 count = 0
240 output = []
240 output = []
241 for line in reversed(lines):
241 for line in reversed(lines):
242 if count >= limit:
242 if count >= limit:
243 break
243 break
244
244
245 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
245 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
246 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
246 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
247 count += 1
247 count += 1
248 output.append(line)
248 output.append(line)
249
249
250 ui.status('\n'.join(reversed(output)))
250 ui.status('\n'.join(reversed(output)))
@@ -1,928 +1,928 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''hooks for integrating with the Bugzilla bug tracker
9 '''hooks for integrating with the Bugzilla bug tracker
10
10
11 This hook extension adds comments on bugs in Bugzilla when changesets
11 This hook extension adds comments on bugs in Bugzilla when changesets
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 the Mercurial template mechanism.
13 the Mercurial template mechanism.
14
14
15 The bug references can optionally include an update for Bugzilla of the
15 The bug references can optionally include an update for Bugzilla of the
16 hours spent working on the bug. Bugs can also be marked fixed.
16 hours spent working on the bug. Bugs can also be marked fixed.
17
17
18 Three basic modes of access to Bugzilla are provided:
18 Three basic modes of access to Bugzilla are provided:
19
19
20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21
21
22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24
24
25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 using MySQL are supported. Requires Python MySQLdb.
26 using MySQL are supported. Requires Python MySQLdb.
27
27
28 Writing directly to the database is susceptible to schema changes, and
28 Writing directly to the database is susceptible to schema changes, and
29 relies on a Bugzilla contrib script to send out bug change
29 relies on a Bugzilla contrib script to send out bug change
30 notification emails. This script runs as the user running Mercurial,
30 notification emails. This script runs as the user running Mercurial,
31 must be run on the host with the Bugzilla install, and requires
31 must be run on the host with the Bugzilla install, and requires
32 permission to read Bugzilla configuration details and the necessary
32 permission to read Bugzilla configuration details and the necessary
33 MySQL user and password to have full access rights to the Bugzilla
33 MySQL user and password to have full access rights to the Bugzilla
34 database. For these reasons this access mode is now considered
34 database. For these reasons this access mode is now considered
35 deprecated, and will not be updated for new Bugzilla versions going
35 deprecated, and will not be updated for new Bugzilla versions going
36 forward. Only adding comments is supported in this access mode.
36 forward. Only adding comments is supported in this access mode.
37
37
38 Access via XMLRPC needs a Bugzilla username and password to be specified
38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 in the configuration. Comments are added under that username. Since the
39 in the configuration. Comments are added under that username. Since the
40 configuration must be readable by all Mercurial users, it is recommended
40 configuration must be readable by all Mercurial users, it is recommended
41 that the rights of that user are restricted in Bugzilla to the minimum
41 that the rights of that user are restricted in Bugzilla to the minimum
42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43
43
44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 email to the Bugzilla email interface to submit comments to bugs.
45 email to the Bugzilla email interface to submit comments to bugs.
46 The From: address in the email is set to the email address of the Mercurial
46 The From: address in the email is set to the email address of the Mercurial
47 user, so the comment appears to come from the Mercurial user. In the event
47 user, so the comment appears to come from the Mercurial user. In the event
48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
49 user, the email associated with the Bugzilla username used to log into
49 user, the email associated with the Bugzilla username used to log into
50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 works on all supported Bugzilla versions.
51 works on all supported Bugzilla versions.
52
52
53 Configuration items common to all access modes:
53 Configuration items common to all access modes:
54
54
55 bugzilla.version
55 bugzilla.version
56 The access type to use. Values recognized are:
56 The access type to use. Values recognized are:
57
57
58 :``xmlrpc``: Bugzilla XMLRPC interface.
58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 including 3.0.
62 including 3.0.
63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 including 2.18.
64 including 2.18.
65
65
66 bugzilla.regexp
66 bugzilla.regexp
67 Regular expression to match bug IDs for update in changeset commit message.
67 Regular expression to match bug IDs for update in changeset commit message.
68 It must contain one "()" named group ``<ids>`` containing the bug
68 It must contain one "()" named group ``<ids>`` containing the bug
69 IDs separated by non-digit characters. It may also contain
69 IDs separated by non-digit characters. It may also contain
70 a named group ``<hours>`` with a floating-point number giving the
70 a named group ``<hours>`` with a floating-point number giving the
71 hours worked on the bug. If no named groups are present, the first
71 hours worked on the bug. If no named groups are present, the first
72 "()" group is assumed to contain the bug IDs, and work time is not
72 "()" group is assumed to contain the bug IDs, and work time is not
73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 variations thereof, followed by an hours number prefixed by ``h`` or
75 variations thereof, followed by an hours number prefixed by ``h`` or
76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77
77
78 bugzilla.fixregexp
78 bugzilla.fixregexp
79 Regular expression to match bug IDs for marking fixed in changeset
79 Regular expression to match bug IDs for marking fixed in changeset
80 commit message. This must contain a "()" named group ``<ids>` containing
80 commit message. This must contain a "()" named group ``<ids>` containing
81 the bug IDs separated by non-digit characters. It may also contain
81 the bug IDs separated by non-digit characters. It may also contain
82 a named group ``<hours>`` with a floating-point number giving the
82 a named group ``<hours>`` with a floating-point number giving the
83 hours worked on the bug. If no named groups are present, the first
83 hours worked on the bug. If no named groups are present, the first
84 "()" group is assumed to contain the bug IDs, and work time is not
84 "()" group is assumed to contain the bug IDs, and work time is not
85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 variations thereof, followed by an hours number prefixed by ``h`` or
87 variations thereof, followed by an hours number prefixed by ``h`` or
88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89
89
90 bugzilla.fixstatus
90 bugzilla.fixstatus
91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92
92
93 bugzilla.fixresolution
93 bugzilla.fixresolution
94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95
95
96 bugzilla.style
96 bugzilla.style
97 The style file to use when formatting comments.
97 The style file to use when formatting comments.
98
98
99 bugzilla.template
99 bugzilla.template
100 Template to use when formatting comments. Overrides style if
100 Template to use when formatting comments. Overrides style if
101 specified. In addition to the usual Mercurial keywords, the
101 specified. In addition to the usual Mercurial keywords, the
102 extension specifies:
102 extension specifies:
103
103
104 :``{bug}``: The Bugzilla bug ID.
104 :``{bug}``: The Bugzilla bug ID.
105 :``{root}``: The full pathname of the Mercurial repository.
105 :``{root}``: The full pathname of the Mercurial repository.
106 :``{webroot}``: Stripped pathname of the Mercurial repository.
106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108
108
109 Default ``changeset {node|short} in repo {root} refers to bug
109 Default ``changeset {node|short} in repo {root} refers to bug
110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111
111
112 bugzilla.strip
112 bugzilla.strip
113 The number of path separator characters to strip from the front of
113 The number of path separator characters to strip from the front of
114 the Mercurial repository path (``{root}`` in templates) to produce
114 the Mercurial repository path (``{root}`` in templates) to produce
115 ``{webroot}``. For example, a repository with ``{root}``
115 ``{webroot}``. For example, a repository with ``{root}``
116 ``/var/local/my-project`` with a strip of 2 gives a value for
116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 ``{webroot}`` of ``my-project``. Default 0.
117 ``{webroot}`` of ``my-project``. Default 0.
118
118
119 web.baseurl
119 web.baseurl
120 Base URL for browsing Mercurial repositories. Referenced from
120 Base URL for browsing Mercurial repositories. Referenced from
121 templates as ``{hgweb}``.
121 templates as ``{hgweb}``.
122
122
123 Configuration items common to XMLRPC+email and MySQL access modes:
123 Configuration items common to XMLRPC+email and MySQL access modes:
124
124
125 bugzilla.usermap
125 bugzilla.usermap
126 Path of file containing Mercurial committer email to Bugzilla user email
126 Path of file containing Mercurial committer email to Bugzilla user email
127 mappings. If specified, the file should contain one mapping per
127 mappings. If specified, the file should contain one mapping per
128 line::
128 line::
129
129
130 committer = Bugzilla user
130 committer = Bugzilla user
131
131
132 See also the ``[usermap]`` section.
132 See also the ``[usermap]`` section.
133
133
134 The ``[usermap]`` section is used to specify mappings of Mercurial
134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 Contains entries of the form ``committer = Bugzilla user``.
136 Contains entries of the form ``committer = Bugzilla user``.
137
137
138 XMLRPC access mode configuration:
138 XMLRPC access mode configuration:
139
139
140 bugzilla.bzurl
140 bugzilla.bzurl
141 The base URL for the Bugzilla installation.
141 The base URL for the Bugzilla installation.
142 Default ``http://localhost/bugzilla``.
142 Default ``http://localhost/bugzilla``.
143
143
144 bugzilla.user
144 bugzilla.user
145 The username to use to log into Bugzilla via XMLRPC. Default
145 The username to use to log into Bugzilla via XMLRPC. Default
146 ``bugs``.
146 ``bugs``.
147
147
148 bugzilla.password
148 bugzilla.password
149 The password for Bugzilla login.
149 The password for Bugzilla login.
150
150
151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 and also:
152 and also:
153
153
154 bugzilla.bzemail
154 bugzilla.bzemail
155 The Bugzilla email address.
155 The Bugzilla email address.
156
156
157 In addition, the Mercurial email settings must be configured. See the
157 In addition, the Mercurial email settings must be configured. See the
158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159
159
160 MySQL access mode configuration:
160 MySQL access mode configuration:
161
161
162 bugzilla.host
162 bugzilla.host
163 Hostname of the MySQL server holding the Bugzilla database.
163 Hostname of the MySQL server holding the Bugzilla database.
164 Default ``localhost``.
164 Default ``localhost``.
165
165
166 bugzilla.db
166 bugzilla.db
167 Name of the Bugzilla database in MySQL. Default ``bugs``.
167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168
168
169 bugzilla.user
169 bugzilla.user
170 Username to use to access MySQL server. Default ``bugs``.
170 Username to use to access MySQL server. Default ``bugs``.
171
171
172 bugzilla.password
172 bugzilla.password
173 Password to use to access MySQL server.
173 Password to use to access MySQL server.
174
174
175 bugzilla.timeout
175 bugzilla.timeout
176 Database connection timeout (seconds). Default 5.
176 Database connection timeout (seconds). Default 5.
177
177
178 bugzilla.bzuser
178 bugzilla.bzuser
179 Fallback Bugzilla user name to record comments with, if changeset
179 Fallback Bugzilla user name to record comments with, if changeset
180 committer cannot be found as a Bugzilla user.
180 committer cannot be found as a Bugzilla user.
181
181
182 bugzilla.bzdir
182 bugzilla.bzdir
183 Bugzilla install directory. Used by default notify. Default
183 Bugzilla install directory. Used by default notify. Default
184 ``/var/www/html/bugzilla``.
184 ``/var/www/html/bugzilla``.
185
185
186 bugzilla.notify
186 bugzilla.notify
187 The command to run to get Bugzilla to send bug change notification
187 The command to run to get Bugzilla to send bug change notification
188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 id) and ``user`` (committer bugzilla email). Default depends on
189 id) and ``user`` (committer bugzilla email). Default depends on
190 version; from 2.18 it is "cd %(bzdir)s && perl -T
190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 contrib/sendbugmail.pl %(id)s %(user)s".
191 contrib/sendbugmail.pl %(id)s %(user)s".
192
192
193 Activating the extension::
193 Activating the extension::
194
194
195 [extensions]
195 [extensions]
196 bugzilla =
196 bugzilla =
197
197
198 [hooks]
198 [hooks]
199 # run bugzilla hook on every change pulled or pushed in here
199 # run bugzilla hook on every change pulled or pushed in here
200 incoming.bugzilla = python:hgext.bugzilla.hook
200 incoming.bugzilla = python:hgext.bugzilla.hook
201
201
202 Example configurations:
202 Example configurations:
203
203
204 XMLRPC example configuration. This uses the Bugzilla at
204 XMLRPC example configuration. This uses the Bugzilla at
205 ``http://my-project.org/bugzilla``, logging in as user
205 ``http://my-project.org/bugzilla``, logging in as user
206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 with a web interface at ``http://my-project.org/hg``. ::
208 with a web interface at ``http://my-project.org/hg``. ::
209
209
210 [bugzilla]
210 [bugzilla]
211 bzurl=http://my-project.org/bugzilla
211 bzurl=http://my-project.org/bugzilla
212 user=bugmail@my-project.org
212 user=bugmail@my-project.org
213 password=plugh
213 password=plugh
214 version=xmlrpc
214 version=xmlrpc
215 template=Changeset {node|short} in {root|basename}.
215 template=Changeset {node|short} in {root|basename}.
216 {hgweb}/{webroot}/rev/{node|short}\\n
216 {hgweb}/{webroot}/rev/{node|short}\\n
217 {desc}\\n
217 {desc}\\n
218 strip=5
218 strip=5
219
219
220 [web]
220 [web]
221 baseurl=http://my-project.org/hg
221 baseurl=http://my-project.org/hg
222
222
223 XMLRPC+email example configuration. This uses the Bugzilla at
223 XMLRPC+email example configuration. This uses the Bugzilla at
224 ``http://my-project.org/bugzilla``, logging in as user
224 ``http://my-project.org/bugzilla``, logging in as user
225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 with a web interface at ``http://my-project.org/hg``. Bug comments
227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 are sent to the Bugzilla email address
228 are sent to the Bugzilla email address
229 ``bugzilla@my-project.org``. ::
229 ``bugzilla@my-project.org``. ::
230
230
231 [bugzilla]
231 [bugzilla]
232 bzurl=http://my-project.org/bugzilla
232 bzurl=http://my-project.org/bugzilla
233 user=bugmail@my-project.org
233 user=bugmail@my-project.org
234 password=plugh
234 password=plugh
235 version=xmlrpc+email
235 version=xmlrpc+email
236 bzemail=bugzilla@my-project.org
236 bzemail=bugzilla@my-project.org
237 template=Changeset {node|short} in {root|basename}.
237 template=Changeset {node|short} in {root|basename}.
238 {hgweb}/{webroot}/rev/{node|short}\\n
238 {hgweb}/{webroot}/rev/{node|short}\\n
239 {desc}\\n
239 {desc}\\n
240 strip=5
240 strip=5
241
241
242 [web]
242 [web]
243 baseurl=http://my-project.org/hg
243 baseurl=http://my-project.org/hg
244
244
245 [usermap]
245 [usermap]
246 user@emaildomain.com=user.name@bugzilladomain.com
246 user@emaildomain.com=user.name@bugzilladomain.com
247
247
248 MySQL example configuration. This has a local Bugzilla 3.2 installation
248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 the Bugzilla database name is ``bugs`` and MySQL is
250 the Bugzilla database name is ``bugs`` and MySQL is
251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 with a web interface at ``http://my-project.org/hg``. ::
253 with a web interface at ``http://my-project.org/hg``. ::
254
254
255 [bugzilla]
255 [bugzilla]
256 host=localhost
256 host=localhost
257 password=XYZZY
257 password=XYZZY
258 version=3.0
258 version=3.0
259 bzuser=unknown@domain.com
259 bzuser=unknown@domain.com
260 bzdir=/opt/bugzilla-3.2
260 bzdir=/opt/bugzilla-3.2
261 template=Changeset {node|short} in {root|basename}.
261 template=Changeset {node|short} in {root|basename}.
262 {hgweb}/{webroot}/rev/{node|short}\\n
262 {hgweb}/{webroot}/rev/{node|short}\\n
263 {desc}\\n
263 {desc}\\n
264 strip=5
264 strip=5
265
265
266 [web]
266 [web]
267 baseurl=http://my-project.org/hg
267 baseurl=http://my-project.org/hg
268
268
269 [usermap]
269 [usermap]
270 user@emaildomain.com=user.name@bugzilladomain.com
270 user@emaildomain.com=user.name@bugzilladomain.com
271
271
272 All the above add a comment to the Bugzilla bug record of the form::
272 All the above add a comment to the Bugzilla bug record of the form::
273
273
274 Changeset 3b16791d6642 in repository-name.
274 Changeset 3b16791d6642 in repository-name.
275 http://my-project.org/hg/repository-name/rev/3b16791d6642
275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276
276
277 Changeset commit comment. Bug 1234.
277 Changeset commit comment. Bug 1234.
278 '''
278 '''
279
279
280 from __future__ import absolute_import
280 from __future__ import absolute_import
281
281
282 import re
282 import re
283 import time
283 import time
284
284
285 from mercurial.i18n import _
285 from mercurial.i18n import _
286 from mercurial.node import short
286 from mercurial.node import short
287 from mercurial import (
287 from mercurial import (
288 cmdutil,
288 cmdutil,
289 error,
289 error,
290 mail,
290 mail,
291 util,
291 util,
292 )
292 )
293
293
294 urlparse = util.urlparse
294 urlparse = util.urlparse
295 xmlrpclib = util.xmlrpclib
295 xmlrpclib = util.xmlrpclib
296
296
297 # Note for extension authors: ONLY specify testedwith = 'internal' for
297 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
298 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
298 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
299 # be specifying the version(s) of Mercurial they are tested with, or
299 # be specifying the version(s) of Mercurial they are tested with, or
300 # leave the attribute unspecified.
300 # leave the attribute unspecified.
301 testedwith = 'internal'
301 testedwith = 'ships-with-hg-core'
302
302
303 class bzaccess(object):
303 class bzaccess(object):
304 '''Base class for access to Bugzilla.'''
304 '''Base class for access to Bugzilla.'''
305
305
306 def __init__(self, ui):
306 def __init__(self, ui):
307 self.ui = ui
307 self.ui = ui
308 usermap = self.ui.config('bugzilla', 'usermap')
308 usermap = self.ui.config('bugzilla', 'usermap')
309 if usermap:
309 if usermap:
310 self.ui.readconfig(usermap, sections=['usermap'])
310 self.ui.readconfig(usermap, sections=['usermap'])
311
311
312 def map_committer(self, user):
312 def map_committer(self, user):
313 '''map name of committer to Bugzilla user name.'''
313 '''map name of committer to Bugzilla user name.'''
314 for committer, bzuser in self.ui.configitems('usermap'):
314 for committer, bzuser in self.ui.configitems('usermap'):
315 if committer.lower() == user.lower():
315 if committer.lower() == user.lower():
316 return bzuser
316 return bzuser
317 return user
317 return user
318
318
319 # Methods to be implemented by access classes.
319 # Methods to be implemented by access classes.
320 #
320 #
321 # 'bugs' is a dict keyed on bug id, where values are a dict holding
321 # 'bugs' is a dict keyed on bug id, where values are a dict holding
322 # updates to bug state. Recognized dict keys are:
322 # updates to bug state. Recognized dict keys are:
323 #
323 #
324 # 'hours': Value, float containing work hours to be updated.
324 # 'hours': Value, float containing work hours to be updated.
325 # 'fix': If key present, bug is to be marked fixed. Value ignored.
325 # 'fix': If key present, bug is to be marked fixed. Value ignored.
326
326
327 def filter_real_bug_ids(self, bugs):
327 def filter_real_bug_ids(self, bugs):
328 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
328 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
329 pass
329 pass
330
330
331 def filter_cset_known_bug_ids(self, node, bugs):
331 def filter_cset_known_bug_ids(self, node, bugs):
332 '''remove bug IDs where node occurs in comment text from bugs.'''
332 '''remove bug IDs where node occurs in comment text from bugs.'''
333 pass
333 pass
334
334
335 def updatebug(self, bugid, newstate, text, committer):
335 def updatebug(self, bugid, newstate, text, committer):
336 '''update the specified bug. Add comment text and set new states.
336 '''update the specified bug. Add comment text and set new states.
337
337
338 If possible add the comment as being from the committer of
338 If possible add the comment as being from the committer of
339 the changeset. Otherwise use the default Bugzilla user.
339 the changeset. Otherwise use the default Bugzilla user.
340 '''
340 '''
341 pass
341 pass
342
342
343 def notify(self, bugs, committer):
343 def notify(self, bugs, committer):
344 '''Force sending of Bugzilla notification emails.
344 '''Force sending of Bugzilla notification emails.
345
345
346 Only required if the access method does not trigger notification
346 Only required if the access method does not trigger notification
347 emails automatically.
347 emails automatically.
348 '''
348 '''
349 pass
349 pass
350
350
351 # Bugzilla via direct access to MySQL database.
351 # Bugzilla via direct access to MySQL database.
352 class bzmysql(bzaccess):
352 class bzmysql(bzaccess):
353 '''Support for direct MySQL access to Bugzilla.
353 '''Support for direct MySQL access to Bugzilla.
354
354
355 The earliest Bugzilla version this is tested with is version 2.16.
355 The earliest Bugzilla version this is tested with is version 2.16.
356
356
357 If your Bugzilla is version 3.4 or above, you are strongly
357 If your Bugzilla is version 3.4 or above, you are strongly
358 recommended to use the XMLRPC access method instead.
358 recommended to use the XMLRPC access method instead.
359 '''
359 '''
360
360
361 @staticmethod
361 @staticmethod
362 def sql_buglist(ids):
362 def sql_buglist(ids):
363 '''return SQL-friendly list of bug ids'''
363 '''return SQL-friendly list of bug ids'''
364 return '(' + ','.join(map(str, ids)) + ')'
364 return '(' + ','.join(map(str, ids)) + ')'
365
365
366 _MySQLdb = None
366 _MySQLdb = None
367
367
368 def __init__(self, ui):
368 def __init__(self, ui):
369 try:
369 try:
370 import MySQLdb as mysql
370 import MySQLdb as mysql
371 bzmysql._MySQLdb = mysql
371 bzmysql._MySQLdb = mysql
372 except ImportError as err:
372 except ImportError as err:
373 raise error.Abort(_('python mysql support not available: %s') % err)
373 raise error.Abort(_('python mysql support not available: %s') % err)
374
374
375 bzaccess.__init__(self, ui)
375 bzaccess.__init__(self, ui)
376
376
377 host = self.ui.config('bugzilla', 'host', 'localhost')
377 host = self.ui.config('bugzilla', 'host', 'localhost')
378 user = self.ui.config('bugzilla', 'user', 'bugs')
378 user = self.ui.config('bugzilla', 'user', 'bugs')
379 passwd = self.ui.config('bugzilla', 'password')
379 passwd = self.ui.config('bugzilla', 'password')
380 db = self.ui.config('bugzilla', 'db', 'bugs')
380 db = self.ui.config('bugzilla', 'db', 'bugs')
381 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
381 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
382 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
382 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
383 (host, db, user, '*' * len(passwd)))
383 (host, db, user, '*' * len(passwd)))
384 self.conn = bzmysql._MySQLdb.connect(host=host,
384 self.conn = bzmysql._MySQLdb.connect(host=host,
385 user=user, passwd=passwd,
385 user=user, passwd=passwd,
386 db=db,
386 db=db,
387 connect_timeout=timeout)
387 connect_timeout=timeout)
388 self.cursor = self.conn.cursor()
388 self.cursor = self.conn.cursor()
389 self.longdesc_id = self.get_longdesc_id()
389 self.longdesc_id = self.get_longdesc_id()
390 self.user_ids = {}
390 self.user_ids = {}
391 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
391 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
392
392
393 def run(self, *args, **kwargs):
393 def run(self, *args, **kwargs):
394 '''run a query.'''
394 '''run a query.'''
395 self.ui.note(_('query: %s %s\n') % (args, kwargs))
395 self.ui.note(_('query: %s %s\n') % (args, kwargs))
396 try:
396 try:
397 self.cursor.execute(*args, **kwargs)
397 self.cursor.execute(*args, **kwargs)
398 except bzmysql._MySQLdb.MySQLError:
398 except bzmysql._MySQLdb.MySQLError:
399 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
399 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
400 raise
400 raise
401
401
402 def get_longdesc_id(self):
402 def get_longdesc_id(self):
403 '''get identity of longdesc field'''
403 '''get identity of longdesc field'''
404 self.run('select fieldid from fielddefs where name = "longdesc"')
404 self.run('select fieldid from fielddefs where name = "longdesc"')
405 ids = self.cursor.fetchall()
405 ids = self.cursor.fetchall()
406 if len(ids) != 1:
406 if len(ids) != 1:
407 raise error.Abort(_('unknown database schema'))
407 raise error.Abort(_('unknown database schema'))
408 return ids[0][0]
408 return ids[0][0]
409
409
410 def filter_real_bug_ids(self, bugs):
410 def filter_real_bug_ids(self, bugs):
411 '''filter not-existing bugs from set.'''
411 '''filter not-existing bugs from set.'''
412 self.run('select bug_id from bugs where bug_id in %s' %
412 self.run('select bug_id from bugs where bug_id in %s' %
413 bzmysql.sql_buglist(bugs.keys()))
413 bzmysql.sql_buglist(bugs.keys()))
414 existing = [id for (id,) in self.cursor.fetchall()]
414 existing = [id for (id,) in self.cursor.fetchall()]
415 for id in bugs.keys():
415 for id in bugs.keys():
416 if id not in existing:
416 if id not in existing:
417 self.ui.status(_('bug %d does not exist\n') % id)
417 self.ui.status(_('bug %d does not exist\n') % id)
418 del bugs[id]
418 del bugs[id]
419
419
420 def filter_cset_known_bug_ids(self, node, bugs):
420 def filter_cset_known_bug_ids(self, node, bugs):
421 '''filter bug ids that already refer to this changeset from set.'''
421 '''filter bug ids that already refer to this changeset from set.'''
422 self.run('''select bug_id from longdescs where
422 self.run('''select bug_id from longdescs where
423 bug_id in %s and thetext like "%%%s%%"''' %
423 bug_id in %s and thetext like "%%%s%%"''' %
424 (bzmysql.sql_buglist(bugs.keys()), short(node)))
424 (bzmysql.sql_buglist(bugs.keys()), short(node)))
425 for (id,) in self.cursor.fetchall():
425 for (id,) in self.cursor.fetchall():
426 self.ui.status(_('bug %d already knows about changeset %s\n') %
426 self.ui.status(_('bug %d already knows about changeset %s\n') %
427 (id, short(node)))
427 (id, short(node)))
428 del bugs[id]
428 del bugs[id]
429
429
430 def notify(self, bugs, committer):
430 def notify(self, bugs, committer):
431 '''tell bugzilla to send mail.'''
431 '''tell bugzilla to send mail.'''
432 self.ui.status(_('telling bugzilla to send mail:\n'))
432 self.ui.status(_('telling bugzilla to send mail:\n'))
433 (user, userid) = self.get_bugzilla_user(committer)
433 (user, userid) = self.get_bugzilla_user(committer)
434 for id in bugs.keys():
434 for id in bugs.keys():
435 self.ui.status(_(' bug %s\n') % id)
435 self.ui.status(_(' bug %s\n') % id)
436 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
436 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
437 bzdir = self.ui.config('bugzilla', 'bzdir',
437 bzdir = self.ui.config('bugzilla', 'bzdir',
438 '/var/www/html/bugzilla')
438 '/var/www/html/bugzilla')
439 try:
439 try:
440 # Backwards-compatible with old notify string, which
440 # Backwards-compatible with old notify string, which
441 # took one string. This will throw with a new format
441 # took one string. This will throw with a new format
442 # string.
442 # string.
443 cmd = cmdfmt % id
443 cmd = cmdfmt % id
444 except TypeError:
444 except TypeError:
445 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
445 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
446 self.ui.note(_('running notify command %s\n') % cmd)
446 self.ui.note(_('running notify command %s\n') % cmd)
447 fp = util.popen('(%s) 2>&1' % cmd)
447 fp = util.popen('(%s) 2>&1' % cmd)
448 out = fp.read()
448 out = fp.read()
449 ret = fp.close()
449 ret = fp.close()
450 if ret:
450 if ret:
451 self.ui.warn(out)
451 self.ui.warn(out)
452 raise error.Abort(_('bugzilla notify command %s') %
452 raise error.Abort(_('bugzilla notify command %s') %
453 util.explainexit(ret)[0])
453 util.explainexit(ret)[0])
454 self.ui.status(_('done\n'))
454 self.ui.status(_('done\n'))
455
455
456 def get_user_id(self, user):
456 def get_user_id(self, user):
457 '''look up numeric bugzilla user id.'''
457 '''look up numeric bugzilla user id.'''
458 try:
458 try:
459 return self.user_ids[user]
459 return self.user_ids[user]
460 except KeyError:
460 except KeyError:
461 try:
461 try:
462 userid = int(user)
462 userid = int(user)
463 except ValueError:
463 except ValueError:
464 self.ui.note(_('looking up user %s\n') % user)
464 self.ui.note(_('looking up user %s\n') % user)
465 self.run('''select userid from profiles
465 self.run('''select userid from profiles
466 where login_name like %s''', user)
466 where login_name like %s''', user)
467 all = self.cursor.fetchall()
467 all = self.cursor.fetchall()
468 if len(all) != 1:
468 if len(all) != 1:
469 raise KeyError(user)
469 raise KeyError(user)
470 userid = int(all[0][0])
470 userid = int(all[0][0])
471 self.user_ids[user] = userid
471 self.user_ids[user] = userid
472 return userid
472 return userid
473
473
474 def get_bugzilla_user(self, committer):
474 def get_bugzilla_user(self, committer):
475 '''See if committer is a registered bugzilla user. Return
475 '''See if committer is a registered bugzilla user. Return
476 bugzilla username and userid if so. If not, return default
476 bugzilla username and userid if so. If not, return default
477 bugzilla username and userid.'''
477 bugzilla username and userid.'''
478 user = self.map_committer(committer)
478 user = self.map_committer(committer)
479 try:
479 try:
480 userid = self.get_user_id(user)
480 userid = self.get_user_id(user)
481 except KeyError:
481 except KeyError:
482 try:
482 try:
483 defaultuser = self.ui.config('bugzilla', 'bzuser')
483 defaultuser = self.ui.config('bugzilla', 'bzuser')
484 if not defaultuser:
484 if not defaultuser:
485 raise error.Abort(_('cannot find bugzilla user id for %s') %
485 raise error.Abort(_('cannot find bugzilla user id for %s') %
486 user)
486 user)
487 userid = self.get_user_id(defaultuser)
487 userid = self.get_user_id(defaultuser)
488 user = defaultuser
488 user = defaultuser
489 except KeyError:
489 except KeyError:
490 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
490 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
491 % (user, defaultuser))
491 % (user, defaultuser))
492 return (user, userid)
492 return (user, userid)
493
493
494 def updatebug(self, bugid, newstate, text, committer):
494 def updatebug(self, bugid, newstate, text, committer):
495 '''update bug state with comment text.
495 '''update bug state with comment text.
496
496
497 Try adding comment as committer of changeset, otherwise as
497 Try adding comment as committer of changeset, otherwise as
498 default bugzilla user.'''
498 default bugzilla user.'''
499 if len(newstate) > 0:
499 if len(newstate) > 0:
500 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
500 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
501
501
502 (user, userid) = self.get_bugzilla_user(committer)
502 (user, userid) = self.get_bugzilla_user(committer)
503 now = time.strftime('%Y-%m-%d %H:%M:%S')
503 now = time.strftime('%Y-%m-%d %H:%M:%S')
504 self.run('''insert into longdescs
504 self.run('''insert into longdescs
505 (bug_id, who, bug_when, thetext)
505 (bug_id, who, bug_when, thetext)
506 values (%s, %s, %s, %s)''',
506 values (%s, %s, %s, %s)''',
507 (bugid, userid, now, text))
507 (bugid, userid, now, text))
508 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
508 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
509 values (%s, %s, %s, %s)''',
509 values (%s, %s, %s, %s)''',
510 (bugid, userid, now, self.longdesc_id))
510 (bugid, userid, now, self.longdesc_id))
511 self.conn.commit()
511 self.conn.commit()
512
512
513 class bzmysql_2_18(bzmysql):
513 class bzmysql_2_18(bzmysql):
514 '''support for bugzilla 2.18 series.'''
514 '''support for bugzilla 2.18 series.'''
515
515
516 def __init__(self, ui):
516 def __init__(self, ui):
517 bzmysql.__init__(self, ui)
517 bzmysql.__init__(self, ui)
518 self.default_notify = \
518 self.default_notify = \
519 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
519 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
520
520
521 class bzmysql_3_0(bzmysql_2_18):
521 class bzmysql_3_0(bzmysql_2_18):
522 '''support for bugzilla 3.0 series.'''
522 '''support for bugzilla 3.0 series.'''
523
523
524 def __init__(self, ui):
524 def __init__(self, ui):
525 bzmysql_2_18.__init__(self, ui)
525 bzmysql_2_18.__init__(self, ui)
526
526
527 def get_longdesc_id(self):
527 def get_longdesc_id(self):
528 '''get identity of longdesc field'''
528 '''get identity of longdesc field'''
529 self.run('select id from fielddefs where name = "longdesc"')
529 self.run('select id from fielddefs where name = "longdesc"')
530 ids = self.cursor.fetchall()
530 ids = self.cursor.fetchall()
531 if len(ids) != 1:
531 if len(ids) != 1:
532 raise error.Abort(_('unknown database schema'))
532 raise error.Abort(_('unknown database schema'))
533 return ids[0][0]
533 return ids[0][0]
534
534
535 # Bugzilla via XMLRPC interface.
535 # Bugzilla via XMLRPC interface.
536
536
537 class cookietransportrequest(object):
537 class cookietransportrequest(object):
538 """A Transport request method that retains cookies over its lifetime.
538 """A Transport request method that retains cookies over its lifetime.
539
539
540 The regular xmlrpclib transports ignore cookies. Which causes
540 The regular xmlrpclib transports ignore cookies. Which causes
541 a bit of a problem when you need a cookie-based login, as with
541 a bit of a problem when you need a cookie-based login, as with
542 the Bugzilla XMLRPC interface prior to 4.4.3.
542 the Bugzilla XMLRPC interface prior to 4.4.3.
543
543
544 So this is a helper for defining a Transport which looks for
544 So this is a helper for defining a Transport which looks for
545 cookies being set in responses and saves them to add to all future
545 cookies being set in responses and saves them to add to all future
546 requests.
546 requests.
547 """
547 """
548
548
549 # Inspiration drawn from
549 # Inspiration drawn from
550 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
550 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
551 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
551 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
552
552
553 cookies = []
553 cookies = []
554 def send_cookies(self, connection):
554 def send_cookies(self, connection):
555 if self.cookies:
555 if self.cookies:
556 for cookie in self.cookies:
556 for cookie in self.cookies:
557 connection.putheader("Cookie", cookie)
557 connection.putheader("Cookie", cookie)
558
558
559 def request(self, host, handler, request_body, verbose=0):
559 def request(self, host, handler, request_body, verbose=0):
560 self.verbose = verbose
560 self.verbose = verbose
561 self.accept_gzip_encoding = False
561 self.accept_gzip_encoding = False
562
562
563 # issue XML-RPC request
563 # issue XML-RPC request
564 h = self.make_connection(host)
564 h = self.make_connection(host)
565 if verbose:
565 if verbose:
566 h.set_debuglevel(1)
566 h.set_debuglevel(1)
567
567
568 self.send_request(h, handler, request_body)
568 self.send_request(h, handler, request_body)
569 self.send_host(h, host)
569 self.send_host(h, host)
570 self.send_cookies(h)
570 self.send_cookies(h)
571 self.send_user_agent(h)
571 self.send_user_agent(h)
572 self.send_content(h, request_body)
572 self.send_content(h, request_body)
573
573
574 # Deal with differences between Python 2.4-2.6 and 2.7.
574 # Deal with differences between Python 2.4-2.6 and 2.7.
575 # In the former h is a HTTP(S). In the latter it's a
575 # In the former h is a HTTP(S). In the latter it's a
576 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
576 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
577 # HTTP(S) has an underlying HTTP(S)Connection, so extract
577 # HTTP(S) has an underlying HTTP(S)Connection, so extract
578 # that and use it.
578 # that and use it.
579 try:
579 try:
580 response = h.getresponse()
580 response = h.getresponse()
581 except AttributeError:
581 except AttributeError:
582 response = h._conn.getresponse()
582 response = h._conn.getresponse()
583
583
584 # Add any cookie definitions to our list.
584 # Add any cookie definitions to our list.
585 for header in response.msg.getallmatchingheaders("Set-Cookie"):
585 for header in response.msg.getallmatchingheaders("Set-Cookie"):
586 val = header.split(": ", 1)[1]
586 val = header.split(": ", 1)[1]
587 cookie = val.split(";", 1)[0]
587 cookie = val.split(";", 1)[0]
588 self.cookies.append(cookie)
588 self.cookies.append(cookie)
589
589
590 if response.status != 200:
590 if response.status != 200:
591 raise xmlrpclib.ProtocolError(host + handler, response.status,
591 raise xmlrpclib.ProtocolError(host + handler, response.status,
592 response.reason, response.msg.headers)
592 response.reason, response.msg.headers)
593
593
594 payload = response.read()
594 payload = response.read()
595 parser, unmarshaller = self.getparser()
595 parser, unmarshaller = self.getparser()
596 parser.feed(payload)
596 parser.feed(payload)
597 parser.close()
597 parser.close()
598
598
599 return unmarshaller.close()
599 return unmarshaller.close()
600
600
601 # The explicit calls to the underlying xmlrpclib __init__() methods are
601 # The explicit calls to the underlying xmlrpclib __init__() methods are
602 # necessary. The xmlrpclib.Transport classes are old-style classes, and
602 # necessary. The xmlrpclib.Transport classes are old-style classes, and
603 # it turns out their __init__() doesn't get called when doing multiple
603 # it turns out their __init__() doesn't get called when doing multiple
604 # inheritance with a new-style class.
604 # inheritance with a new-style class.
605 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
605 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
606 def __init__(self, use_datetime=0):
606 def __init__(self, use_datetime=0):
607 if util.safehasattr(xmlrpclib.Transport, "__init__"):
607 if util.safehasattr(xmlrpclib.Transport, "__init__"):
608 xmlrpclib.Transport.__init__(self, use_datetime)
608 xmlrpclib.Transport.__init__(self, use_datetime)
609
609
610 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
610 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
611 def __init__(self, use_datetime=0):
611 def __init__(self, use_datetime=0):
612 if util.safehasattr(xmlrpclib.Transport, "__init__"):
612 if util.safehasattr(xmlrpclib.Transport, "__init__"):
613 xmlrpclib.SafeTransport.__init__(self, use_datetime)
613 xmlrpclib.SafeTransport.__init__(self, use_datetime)
614
614
615 class bzxmlrpc(bzaccess):
615 class bzxmlrpc(bzaccess):
616 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
616 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
617
617
618 Requires a minimum Bugzilla version 3.4.
618 Requires a minimum Bugzilla version 3.4.
619 """
619 """
620
620
621 def __init__(self, ui):
621 def __init__(self, ui):
622 bzaccess.__init__(self, ui)
622 bzaccess.__init__(self, ui)
623
623
624 bzweb = self.ui.config('bugzilla', 'bzurl',
624 bzweb = self.ui.config('bugzilla', 'bzurl',
625 'http://localhost/bugzilla/')
625 'http://localhost/bugzilla/')
626 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
626 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
627
627
628 user = self.ui.config('bugzilla', 'user', 'bugs')
628 user = self.ui.config('bugzilla', 'user', 'bugs')
629 passwd = self.ui.config('bugzilla', 'password')
629 passwd = self.ui.config('bugzilla', 'password')
630
630
631 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
631 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
632 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
632 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
633 'FIXED')
633 'FIXED')
634
634
635 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
635 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
636 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
636 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
637 self.bzvermajor = int(ver[0])
637 self.bzvermajor = int(ver[0])
638 self.bzverminor = int(ver[1])
638 self.bzverminor = int(ver[1])
639 login = self.bzproxy.User.login({'login': user, 'password': passwd,
639 login = self.bzproxy.User.login({'login': user, 'password': passwd,
640 'restrict_login': True})
640 'restrict_login': True})
641 self.bztoken = login.get('token', '')
641 self.bztoken = login.get('token', '')
642
642
643 def transport(self, uri):
643 def transport(self, uri):
644 if urlparse.urlparse(uri, "http")[0] == "https":
644 if urlparse.urlparse(uri, "http")[0] == "https":
645 return cookiesafetransport()
645 return cookiesafetransport()
646 else:
646 else:
647 return cookietransport()
647 return cookietransport()
648
648
649 def get_bug_comments(self, id):
649 def get_bug_comments(self, id):
650 """Return a string with all comment text for a bug."""
650 """Return a string with all comment text for a bug."""
651 c = self.bzproxy.Bug.comments({'ids': [id],
651 c = self.bzproxy.Bug.comments({'ids': [id],
652 'include_fields': ['text'],
652 'include_fields': ['text'],
653 'token': self.bztoken})
653 'token': self.bztoken})
654 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
654 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
655
655
656 def filter_real_bug_ids(self, bugs):
656 def filter_real_bug_ids(self, bugs):
657 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
657 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
658 'include_fields': [],
658 'include_fields': [],
659 'permissive': True,
659 'permissive': True,
660 'token': self.bztoken,
660 'token': self.bztoken,
661 })
661 })
662 for badbug in probe['faults']:
662 for badbug in probe['faults']:
663 id = badbug['id']
663 id = badbug['id']
664 self.ui.status(_('bug %d does not exist\n') % id)
664 self.ui.status(_('bug %d does not exist\n') % id)
665 del bugs[id]
665 del bugs[id]
666
666
667 def filter_cset_known_bug_ids(self, node, bugs):
667 def filter_cset_known_bug_ids(self, node, bugs):
668 for id in sorted(bugs.keys()):
668 for id in sorted(bugs.keys()):
669 if self.get_bug_comments(id).find(short(node)) != -1:
669 if self.get_bug_comments(id).find(short(node)) != -1:
670 self.ui.status(_('bug %d already knows about changeset %s\n') %
670 self.ui.status(_('bug %d already knows about changeset %s\n') %
671 (id, short(node)))
671 (id, short(node)))
672 del bugs[id]
672 del bugs[id]
673
673
674 def updatebug(self, bugid, newstate, text, committer):
674 def updatebug(self, bugid, newstate, text, committer):
675 args = {}
675 args = {}
676 if 'hours' in newstate:
676 if 'hours' in newstate:
677 args['work_time'] = newstate['hours']
677 args['work_time'] = newstate['hours']
678
678
679 if self.bzvermajor >= 4:
679 if self.bzvermajor >= 4:
680 args['ids'] = [bugid]
680 args['ids'] = [bugid]
681 args['comment'] = {'body' : text}
681 args['comment'] = {'body' : text}
682 if 'fix' in newstate:
682 if 'fix' in newstate:
683 args['status'] = self.fixstatus
683 args['status'] = self.fixstatus
684 args['resolution'] = self.fixresolution
684 args['resolution'] = self.fixresolution
685 args['token'] = self.bztoken
685 args['token'] = self.bztoken
686 self.bzproxy.Bug.update(args)
686 self.bzproxy.Bug.update(args)
687 else:
687 else:
688 if 'fix' in newstate:
688 if 'fix' in newstate:
689 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
689 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
690 "to mark bugs fixed\n"))
690 "to mark bugs fixed\n"))
691 args['id'] = bugid
691 args['id'] = bugid
692 args['comment'] = text
692 args['comment'] = text
693 self.bzproxy.Bug.add_comment(args)
693 self.bzproxy.Bug.add_comment(args)
694
694
695 class bzxmlrpcemail(bzxmlrpc):
695 class bzxmlrpcemail(bzxmlrpc):
696 """Read data from Bugzilla via XMLRPC, send updates via email.
696 """Read data from Bugzilla via XMLRPC, send updates via email.
697
697
698 Advantages of sending updates via email:
698 Advantages of sending updates via email:
699 1. Comments can be added as any user, not just logged in user.
699 1. Comments can be added as any user, not just logged in user.
700 2. Bug statuses or other fields not accessible via XMLRPC can
700 2. Bug statuses or other fields not accessible via XMLRPC can
701 potentially be updated.
701 potentially be updated.
702
702
703 There is no XMLRPC function to change bug status before Bugzilla
703 There is no XMLRPC function to change bug status before Bugzilla
704 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
704 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
705 But bugs can be marked fixed via email from 3.4 onwards.
705 But bugs can be marked fixed via email from 3.4 onwards.
706 """
706 """
707
707
708 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
708 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
709 # in-email fields are specified as '@<fieldname> = <value>'. In
709 # in-email fields are specified as '@<fieldname> = <value>'. In
710 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
710 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
711 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
711 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
712 # compatibility, but rather than rely on this use the new format for
712 # compatibility, but rather than rely on this use the new format for
713 # 4.0 onwards.
713 # 4.0 onwards.
714
714
715 def __init__(self, ui):
715 def __init__(self, ui):
716 bzxmlrpc.__init__(self, ui)
716 bzxmlrpc.__init__(self, ui)
717
717
718 self.bzemail = self.ui.config('bugzilla', 'bzemail')
718 self.bzemail = self.ui.config('bugzilla', 'bzemail')
719 if not self.bzemail:
719 if not self.bzemail:
720 raise error.Abort(_("configuration 'bzemail' missing"))
720 raise error.Abort(_("configuration 'bzemail' missing"))
721 mail.validateconfig(self.ui)
721 mail.validateconfig(self.ui)
722
722
723 def makecommandline(self, fieldname, value):
723 def makecommandline(self, fieldname, value):
724 if self.bzvermajor >= 4:
724 if self.bzvermajor >= 4:
725 return "@%s %s" % (fieldname, str(value))
725 return "@%s %s" % (fieldname, str(value))
726 else:
726 else:
727 if fieldname == "id":
727 if fieldname == "id":
728 fieldname = "bug_id"
728 fieldname = "bug_id"
729 return "@%s = %s" % (fieldname, str(value))
729 return "@%s = %s" % (fieldname, str(value))
730
730
731 def send_bug_modify_email(self, bugid, commands, comment, committer):
731 def send_bug_modify_email(self, bugid, commands, comment, committer):
732 '''send modification message to Bugzilla bug via email.
732 '''send modification message to Bugzilla bug via email.
733
733
734 The message format is documented in the Bugzilla email_in.pl
734 The message format is documented in the Bugzilla email_in.pl
735 specification. commands is a list of command lines, comment is the
735 specification. commands is a list of command lines, comment is the
736 comment text.
736 comment text.
737
737
738 To stop users from crafting commit comments with
738 To stop users from crafting commit comments with
739 Bugzilla commands, specify the bug ID via the message body, rather
739 Bugzilla commands, specify the bug ID via the message body, rather
740 than the subject line, and leave a blank line after it.
740 than the subject line, and leave a blank line after it.
741 '''
741 '''
742 user = self.map_committer(committer)
742 user = self.map_committer(committer)
743 matches = self.bzproxy.User.get({'match': [user],
743 matches = self.bzproxy.User.get({'match': [user],
744 'token': self.bztoken})
744 'token': self.bztoken})
745 if not matches['users']:
745 if not matches['users']:
746 user = self.ui.config('bugzilla', 'user', 'bugs')
746 user = self.ui.config('bugzilla', 'user', 'bugs')
747 matches = self.bzproxy.User.get({'match': [user],
747 matches = self.bzproxy.User.get({'match': [user],
748 'token': self.bztoken})
748 'token': self.bztoken})
749 if not matches['users']:
749 if not matches['users']:
750 raise error.Abort(_("default bugzilla user %s email not found")
750 raise error.Abort(_("default bugzilla user %s email not found")
751 % user)
751 % user)
752 user = matches['users'][0]['email']
752 user = matches['users'][0]['email']
753 commands.append(self.makecommandline("id", bugid))
753 commands.append(self.makecommandline("id", bugid))
754
754
755 text = "\n".join(commands) + "\n\n" + comment
755 text = "\n".join(commands) + "\n\n" + comment
756
756
757 _charsets = mail._charsets(self.ui)
757 _charsets = mail._charsets(self.ui)
758 user = mail.addressencode(self.ui, user, _charsets)
758 user = mail.addressencode(self.ui, user, _charsets)
759 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
759 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
760 msg = mail.mimeencode(self.ui, text, _charsets)
760 msg = mail.mimeencode(self.ui, text, _charsets)
761 msg['From'] = user
761 msg['From'] = user
762 msg['To'] = bzemail
762 msg['To'] = bzemail
763 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
763 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
764 sendmail = mail.connect(self.ui)
764 sendmail = mail.connect(self.ui)
765 sendmail(user, bzemail, msg.as_string())
765 sendmail(user, bzemail, msg.as_string())
766
766
767 def updatebug(self, bugid, newstate, text, committer):
767 def updatebug(self, bugid, newstate, text, committer):
768 cmds = []
768 cmds = []
769 if 'hours' in newstate:
769 if 'hours' in newstate:
770 cmds.append(self.makecommandline("work_time", newstate['hours']))
770 cmds.append(self.makecommandline("work_time", newstate['hours']))
771 if 'fix' in newstate:
771 if 'fix' in newstate:
772 cmds.append(self.makecommandline("bug_status", self.fixstatus))
772 cmds.append(self.makecommandline("bug_status", self.fixstatus))
773 cmds.append(self.makecommandline("resolution", self.fixresolution))
773 cmds.append(self.makecommandline("resolution", self.fixresolution))
774 self.send_bug_modify_email(bugid, cmds, text, committer)
774 self.send_bug_modify_email(bugid, cmds, text, committer)
775
775
776 class bugzilla(object):
776 class bugzilla(object):
777 # supported versions of bugzilla. different versions have
777 # supported versions of bugzilla. different versions have
778 # different schemas.
778 # different schemas.
779 _versions = {
779 _versions = {
780 '2.16': bzmysql,
780 '2.16': bzmysql,
781 '2.18': bzmysql_2_18,
781 '2.18': bzmysql_2_18,
782 '3.0': bzmysql_3_0,
782 '3.0': bzmysql_3_0,
783 'xmlrpc': bzxmlrpc,
783 'xmlrpc': bzxmlrpc,
784 'xmlrpc+email': bzxmlrpcemail
784 'xmlrpc+email': bzxmlrpcemail
785 }
785 }
786
786
787 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
787 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
788 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
788 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
789 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
789 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
790
790
791 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
791 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
792 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
792 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
793 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
793 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
794 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
794 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
795
795
796 def __init__(self, ui, repo):
796 def __init__(self, ui, repo):
797 self.ui = ui
797 self.ui = ui
798 self.repo = repo
798 self.repo = repo
799
799
800 bzversion = self.ui.config('bugzilla', 'version')
800 bzversion = self.ui.config('bugzilla', 'version')
801 try:
801 try:
802 bzclass = bugzilla._versions[bzversion]
802 bzclass = bugzilla._versions[bzversion]
803 except KeyError:
803 except KeyError:
804 raise error.Abort(_('bugzilla version %s not supported') %
804 raise error.Abort(_('bugzilla version %s not supported') %
805 bzversion)
805 bzversion)
806 self.bzdriver = bzclass(self.ui)
806 self.bzdriver = bzclass(self.ui)
807
807
808 self.bug_re = re.compile(
808 self.bug_re = re.compile(
809 self.ui.config('bugzilla', 'regexp',
809 self.ui.config('bugzilla', 'regexp',
810 bugzilla._default_bug_re), re.IGNORECASE)
810 bugzilla._default_bug_re), re.IGNORECASE)
811 self.fix_re = re.compile(
811 self.fix_re = re.compile(
812 self.ui.config('bugzilla', 'fixregexp',
812 self.ui.config('bugzilla', 'fixregexp',
813 bugzilla._default_fix_re), re.IGNORECASE)
813 bugzilla._default_fix_re), re.IGNORECASE)
814 self.split_re = re.compile(r'\D+')
814 self.split_re = re.compile(r'\D+')
815
815
816 def find_bugs(self, ctx):
816 def find_bugs(self, ctx):
817 '''return bugs dictionary created from commit comment.
817 '''return bugs dictionary created from commit comment.
818
818
819 Extract bug info from changeset comments. Filter out any that are
819 Extract bug info from changeset comments. Filter out any that are
820 not known to Bugzilla, and any that already have a reference to
820 not known to Bugzilla, and any that already have a reference to
821 the given changeset in their comments.
821 the given changeset in their comments.
822 '''
822 '''
823 start = 0
823 start = 0
824 hours = 0.0
824 hours = 0.0
825 bugs = {}
825 bugs = {}
826 bugmatch = self.bug_re.search(ctx.description(), start)
826 bugmatch = self.bug_re.search(ctx.description(), start)
827 fixmatch = self.fix_re.search(ctx.description(), start)
827 fixmatch = self.fix_re.search(ctx.description(), start)
828 while True:
828 while True:
829 bugattribs = {}
829 bugattribs = {}
830 if not bugmatch and not fixmatch:
830 if not bugmatch and not fixmatch:
831 break
831 break
832 if not bugmatch:
832 if not bugmatch:
833 m = fixmatch
833 m = fixmatch
834 elif not fixmatch:
834 elif not fixmatch:
835 m = bugmatch
835 m = bugmatch
836 else:
836 else:
837 if bugmatch.start() < fixmatch.start():
837 if bugmatch.start() < fixmatch.start():
838 m = bugmatch
838 m = bugmatch
839 else:
839 else:
840 m = fixmatch
840 m = fixmatch
841 start = m.end()
841 start = m.end()
842 if m is bugmatch:
842 if m is bugmatch:
843 bugmatch = self.bug_re.search(ctx.description(), start)
843 bugmatch = self.bug_re.search(ctx.description(), start)
844 if 'fix' in bugattribs:
844 if 'fix' in bugattribs:
845 del bugattribs['fix']
845 del bugattribs['fix']
846 else:
846 else:
847 fixmatch = self.fix_re.search(ctx.description(), start)
847 fixmatch = self.fix_re.search(ctx.description(), start)
848 bugattribs['fix'] = None
848 bugattribs['fix'] = None
849
849
850 try:
850 try:
851 ids = m.group('ids')
851 ids = m.group('ids')
852 except IndexError:
852 except IndexError:
853 ids = m.group(1)
853 ids = m.group(1)
854 try:
854 try:
855 hours = float(m.group('hours'))
855 hours = float(m.group('hours'))
856 bugattribs['hours'] = hours
856 bugattribs['hours'] = hours
857 except IndexError:
857 except IndexError:
858 pass
858 pass
859 except TypeError:
859 except TypeError:
860 pass
860 pass
861 except ValueError:
861 except ValueError:
862 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
862 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
863
863
864 for id in self.split_re.split(ids):
864 for id in self.split_re.split(ids):
865 if not id:
865 if not id:
866 continue
866 continue
867 bugs[int(id)] = bugattribs
867 bugs[int(id)] = bugattribs
868 if bugs:
868 if bugs:
869 self.bzdriver.filter_real_bug_ids(bugs)
869 self.bzdriver.filter_real_bug_ids(bugs)
870 if bugs:
870 if bugs:
871 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
871 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
872 return bugs
872 return bugs
873
873
874 def update(self, bugid, newstate, ctx):
874 def update(self, bugid, newstate, ctx):
875 '''update bugzilla bug with reference to changeset.'''
875 '''update bugzilla bug with reference to changeset.'''
876
876
877 def webroot(root):
877 def webroot(root):
878 '''strip leading prefix of repo root and turn into
878 '''strip leading prefix of repo root and turn into
879 url-safe path.'''
879 url-safe path.'''
880 count = int(self.ui.config('bugzilla', 'strip', 0))
880 count = int(self.ui.config('bugzilla', 'strip', 0))
881 root = util.pconvert(root)
881 root = util.pconvert(root)
882 while count > 0:
882 while count > 0:
883 c = root.find('/')
883 c = root.find('/')
884 if c == -1:
884 if c == -1:
885 break
885 break
886 root = root[c + 1:]
886 root = root[c + 1:]
887 count -= 1
887 count -= 1
888 return root
888 return root
889
889
890 mapfile = None
890 mapfile = None
891 tmpl = self.ui.config('bugzilla', 'template')
891 tmpl = self.ui.config('bugzilla', 'template')
892 if not tmpl:
892 if not tmpl:
893 mapfile = self.ui.config('bugzilla', 'style')
893 mapfile = self.ui.config('bugzilla', 'style')
894 if not mapfile and not tmpl:
894 if not mapfile and not tmpl:
895 tmpl = _('changeset {node|short} in repo {root} refers '
895 tmpl = _('changeset {node|short} in repo {root} refers '
896 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
896 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
897 t = cmdutil.changeset_templater(self.ui, self.repo,
897 t = cmdutil.changeset_templater(self.ui, self.repo,
898 False, None, tmpl, mapfile, False)
898 False, None, tmpl, mapfile, False)
899 self.ui.pushbuffer()
899 self.ui.pushbuffer()
900 t.show(ctx, changes=ctx.changeset(),
900 t.show(ctx, changes=ctx.changeset(),
901 bug=str(bugid),
901 bug=str(bugid),
902 hgweb=self.ui.config('web', 'baseurl'),
902 hgweb=self.ui.config('web', 'baseurl'),
903 root=self.repo.root,
903 root=self.repo.root,
904 webroot=webroot(self.repo.root))
904 webroot=webroot(self.repo.root))
905 data = self.ui.popbuffer()
905 data = self.ui.popbuffer()
906 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
906 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
907
907
908 def notify(self, bugs, committer):
908 def notify(self, bugs, committer):
909 '''ensure Bugzilla users are notified of bug change.'''
909 '''ensure Bugzilla users are notified of bug change.'''
910 self.bzdriver.notify(bugs, committer)
910 self.bzdriver.notify(bugs, committer)
911
911
912 def hook(ui, repo, hooktype, node=None, **kwargs):
912 def hook(ui, repo, hooktype, node=None, **kwargs):
913 '''add comment to bugzilla for each changeset that refers to a
913 '''add comment to bugzilla for each changeset that refers to a
914 bugzilla bug id. only add a comment once per bug, so same change
914 bugzilla bug id. only add a comment once per bug, so same change
915 seen multiple times does not fill bug with duplicate data.'''
915 seen multiple times does not fill bug with duplicate data.'''
916 if node is None:
916 if node is None:
917 raise error.Abort(_('hook type %s does not pass a changeset id') %
917 raise error.Abort(_('hook type %s does not pass a changeset id') %
918 hooktype)
918 hooktype)
919 try:
919 try:
920 bz = bugzilla(ui, repo)
920 bz = bugzilla(ui, repo)
921 ctx = repo[node]
921 ctx = repo[node]
922 bugs = bz.find_bugs(ctx)
922 bugs = bz.find_bugs(ctx)
923 if bugs:
923 if bugs:
924 for bug in bugs:
924 for bug in bugs:
925 bz.update(bug, bugs[bug], ctx)
925 bz.update(bug, bugs[bug], ctx)
926 bz.notify(bugs, util.email(ctx.user()))
926 bz.notify(bugs, util.email(ctx.user()))
927 except Exception as e:
927 except Exception as e:
928 raise error.Abort(_('Bugzilla error: %s') % e)
928 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,190 +1,190 b''
1 # Copyright (C) 2015 - Mike Edgar <adgar@google.com>
1 # Copyright (C) 2015 - Mike Edgar <adgar@google.com>
2 #
2 #
3 # This extension enables removal of file content at a given revision,
3 # This extension enables removal of file content at a given revision,
4 # rewriting the data/metadata of successive revisions to preserve revision log
4 # rewriting the data/metadata of successive revisions to preserve revision log
5 # integrity.
5 # integrity.
6
6
7 """erase file content at a given revision
7 """erase file content at a given revision
8
8
9 The censor command instructs Mercurial to erase all content of a file at a given
9 The censor command instructs Mercurial to erase all content of a file at a given
10 revision *without updating the changeset hash.* This allows existing history to
10 revision *without updating the changeset hash.* This allows existing history to
11 remain valid while preventing future clones/pulls from receiving the erased
11 remain valid while preventing future clones/pulls from receiving the erased
12 data.
12 data.
13
13
14 Typical uses for censor are due to security or legal requirements, including::
14 Typical uses for censor are due to security or legal requirements, including::
15
15
16 * Passwords, private keys, cryptographic material
16 * Passwords, private keys, cryptographic material
17 * Licensed data/code/libraries for which the license has expired
17 * Licensed data/code/libraries for which the license has expired
18 * Personally Identifiable Information or other private data
18 * Personally Identifiable Information or other private data
19
19
20 Censored nodes can interrupt mercurial's typical operation whenever the excised
20 Censored nodes can interrupt mercurial's typical operation whenever the excised
21 data needs to be materialized. Some commands, like ``hg cat``/``hg revert``,
21 data needs to be materialized. Some commands, like ``hg cat``/``hg revert``,
22 simply fail when asked to produce censored data. Others, like ``hg verify`` and
22 simply fail when asked to produce censored data. Others, like ``hg verify`` and
23 ``hg update``, must be capable of tolerating censored data to continue to
23 ``hg update``, must be capable of tolerating censored data to continue to
24 function in a meaningful way. Such commands only tolerate censored file
24 function in a meaningful way. Such commands only tolerate censored file
25 revisions if they are allowed by the "censor.policy=ignore" config option.
25 revisions if they are allowed by the "censor.policy=ignore" config option.
26 """
26 """
27
27
28 from __future__ import absolute_import
28 from __future__ import absolute_import
29
29
30 from mercurial.i18n import _
30 from mercurial.i18n import _
31 from mercurial.node import short
31 from mercurial.node import short
32
32
33 from mercurial import (
33 from mercurial import (
34 cmdutil,
34 cmdutil,
35 error,
35 error,
36 filelog,
36 filelog,
37 lock as lockmod,
37 lock as lockmod,
38 revlog,
38 revlog,
39 scmutil,
39 scmutil,
40 util,
40 util,
41 )
41 )
42
42
43 cmdtable = {}
43 cmdtable = {}
44 command = cmdutil.command(cmdtable)
44 command = cmdutil.command(cmdtable)
45 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
47 # be specifying the version(s) of Mercurial they are tested with, or
47 # be specifying the version(s) of Mercurial they are tested with, or
48 # leave the attribute unspecified.
48 # leave the attribute unspecified.
49 testedwith = 'internal'
49 testedwith = 'ships-with-hg-core'
50
50
51 @command('censor',
51 @command('censor',
52 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
52 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
53 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
53 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
54 _('-r REV [-t TEXT] [FILE]'))
54 _('-r REV [-t TEXT] [FILE]'))
55 def censor(ui, repo, path, rev='', tombstone='', **opts):
55 def censor(ui, repo, path, rev='', tombstone='', **opts):
56 wlock = lock = None
56 wlock = lock = None
57 try:
57 try:
58 wlock = repo.wlock()
58 wlock = repo.wlock()
59 lock = repo.lock()
59 lock = repo.lock()
60 return _docensor(ui, repo, path, rev, tombstone, **opts)
60 return _docensor(ui, repo, path, rev, tombstone, **opts)
61 finally:
61 finally:
62 lockmod.release(lock, wlock)
62 lockmod.release(lock, wlock)
63
63
64 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
64 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
65 if not path:
65 if not path:
66 raise error.Abort(_('must specify file path to censor'))
66 raise error.Abort(_('must specify file path to censor'))
67 if not rev:
67 if not rev:
68 raise error.Abort(_('must specify revision to censor'))
68 raise error.Abort(_('must specify revision to censor'))
69
69
70 wctx = repo[None]
70 wctx = repo[None]
71
71
72 m = scmutil.match(wctx, (path,))
72 m = scmutil.match(wctx, (path,))
73 if m.anypats() or len(m.files()) != 1:
73 if m.anypats() or len(m.files()) != 1:
74 raise error.Abort(_('can only specify an explicit filename'))
74 raise error.Abort(_('can only specify an explicit filename'))
75 path = m.files()[0]
75 path = m.files()[0]
76 flog = repo.file(path)
76 flog = repo.file(path)
77 if not len(flog):
77 if not len(flog):
78 raise error.Abort(_('cannot censor file with no history'))
78 raise error.Abort(_('cannot censor file with no history'))
79
79
80 rev = scmutil.revsingle(repo, rev, rev).rev()
80 rev = scmutil.revsingle(repo, rev, rev).rev()
81 try:
81 try:
82 ctx = repo[rev]
82 ctx = repo[rev]
83 except KeyError:
83 except KeyError:
84 raise error.Abort(_('invalid revision identifier %s') % rev)
84 raise error.Abort(_('invalid revision identifier %s') % rev)
85
85
86 try:
86 try:
87 fctx = ctx.filectx(path)
87 fctx = ctx.filectx(path)
88 except error.LookupError:
88 except error.LookupError:
89 raise error.Abort(_('file does not exist at revision %s') % rev)
89 raise error.Abort(_('file does not exist at revision %s') % rev)
90
90
91 fnode = fctx.filenode()
91 fnode = fctx.filenode()
92 headctxs = [repo[c] for c in repo.heads()]
92 headctxs = [repo[c] for c in repo.heads()]
93 heads = [c for c in headctxs if path in c and c.filenode(path) == fnode]
93 heads = [c for c in headctxs if path in c and c.filenode(path) == fnode]
94 if heads:
94 if heads:
95 headlist = ', '.join([short(c.node()) for c in heads])
95 headlist = ', '.join([short(c.node()) for c in heads])
96 raise error.Abort(_('cannot censor file in heads (%s)') % headlist,
96 raise error.Abort(_('cannot censor file in heads (%s)') % headlist,
97 hint=_('clean/delete and commit first'))
97 hint=_('clean/delete and commit first'))
98
98
99 wp = wctx.parents()
99 wp = wctx.parents()
100 if ctx.node() in [p.node() for p in wp]:
100 if ctx.node() in [p.node() for p in wp]:
101 raise error.Abort(_('cannot censor working directory'),
101 raise error.Abort(_('cannot censor working directory'),
102 hint=_('clean/delete/update first'))
102 hint=_('clean/delete/update first'))
103
103
104 flogv = flog.version & 0xFFFF
104 flogv = flog.version & 0xFFFF
105 if flogv != revlog.REVLOGNG:
105 if flogv != revlog.REVLOGNG:
106 raise error.Abort(
106 raise error.Abort(
107 _('censor does not support revlog version %d') % (flogv,))
107 _('censor does not support revlog version %d') % (flogv,))
108
108
109 tombstone = filelog.packmeta({"censored": tombstone}, "")
109 tombstone = filelog.packmeta({"censored": tombstone}, "")
110
110
111 crev = fctx.filerev()
111 crev = fctx.filerev()
112
112
113 if len(tombstone) > flog.rawsize(crev):
113 if len(tombstone) > flog.rawsize(crev):
114 raise error.Abort(_(
114 raise error.Abort(_(
115 'censor tombstone must be no longer than censored data'))
115 'censor tombstone must be no longer than censored data'))
116
116
117 # Using two files instead of one makes it easy to rewrite entry-by-entry
117 # Using two files instead of one makes it easy to rewrite entry-by-entry
118 idxread = repo.svfs(flog.indexfile, 'r')
118 idxread = repo.svfs(flog.indexfile, 'r')
119 idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True)
119 idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True)
120 if flog.version & revlog.REVLOGNGINLINEDATA:
120 if flog.version & revlog.REVLOGNGINLINEDATA:
121 dataread, datawrite = idxread, idxwrite
121 dataread, datawrite = idxread, idxwrite
122 else:
122 else:
123 dataread = repo.svfs(flog.datafile, 'r')
123 dataread = repo.svfs(flog.datafile, 'r')
124 datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True)
124 datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True)
125
125
126 # Copy all revlog data up to the entry to be censored.
126 # Copy all revlog data up to the entry to be censored.
127 rio = revlog.revlogio()
127 rio = revlog.revlogio()
128 offset = flog.start(crev)
128 offset = flog.start(crev)
129
129
130 for chunk in util.filechunkiter(idxread, limit=crev * rio.size):
130 for chunk in util.filechunkiter(idxread, limit=crev * rio.size):
131 idxwrite.write(chunk)
131 idxwrite.write(chunk)
132 for chunk in util.filechunkiter(dataread, limit=offset):
132 for chunk in util.filechunkiter(dataread, limit=offset):
133 datawrite.write(chunk)
133 datawrite.write(chunk)
134
134
135 def rewriteindex(r, newoffs, newdata=None):
135 def rewriteindex(r, newoffs, newdata=None):
136 """Rewrite the index entry with a new data offset and optional new data.
136 """Rewrite the index entry with a new data offset and optional new data.
137
137
138 The newdata argument, if given, is a tuple of three positive integers:
138 The newdata argument, if given, is a tuple of three positive integers:
139 (new compressed, new uncompressed, added flag bits).
139 (new compressed, new uncompressed, added flag bits).
140 """
140 """
141 offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r]
141 offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r]
142 flags = revlog.gettype(offlags)
142 flags = revlog.gettype(offlags)
143 if newdata:
143 if newdata:
144 comp, uncomp, nflags = newdata
144 comp, uncomp, nflags = newdata
145 flags |= nflags
145 flags |= nflags
146 offlags = revlog.offset_type(newoffs, flags)
146 offlags = revlog.offset_type(newoffs, flags)
147 e = (offlags, comp, uncomp, r, link, p1, p2, nodeid)
147 e = (offlags, comp, uncomp, r, link, p1, p2, nodeid)
148 idxwrite.write(rio.packentry(e, None, flog.version, r))
148 idxwrite.write(rio.packentry(e, None, flog.version, r))
149 idxread.seek(rio.size, 1)
149 idxread.seek(rio.size, 1)
150
150
151 def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS):
151 def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS):
152 """Write the given full text to the filelog with the given data offset.
152 """Write the given full text to the filelog with the given data offset.
153
153
154 Returns:
154 Returns:
155 The integer number of data bytes written, for tracking data offsets.
155 The integer number of data bytes written, for tracking data offsets.
156 """
156 """
157 flag, compdata = flog.compress(data)
157 flag, compdata = flog.compress(data)
158 newcomp = len(flag) + len(compdata)
158 newcomp = len(flag) + len(compdata)
159 rewriteindex(r, offs, (newcomp, len(data), nflags))
159 rewriteindex(r, offs, (newcomp, len(data), nflags))
160 datawrite.write(flag)
160 datawrite.write(flag)
161 datawrite.write(compdata)
161 datawrite.write(compdata)
162 dataread.seek(flog.length(r), 1)
162 dataread.seek(flog.length(r), 1)
163 return newcomp
163 return newcomp
164
164
165 # Rewrite censored revlog entry with (padded) tombstone data.
165 # Rewrite censored revlog entry with (padded) tombstone data.
166 pad = ' ' * (flog.rawsize(crev) - len(tombstone))
166 pad = ' ' * (flog.rawsize(crev) - len(tombstone))
167 offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED)
167 offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED)
168
168
169 # Rewrite all following filelog revisions fixing up offsets and deltas.
169 # Rewrite all following filelog revisions fixing up offsets and deltas.
170 for srev in xrange(crev + 1, len(flog)):
170 for srev in xrange(crev + 1, len(flog)):
171 if crev in flog.parentrevs(srev):
171 if crev in flog.parentrevs(srev):
172 # Immediate children of censored node must be re-added as fulltext.
172 # Immediate children of censored node must be re-added as fulltext.
173 try:
173 try:
174 revdata = flog.revision(srev)
174 revdata = flog.revision(srev)
175 except error.CensoredNodeError as e:
175 except error.CensoredNodeError as e:
176 revdata = e.tombstone
176 revdata = e.tombstone
177 dlen = rewrite(srev, offset, revdata)
177 dlen = rewrite(srev, offset, revdata)
178 else:
178 else:
179 # Copy any other revision data verbatim after fixing up the offset.
179 # Copy any other revision data verbatim after fixing up the offset.
180 rewriteindex(srev, offset)
180 rewriteindex(srev, offset)
181 dlen = flog.length(srev)
181 dlen = flog.length(srev)
182 for chunk in util.filechunkiter(dataread, limit=dlen):
182 for chunk in util.filechunkiter(dataread, limit=dlen):
183 datawrite.write(chunk)
183 datawrite.write(chunk)
184 offset += dlen
184 offset += dlen
185
185
186 idxread.close()
186 idxread.close()
187 idxwrite.close()
187 idxwrite.close()
188 if dataread is not idxread:
188 if dataread is not idxread:
189 dataread.close()
189 dataread.close()
190 datawrite.close()
190 datawrite.close()
@@ -1,643 +1,643 b''
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """command server extension for cHg (EXPERIMENTAL)
8 """command server extension for cHg (EXPERIMENTAL)
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'getpager' command
19 'getpager' command
20 checks if pager is enabled and which pager should be executed
20 checks if pager is enabled and which pager should be executed
21
21
22 'setenv' command
22 'setenv' command
23 replace os.environ completely
23 replace os.environ completely
24
24
25 'setumask' command
25 'setumask' command
26 set umask
26 set umask
27
27
28 'validate' command
28 'validate' command
29 reload the config and check if the server is up to date
29 reload the config and check if the server is up to date
30
30
31 Config
31 Config
32 ------
32 ------
33
33
34 ::
34 ::
35
35
36 [chgserver]
36 [chgserver]
37 idletimeout = 3600 # seconds, after which an idle server will exit
37 idletimeout = 3600 # seconds, after which an idle server will exit
38 skiphash = False # whether to skip config or env change checks
38 skiphash = False # whether to skip config or env change checks
39 """
39 """
40
40
41 from __future__ import absolute_import
41 from __future__ import absolute_import
42
42
43 import errno
43 import errno
44 import hashlib
44 import hashlib
45 import inspect
45 import inspect
46 import os
46 import os
47 import re
47 import re
48 import signal
48 import signal
49 import struct
49 import struct
50 import sys
50 import sys
51 import time
51 import time
52
52
53 from mercurial.i18n import _
53 from mercurial.i18n import _
54
54
55 from mercurial import (
55 from mercurial import (
56 cmdutil,
56 cmdutil,
57 commands,
57 commands,
58 commandserver,
58 commandserver,
59 dispatch,
59 dispatch,
60 error,
60 error,
61 extensions,
61 extensions,
62 osutil,
62 osutil,
63 util,
63 util,
64 )
64 )
65
65
66 # Note for extension authors: ONLY specify testedwith = 'internal' for
66 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
67 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
67 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
68 # be specifying the version(s) of Mercurial they are tested with, or
68 # be specifying the version(s) of Mercurial they are tested with, or
69 # leave the attribute unspecified.
69 # leave the attribute unspecified.
70 testedwith = 'internal'
70 testedwith = 'ships-with-hg-core'
71
71
72 _log = commandserver.log
72 _log = commandserver.log
73
73
74 def _hashlist(items):
74 def _hashlist(items):
75 """return sha1 hexdigest for a list"""
75 """return sha1 hexdigest for a list"""
76 return hashlib.sha1(str(items)).hexdigest()
76 return hashlib.sha1(str(items)).hexdigest()
77
77
78 # sensitive config sections affecting confighash
78 # sensitive config sections affecting confighash
79 _configsections = [
79 _configsections = [
80 'alias', # affects global state commands.table
80 'alias', # affects global state commands.table
81 'extdiff', # uisetup will register new commands
81 'extdiff', # uisetup will register new commands
82 'extensions',
82 'extensions',
83 ]
83 ]
84
84
85 # sensitive environment variables affecting confighash
85 # sensitive environment variables affecting confighash
86 _envre = re.compile(r'''\A(?:
86 _envre = re.compile(r'''\A(?:
87 CHGHG
87 CHGHG
88 |HG.*
88 |HG.*
89 |LANG(?:UAGE)?
89 |LANG(?:UAGE)?
90 |LC_.*
90 |LC_.*
91 |LD_.*
91 |LD_.*
92 |PATH
92 |PATH
93 |PYTHON.*
93 |PYTHON.*
94 |TERM(?:INFO)?
94 |TERM(?:INFO)?
95 |TZ
95 |TZ
96 )\Z''', re.X)
96 )\Z''', re.X)
97
97
98 def _confighash(ui):
98 def _confighash(ui):
99 """return a quick hash for detecting config/env changes
99 """return a quick hash for detecting config/env changes
100
100
101 confighash is the hash of sensitive config items and environment variables.
101 confighash is the hash of sensitive config items and environment variables.
102
102
103 for chgserver, it is designed that once confighash changes, the server is
103 for chgserver, it is designed that once confighash changes, the server is
104 not qualified to serve its client and should redirect the client to a new
104 not qualified to serve its client and should redirect the client to a new
105 server. different from mtimehash, confighash change will not mark the
105 server. different from mtimehash, confighash change will not mark the
106 server outdated and exit since the user can have different configs at the
106 server outdated and exit since the user can have different configs at the
107 same time.
107 same time.
108 """
108 """
109 sectionitems = []
109 sectionitems = []
110 for section in _configsections:
110 for section in _configsections:
111 sectionitems.append(ui.configitems(section))
111 sectionitems.append(ui.configitems(section))
112 sectionhash = _hashlist(sectionitems)
112 sectionhash = _hashlist(sectionitems)
113 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
113 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
114 envhash = _hashlist(sorted(envitems))
114 envhash = _hashlist(sorted(envitems))
115 return sectionhash[:6] + envhash[:6]
115 return sectionhash[:6] + envhash[:6]
116
116
117 def _getmtimepaths(ui):
117 def _getmtimepaths(ui):
118 """get a list of paths that should be checked to detect change
118 """get a list of paths that should be checked to detect change
119
119
120 The list will include:
120 The list will include:
121 - extensions (will not cover all files for complex extensions)
121 - extensions (will not cover all files for complex extensions)
122 - mercurial/__version__.py
122 - mercurial/__version__.py
123 - python binary
123 - python binary
124 """
124 """
125 modules = [m for n, m in extensions.extensions(ui)]
125 modules = [m for n, m in extensions.extensions(ui)]
126 try:
126 try:
127 from mercurial import __version__
127 from mercurial import __version__
128 modules.append(__version__)
128 modules.append(__version__)
129 except ImportError:
129 except ImportError:
130 pass
130 pass
131 files = [sys.executable]
131 files = [sys.executable]
132 for m in modules:
132 for m in modules:
133 try:
133 try:
134 files.append(inspect.getabsfile(m))
134 files.append(inspect.getabsfile(m))
135 except TypeError:
135 except TypeError:
136 pass
136 pass
137 return sorted(set(files))
137 return sorted(set(files))
138
138
139 def _mtimehash(paths):
139 def _mtimehash(paths):
140 """return a quick hash for detecting file changes
140 """return a quick hash for detecting file changes
141
141
142 mtimehash calls stat on given paths and calculate a hash based on size and
142 mtimehash calls stat on given paths and calculate a hash based on size and
143 mtime of each file. mtimehash does not read file content because reading is
143 mtime of each file. mtimehash does not read file content because reading is
144 expensive. therefore it's not 100% reliable for detecting content changes.
144 expensive. therefore it's not 100% reliable for detecting content changes.
145 it's possible to return different hashes for same file contents.
145 it's possible to return different hashes for same file contents.
146 it's also possible to return a same hash for different file contents for
146 it's also possible to return a same hash for different file contents for
147 some carefully crafted situation.
147 some carefully crafted situation.
148
148
149 for chgserver, it is designed that once mtimehash changes, the server is
149 for chgserver, it is designed that once mtimehash changes, the server is
150 considered outdated immediately and should no longer provide service.
150 considered outdated immediately and should no longer provide service.
151
151
152 mtimehash is not included in confighash because we only know the paths of
152 mtimehash is not included in confighash because we only know the paths of
153 extensions after importing them (there is imp.find_module but that faces
153 extensions after importing them (there is imp.find_module but that faces
154 race conditions). We need to calculate confighash without importing.
154 race conditions). We need to calculate confighash without importing.
155 """
155 """
156 def trystat(path):
156 def trystat(path):
157 try:
157 try:
158 st = os.stat(path)
158 st = os.stat(path)
159 return (st.st_mtime, st.st_size)
159 return (st.st_mtime, st.st_size)
160 except OSError:
160 except OSError:
161 # could be ENOENT, EPERM etc. not fatal in any case
161 # could be ENOENT, EPERM etc. not fatal in any case
162 pass
162 pass
163 return _hashlist(map(trystat, paths))[:12]
163 return _hashlist(map(trystat, paths))[:12]
164
164
165 class hashstate(object):
165 class hashstate(object):
166 """a structure storing confighash, mtimehash, paths used for mtimehash"""
166 """a structure storing confighash, mtimehash, paths used for mtimehash"""
167 def __init__(self, confighash, mtimehash, mtimepaths):
167 def __init__(self, confighash, mtimehash, mtimepaths):
168 self.confighash = confighash
168 self.confighash = confighash
169 self.mtimehash = mtimehash
169 self.mtimehash = mtimehash
170 self.mtimepaths = mtimepaths
170 self.mtimepaths = mtimepaths
171
171
172 @staticmethod
172 @staticmethod
173 def fromui(ui, mtimepaths=None):
173 def fromui(ui, mtimepaths=None):
174 if mtimepaths is None:
174 if mtimepaths is None:
175 mtimepaths = _getmtimepaths(ui)
175 mtimepaths = _getmtimepaths(ui)
176 confighash = _confighash(ui)
176 confighash = _confighash(ui)
177 mtimehash = _mtimehash(mtimepaths)
177 mtimehash = _mtimehash(mtimepaths)
178 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
178 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
179 return hashstate(confighash, mtimehash, mtimepaths)
179 return hashstate(confighash, mtimehash, mtimepaths)
180
180
181 # copied from hgext/pager.py:uisetup()
181 # copied from hgext/pager.py:uisetup()
182 def _setuppagercmd(ui, options, cmd):
182 def _setuppagercmd(ui, options, cmd):
183 if not ui.formatted():
183 if not ui.formatted():
184 return
184 return
185
185
186 p = ui.config("pager", "pager", os.environ.get("PAGER"))
186 p = ui.config("pager", "pager", os.environ.get("PAGER"))
187 usepager = False
187 usepager = False
188 always = util.parsebool(options['pager'])
188 always = util.parsebool(options['pager'])
189 auto = options['pager'] == 'auto'
189 auto = options['pager'] == 'auto'
190
190
191 if not p:
191 if not p:
192 pass
192 pass
193 elif always:
193 elif always:
194 usepager = True
194 usepager = True
195 elif not auto:
195 elif not auto:
196 usepager = False
196 usepager = False
197 else:
197 else:
198 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
198 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
199 attend = ui.configlist('pager', 'attend', attended)
199 attend = ui.configlist('pager', 'attend', attended)
200 ignore = ui.configlist('pager', 'ignore')
200 ignore = ui.configlist('pager', 'ignore')
201 cmds, _ = cmdutil.findcmd(cmd, commands.table)
201 cmds, _ = cmdutil.findcmd(cmd, commands.table)
202
202
203 for cmd in cmds:
203 for cmd in cmds:
204 var = 'attend-%s' % cmd
204 var = 'attend-%s' % cmd
205 if ui.config('pager', var):
205 if ui.config('pager', var):
206 usepager = ui.configbool('pager', var)
206 usepager = ui.configbool('pager', var)
207 break
207 break
208 if (cmd in attend or
208 if (cmd in attend or
209 (cmd not in ignore and not attend)):
209 (cmd not in ignore and not attend)):
210 usepager = True
210 usepager = True
211 break
211 break
212
212
213 if usepager:
213 if usepager:
214 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
214 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
215 ui.setconfig('ui', 'interactive', False, 'pager')
215 ui.setconfig('ui', 'interactive', False, 'pager')
216 return p
216 return p
217
217
218 def _newchgui(srcui, csystem):
218 def _newchgui(srcui, csystem):
219 class chgui(srcui.__class__):
219 class chgui(srcui.__class__):
220 def __init__(self, src=None):
220 def __init__(self, src=None):
221 super(chgui, self).__init__(src)
221 super(chgui, self).__init__(src)
222 if src:
222 if src:
223 self._csystem = getattr(src, '_csystem', csystem)
223 self._csystem = getattr(src, '_csystem', csystem)
224 else:
224 else:
225 self._csystem = csystem
225 self._csystem = csystem
226
226
227 def system(self, cmd, environ=None, cwd=None, onerr=None,
227 def system(self, cmd, environ=None, cwd=None, onerr=None,
228 errprefix=None):
228 errprefix=None):
229 # fallback to the original system method if the output needs to be
229 # fallback to the original system method if the output needs to be
230 # captured (to self._buffers), or the output stream is not stdout
230 # captured (to self._buffers), or the output stream is not stdout
231 # (e.g. stderr, cStringIO), because the chg client is not aware of
231 # (e.g. stderr, cStringIO), because the chg client is not aware of
232 # these situations and will behave differently (write to stdout).
232 # these situations and will behave differently (write to stdout).
233 if (any(s[1] for s in self._bufferstates)
233 if (any(s[1] for s in self._bufferstates)
234 or not util.safehasattr(self.fout, 'fileno')
234 or not util.safehasattr(self.fout, 'fileno')
235 or self.fout.fileno() != sys.stdout.fileno()):
235 or self.fout.fileno() != sys.stdout.fileno()):
236 return super(chgui, self).system(cmd, environ, cwd, onerr,
236 return super(chgui, self).system(cmd, environ, cwd, onerr,
237 errprefix)
237 errprefix)
238 # copied from mercurial/util.py:system()
238 # copied from mercurial/util.py:system()
239 self.flush()
239 self.flush()
240 def py2shell(val):
240 def py2shell(val):
241 if val is None or val is False:
241 if val is None or val is False:
242 return '0'
242 return '0'
243 if val is True:
243 if val is True:
244 return '1'
244 return '1'
245 return str(val)
245 return str(val)
246 env = os.environ.copy()
246 env = os.environ.copy()
247 if environ:
247 if environ:
248 env.update((k, py2shell(v)) for k, v in environ.iteritems())
248 env.update((k, py2shell(v)) for k, v in environ.iteritems())
249 env['HG'] = util.hgexecutable()
249 env['HG'] = util.hgexecutable()
250 rc = self._csystem(cmd, env, cwd)
250 rc = self._csystem(cmd, env, cwd)
251 if rc and onerr:
251 if rc and onerr:
252 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
252 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
253 util.explainexit(rc)[0])
253 util.explainexit(rc)[0])
254 if errprefix:
254 if errprefix:
255 errmsg = '%s: %s' % (errprefix, errmsg)
255 errmsg = '%s: %s' % (errprefix, errmsg)
256 raise onerr(errmsg)
256 raise onerr(errmsg)
257 return rc
257 return rc
258
258
259 return chgui(srcui)
259 return chgui(srcui)
260
260
261 def _loadnewui(srcui, args):
261 def _loadnewui(srcui, args):
262 newui = srcui.__class__()
262 newui = srcui.__class__()
263 for a in ['fin', 'fout', 'ferr', 'environ']:
263 for a in ['fin', 'fout', 'ferr', 'environ']:
264 setattr(newui, a, getattr(srcui, a))
264 setattr(newui, a, getattr(srcui, a))
265 if util.safehasattr(srcui, '_csystem'):
265 if util.safehasattr(srcui, '_csystem'):
266 newui._csystem = srcui._csystem
266 newui._csystem = srcui._csystem
267
267
268 # internal config: extensions.chgserver
268 # internal config: extensions.chgserver
269 newui.setconfig('extensions', 'chgserver',
269 newui.setconfig('extensions', 'chgserver',
270 srcui.config('extensions', 'chgserver'), '--config')
270 srcui.config('extensions', 'chgserver'), '--config')
271
271
272 # command line args
272 # command line args
273 args = args[:]
273 args = args[:]
274 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
274 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
275
275
276 # stolen from tortoisehg.util.copydynamicconfig()
276 # stolen from tortoisehg.util.copydynamicconfig()
277 for section, name, value in srcui.walkconfig():
277 for section, name, value in srcui.walkconfig():
278 source = srcui.configsource(section, name)
278 source = srcui.configsource(section, name)
279 if ':' in source or source == '--config':
279 if ':' in source or source == '--config':
280 # path:line or command line
280 # path:line or command line
281 continue
281 continue
282 if source == 'none':
282 if source == 'none':
283 # ui.configsource returns 'none' by default
283 # ui.configsource returns 'none' by default
284 source = ''
284 source = ''
285 newui.setconfig(section, name, value, source)
285 newui.setconfig(section, name, value, source)
286
286
287 # load wd and repo config, copied from dispatch.py
287 # load wd and repo config, copied from dispatch.py
288 cwds = dispatch._earlygetopt(['--cwd'], args)
288 cwds = dispatch._earlygetopt(['--cwd'], args)
289 cwd = cwds and os.path.realpath(cwds[-1]) or None
289 cwd = cwds and os.path.realpath(cwds[-1]) or None
290 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
290 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
291 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
291 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
292
292
293 return (newui, newlui)
293 return (newui, newlui)
294
294
295 class channeledsystem(object):
295 class channeledsystem(object):
296 """Propagate ui.system() request in the following format:
296 """Propagate ui.system() request in the following format:
297
297
298 payload length (unsigned int),
298 payload length (unsigned int),
299 cmd, '\0',
299 cmd, '\0',
300 cwd, '\0',
300 cwd, '\0',
301 envkey, '=', val, '\0',
301 envkey, '=', val, '\0',
302 ...
302 ...
303 envkey, '=', val
303 envkey, '=', val
304
304
305 and waits:
305 and waits:
306
306
307 exitcode length (unsigned int),
307 exitcode length (unsigned int),
308 exitcode (int)
308 exitcode (int)
309 """
309 """
310 def __init__(self, in_, out, channel):
310 def __init__(self, in_, out, channel):
311 self.in_ = in_
311 self.in_ = in_
312 self.out = out
312 self.out = out
313 self.channel = channel
313 self.channel = channel
314
314
315 def __call__(self, cmd, environ, cwd):
315 def __call__(self, cmd, environ, cwd):
316 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
316 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
317 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
317 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
318 data = '\0'.join(args)
318 data = '\0'.join(args)
319 self.out.write(struct.pack('>cI', self.channel, len(data)))
319 self.out.write(struct.pack('>cI', self.channel, len(data)))
320 self.out.write(data)
320 self.out.write(data)
321 self.out.flush()
321 self.out.flush()
322
322
323 length = self.in_.read(4)
323 length = self.in_.read(4)
324 length, = struct.unpack('>I', length)
324 length, = struct.unpack('>I', length)
325 if length != 4:
325 if length != 4:
326 raise error.Abort(_('invalid response'))
326 raise error.Abort(_('invalid response'))
327 rc, = struct.unpack('>i', self.in_.read(4))
327 rc, = struct.unpack('>i', self.in_.read(4))
328 return rc
328 return rc
329
329
330 _iochannels = [
330 _iochannels = [
331 # server.ch, ui.fp, mode
331 # server.ch, ui.fp, mode
332 ('cin', 'fin', 'rb'),
332 ('cin', 'fin', 'rb'),
333 ('cout', 'fout', 'wb'),
333 ('cout', 'fout', 'wb'),
334 ('cerr', 'ferr', 'wb'),
334 ('cerr', 'ferr', 'wb'),
335 ]
335 ]
336
336
337 class chgcmdserver(commandserver.server):
337 class chgcmdserver(commandserver.server):
338 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
338 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
339 super(chgcmdserver, self).__init__(
339 super(chgcmdserver, self).__init__(
340 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
340 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
341 self.clientsock = sock
341 self.clientsock = sock
342 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
342 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
343 self.hashstate = hashstate
343 self.hashstate = hashstate
344 self.baseaddress = baseaddress
344 self.baseaddress = baseaddress
345 if hashstate is not None:
345 if hashstate is not None:
346 self.capabilities = self.capabilities.copy()
346 self.capabilities = self.capabilities.copy()
347 self.capabilities['validate'] = chgcmdserver.validate
347 self.capabilities['validate'] = chgcmdserver.validate
348
348
349 def cleanup(self):
349 def cleanup(self):
350 super(chgcmdserver, self).cleanup()
350 super(chgcmdserver, self).cleanup()
351 # dispatch._runcatch() does not flush outputs if exception is not
351 # dispatch._runcatch() does not flush outputs if exception is not
352 # handled by dispatch._dispatch()
352 # handled by dispatch._dispatch()
353 self.ui.flush()
353 self.ui.flush()
354 self._restoreio()
354 self._restoreio()
355
355
356 def attachio(self):
356 def attachio(self):
357 """Attach to client's stdio passed via unix domain socket; all
357 """Attach to client's stdio passed via unix domain socket; all
358 channels except cresult will no longer be used
358 channels except cresult will no longer be used
359 """
359 """
360 # tell client to sendmsg() with 1-byte payload, which makes it
360 # tell client to sendmsg() with 1-byte payload, which makes it
361 # distinctive from "attachio\n" command consumed by client.read()
361 # distinctive from "attachio\n" command consumed by client.read()
362 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
362 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
363 clientfds = osutil.recvfds(self.clientsock.fileno())
363 clientfds = osutil.recvfds(self.clientsock.fileno())
364 _log('received fds: %r\n' % clientfds)
364 _log('received fds: %r\n' % clientfds)
365
365
366 ui = self.ui
366 ui = self.ui
367 ui.flush()
367 ui.flush()
368 first = self._saveio()
368 first = self._saveio()
369 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
369 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
370 assert fd > 0
370 assert fd > 0
371 fp = getattr(ui, fn)
371 fp = getattr(ui, fn)
372 os.dup2(fd, fp.fileno())
372 os.dup2(fd, fp.fileno())
373 os.close(fd)
373 os.close(fd)
374 if not first:
374 if not first:
375 continue
375 continue
376 # reset buffering mode when client is first attached. as we want
376 # reset buffering mode when client is first attached. as we want
377 # to see output immediately on pager, the mode stays unchanged
377 # to see output immediately on pager, the mode stays unchanged
378 # when client re-attached. ferr is unchanged because it should
378 # when client re-attached. ferr is unchanged because it should
379 # be unbuffered no matter if it is a tty or not.
379 # be unbuffered no matter if it is a tty or not.
380 if fn == 'ferr':
380 if fn == 'ferr':
381 newfp = fp
381 newfp = fp
382 else:
382 else:
383 # make it line buffered explicitly because the default is
383 # make it line buffered explicitly because the default is
384 # decided on first write(), where fout could be a pager.
384 # decided on first write(), where fout could be a pager.
385 if fp.isatty():
385 if fp.isatty():
386 bufsize = 1 # line buffered
386 bufsize = 1 # line buffered
387 else:
387 else:
388 bufsize = -1 # system default
388 bufsize = -1 # system default
389 newfp = os.fdopen(fp.fileno(), mode, bufsize)
389 newfp = os.fdopen(fp.fileno(), mode, bufsize)
390 setattr(ui, fn, newfp)
390 setattr(ui, fn, newfp)
391 setattr(self, cn, newfp)
391 setattr(self, cn, newfp)
392
392
393 self.cresult.write(struct.pack('>i', len(clientfds)))
393 self.cresult.write(struct.pack('>i', len(clientfds)))
394
394
395 def _saveio(self):
395 def _saveio(self):
396 if self._oldios:
396 if self._oldios:
397 return False
397 return False
398 ui = self.ui
398 ui = self.ui
399 for cn, fn, _mode in _iochannels:
399 for cn, fn, _mode in _iochannels:
400 ch = getattr(self, cn)
400 ch = getattr(self, cn)
401 fp = getattr(ui, fn)
401 fp = getattr(ui, fn)
402 fd = os.dup(fp.fileno())
402 fd = os.dup(fp.fileno())
403 self._oldios.append((ch, fp, fd))
403 self._oldios.append((ch, fp, fd))
404 return True
404 return True
405
405
406 def _restoreio(self):
406 def _restoreio(self):
407 ui = self.ui
407 ui = self.ui
408 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
408 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
409 newfp = getattr(ui, fn)
409 newfp = getattr(ui, fn)
410 # close newfp while it's associated with client; otherwise it
410 # close newfp while it's associated with client; otherwise it
411 # would be closed when newfp is deleted
411 # would be closed when newfp is deleted
412 if newfp is not fp:
412 if newfp is not fp:
413 newfp.close()
413 newfp.close()
414 # restore original fd: fp is open again
414 # restore original fd: fp is open again
415 os.dup2(fd, fp.fileno())
415 os.dup2(fd, fp.fileno())
416 os.close(fd)
416 os.close(fd)
417 setattr(self, cn, ch)
417 setattr(self, cn, ch)
418 setattr(ui, fn, fp)
418 setattr(ui, fn, fp)
419 del self._oldios[:]
419 del self._oldios[:]
420
420
421 def validate(self):
421 def validate(self):
422 """Reload the config and check if the server is up to date
422 """Reload the config and check if the server is up to date
423
423
424 Read a list of '\0' separated arguments.
424 Read a list of '\0' separated arguments.
425 Write a non-empty list of '\0' separated instruction strings or '\0'
425 Write a non-empty list of '\0' separated instruction strings or '\0'
426 if the list is empty.
426 if the list is empty.
427 An instruction string could be either:
427 An instruction string could be either:
428 - "unlink $path", the client should unlink the path to stop the
428 - "unlink $path", the client should unlink the path to stop the
429 outdated server.
429 outdated server.
430 - "redirect $path", the client should attempt to connect to $path
430 - "redirect $path", the client should attempt to connect to $path
431 first. If it does not work, start a new server. It implies
431 first. If it does not work, start a new server. It implies
432 "reconnect".
432 "reconnect".
433 - "exit $n", the client should exit directly with code n.
433 - "exit $n", the client should exit directly with code n.
434 This may happen if we cannot parse the config.
434 This may happen if we cannot parse the config.
435 - "reconnect", the client should close the connection and
435 - "reconnect", the client should close the connection and
436 reconnect.
436 reconnect.
437 If neither "reconnect" nor "redirect" is included in the instruction
437 If neither "reconnect" nor "redirect" is included in the instruction
438 list, the client can continue with this server after completing all
438 list, the client can continue with this server after completing all
439 the instructions.
439 the instructions.
440 """
440 """
441 args = self._readlist()
441 args = self._readlist()
442 try:
442 try:
443 self.ui, lui = _loadnewui(self.ui, args)
443 self.ui, lui = _loadnewui(self.ui, args)
444 except error.ParseError as inst:
444 except error.ParseError as inst:
445 dispatch._formatparse(self.ui.warn, inst)
445 dispatch._formatparse(self.ui.warn, inst)
446 self.ui.flush()
446 self.ui.flush()
447 self.cresult.write('exit 255')
447 self.cresult.write('exit 255')
448 return
448 return
449 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
449 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
450 insts = []
450 insts = []
451 if newhash.mtimehash != self.hashstate.mtimehash:
451 if newhash.mtimehash != self.hashstate.mtimehash:
452 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
452 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
453 insts.append('unlink %s' % addr)
453 insts.append('unlink %s' % addr)
454 # mtimehash is empty if one or more extensions fail to load.
454 # mtimehash is empty if one or more extensions fail to load.
455 # to be compatible with hg, still serve the client this time.
455 # to be compatible with hg, still serve the client this time.
456 if self.hashstate.mtimehash:
456 if self.hashstate.mtimehash:
457 insts.append('reconnect')
457 insts.append('reconnect')
458 if newhash.confighash != self.hashstate.confighash:
458 if newhash.confighash != self.hashstate.confighash:
459 addr = _hashaddress(self.baseaddress, newhash.confighash)
459 addr = _hashaddress(self.baseaddress, newhash.confighash)
460 insts.append('redirect %s' % addr)
460 insts.append('redirect %s' % addr)
461 _log('validate: %s\n' % insts)
461 _log('validate: %s\n' % insts)
462 self.cresult.write('\0'.join(insts) or '\0')
462 self.cresult.write('\0'.join(insts) or '\0')
463
463
464 def chdir(self):
464 def chdir(self):
465 """Change current directory
465 """Change current directory
466
466
467 Note that the behavior of --cwd option is bit different from this.
467 Note that the behavior of --cwd option is bit different from this.
468 It does not affect --config parameter.
468 It does not affect --config parameter.
469 """
469 """
470 path = self._readstr()
470 path = self._readstr()
471 if not path:
471 if not path:
472 return
472 return
473 _log('chdir to %r\n' % path)
473 _log('chdir to %r\n' % path)
474 os.chdir(path)
474 os.chdir(path)
475
475
476 def setumask(self):
476 def setumask(self):
477 """Change umask"""
477 """Change umask"""
478 mask = struct.unpack('>I', self._read(4))[0]
478 mask = struct.unpack('>I', self._read(4))[0]
479 _log('setumask %r\n' % mask)
479 _log('setumask %r\n' % mask)
480 os.umask(mask)
480 os.umask(mask)
481
481
482 def getpager(self):
482 def getpager(self):
483 """Read cmdargs and write pager command to r-channel if enabled
483 """Read cmdargs and write pager command to r-channel if enabled
484
484
485 If pager isn't enabled, this writes '\0' because channeledoutput
485 If pager isn't enabled, this writes '\0' because channeledoutput
486 does not allow to write empty data.
486 does not allow to write empty data.
487 """
487 """
488 args = self._readlist()
488 args = self._readlist()
489 try:
489 try:
490 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
490 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
491 args)
491 args)
492 except (error.Abort, error.AmbiguousCommand, error.CommandError,
492 except (error.Abort, error.AmbiguousCommand, error.CommandError,
493 error.UnknownCommand):
493 error.UnknownCommand):
494 cmd = None
494 cmd = None
495 options = {}
495 options = {}
496 if not cmd or 'pager' not in options:
496 if not cmd or 'pager' not in options:
497 self.cresult.write('\0')
497 self.cresult.write('\0')
498 return
498 return
499
499
500 pagercmd = _setuppagercmd(self.ui, options, cmd)
500 pagercmd = _setuppagercmd(self.ui, options, cmd)
501 if pagercmd:
501 if pagercmd:
502 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
502 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
503 # we can exit if the pipe to the pager is closed
503 # we can exit if the pipe to the pager is closed
504 if util.safehasattr(signal, 'SIGPIPE') and \
504 if util.safehasattr(signal, 'SIGPIPE') and \
505 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
505 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
506 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
506 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
507 self.cresult.write(pagercmd)
507 self.cresult.write(pagercmd)
508 else:
508 else:
509 self.cresult.write('\0')
509 self.cresult.write('\0')
510
510
511 def setenv(self):
511 def setenv(self):
512 """Clear and update os.environ
512 """Clear and update os.environ
513
513
514 Note that not all variables can make an effect on the running process.
514 Note that not all variables can make an effect on the running process.
515 """
515 """
516 l = self._readlist()
516 l = self._readlist()
517 try:
517 try:
518 newenv = dict(s.split('=', 1) for s in l)
518 newenv = dict(s.split('=', 1) for s in l)
519 except ValueError:
519 except ValueError:
520 raise ValueError('unexpected value in setenv request')
520 raise ValueError('unexpected value in setenv request')
521 _log('setenv: %r\n' % sorted(newenv.keys()))
521 _log('setenv: %r\n' % sorted(newenv.keys()))
522 os.environ.clear()
522 os.environ.clear()
523 os.environ.update(newenv)
523 os.environ.update(newenv)
524
524
525 capabilities = commandserver.server.capabilities.copy()
525 capabilities = commandserver.server.capabilities.copy()
526 capabilities.update({'attachio': attachio,
526 capabilities.update({'attachio': attachio,
527 'chdir': chdir,
527 'chdir': chdir,
528 'getpager': getpager,
528 'getpager': getpager,
529 'setenv': setenv,
529 'setenv': setenv,
530 'setumask': setumask})
530 'setumask': setumask})
531
531
532 def _tempaddress(address):
532 def _tempaddress(address):
533 return '%s.%d.tmp' % (address, os.getpid())
533 return '%s.%d.tmp' % (address, os.getpid())
534
534
535 def _hashaddress(address, hashstr):
535 def _hashaddress(address, hashstr):
536 return '%s-%s' % (address, hashstr)
536 return '%s-%s' % (address, hashstr)
537
537
538 class chgunixservicehandler(object):
538 class chgunixservicehandler(object):
539 """Set of operations for chg services"""
539 """Set of operations for chg services"""
540
540
541 pollinterval = 1 # [sec]
541 pollinterval = 1 # [sec]
542
542
543 def __init__(self, ui):
543 def __init__(self, ui):
544 self.ui = ui
544 self.ui = ui
545 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
545 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
546 self._lastactive = time.time()
546 self._lastactive = time.time()
547
547
548 def bindsocket(self, sock, address):
548 def bindsocket(self, sock, address):
549 self._inithashstate(address)
549 self._inithashstate(address)
550 self._checkextensions()
550 self._checkextensions()
551 self._bind(sock)
551 self._bind(sock)
552 self._createsymlink()
552 self._createsymlink()
553
553
554 def _inithashstate(self, address):
554 def _inithashstate(self, address):
555 self._baseaddress = address
555 self._baseaddress = address
556 if self.ui.configbool('chgserver', 'skiphash', False):
556 if self.ui.configbool('chgserver', 'skiphash', False):
557 self._hashstate = None
557 self._hashstate = None
558 self._realaddress = address
558 self._realaddress = address
559 return
559 return
560 self._hashstate = hashstate.fromui(self.ui)
560 self._hashstate = hashstate.fromui(self.ui)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562
562
563 def _checkextensions(self):
563 def _checkextensions(self):
564 if not self._hashstate:
564 if not self._hashstate:
565 return
565 return
566 if extensions.notloaded():
566 if extensions.notloaded():
567 # one or more extensions failed to load. mtimehash becomes
567 # one or more extensions failed to load. mtimehash becomes
568 # meaningless because we do not know the paths of those extensions.
568 # meaningless because we do not know the paths of those extensions.
569 # set mtimehash to an illegal hash value to invalidate the server.
569 # set mtimehash to an illegal hash value to invalidate the server.
570 self._hashstate.mtimehash = ''
570 self._hashstate.mtimehash = ''
571
571
572 def _bind(self, sock):
572 def _bind(self, sock):
573 # use a unique temp address so we can stat the file and do ownership
573 # use a unique temp address so we can stat the file and do ownership
574 # check later
574 # check later
575 tempaddress = _tempaddress(self._realaddress)
575 tempaddress = _tempaddress(self._realaddress)
576 util.bindunixsocket(sock, tempaddress)
576 util.bindunixsocket(sock, tempaddress)
577 self._socketstat = os.stat(tempaddress)
577 self._socketstat = os.stat(tempaddress)
578 # rename will replace the old socket file if exists atomically. the
578 # rename will replace the old socket file if exists atomically. the
579 # old server will detect ownership change and exit.
579 # old server will detect ownership change and exit.
580 util.rename(tempaddress, self._realaddress)
580 util.rename(tempaddress, self._realaddress)
581
581
582 def _createsymlink(self):
582 def _createsymlink(self):
583 if self._baseaddress == self._realaddress:
583 if self._baseaddress == self._realaddress:
584 return
584 return
585 tempaddress = _tempaddress(self._baseaddress)
585 tempaddress = _tempaddress(self._baseaddress)
586 os.symlink(os.path.basename(self._realaddress), tempaddress)
586 os.symlink(os.path.basename(self._realaddress), tempaddress)
587 util.rename(tempaddress, self._baseaddress)
587 util.rename(tempaddress, self._baseaddress)
588
588
589 def _issocketowner(self):
589 def _issocketowner(self):
590 try:
590 try:
591 stat = os.stat(self._realaddress)
591 stat = os.stat(self._realaddress)
592 return (stat.st_ino == self._socketstat.st_ino and
592 return (stat.st_ino == self._socketstat.st_ino and
593 stat.st_mtime == self._socketstat.st_mtime)
593 stat.st_mtime == self._socketstat.st_mtime)
594 except OSError:
594 except OSError:
595 return False
595 return False
596
596
597 def unlinksocket(self, address):
597 def unlinksocket(self, address):
598 if not self._issocketowner():
598 if not self._issocketowner():
599 return
599 return
600 # it is possible to have a race condition here that we may
600 # it is possible to have a race condition here that we may
601 # remove another server's socket file. but that's okay
601 # remove another server's socket file. but that's okay
602 # since that server will detect and exit automatically and
602 # since that server will detect and exit automatically and
603 # the client will start a new server on demand.
603 # the client will start a new server on demand.
604 try:
604 try:
605 os.unlink(self._realaddress)
605 os.unlink(self._realaddress)
606 except OSError as exc:
606 except OSError as exc:
607 if exc.errno != errno.ENOENT:
607 if exc.errno != errno.ENOENT:
608 raise
608 raise
609
609
610 def printbanner(self, address):
610 def printbanner(self, address):
611 # no "listening at" message should be printed to simulate hg behavior
611 # no "listening at" message should be printed to simulate hg behavior
612 pass
612 pass
613
613
614 def shouldexit(self):
614 def shouldexit(self):
615 if not self._issocketowner():
615 if not self._issocketowner():
616 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
616 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
617 return True
617 return True
618 if time.time() - self._lastactive > self._idletimeout:
618 if time.time() - self._lastactive > self._idletimeout:
619 self.ui.debug('being idle too long. exiting.\n')
619 self.ui.debug('being idle too long. exiting.\n')
620 return True
620 return True
621 return False
621 return False
622
622
623 def newconnection(self):
623 def newconnection(self):
624 self._lastactive = time.time()
624 self._lastactive = time.time()
625
625
626 def createcmdserver(self, repo, conn, fin, fout):
626 def createcmdserver(self, repo, conn, fin, fout):
627 return chgcmdserver(self.ui, repo, fin, fout, conn,
627 return chgcmdserver(self.ui, repo, fin, fout, conn,
628 self._hashstate, self._baseaddress)
628 self._hashstate, self._baseaddress)
629
629
630 def chgunixservice(ui, repo, opts):
630 def chgunixservice(ui, repo, opts):
631 if repo:
631 if repo:
632 # one chgserver can serve multiple repos. drop repo infomation
632 # one chgserver can serve multiple repos. drop repo infomation
633 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
633 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
634 h = chgunixservicehandler(ui)
634 h = chgunixservicehandler(ui)
635 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
635 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
636
636
637 def uisetup(ui):
637 def uisetup(ui):
638 commandserver._servicemap['chgunix'] = chgunixservice
638 commandserver._servicemap['chgunix'] = chgunixservice
639
639
640 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
640 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
641 # start another chg. drop it to avoid possible side effects.
641 # start another chg. drop it to avoid possible side effects.
642 if 'CHGINTERNALMARK' in os.environ:
642 if 'CHGINTERNALMARK' in os.environ:
643 del os.environ['CHGINTERNALMARK']
643 del os.environ['CHGINTERNALMARK']
@@ -1,69 +1,69 b''
1 # Mercurial extension to provide the 'hg children' command
1 # Mercurial extension to provide the 'hg children' command
2 #
2 #
3 # Copyright 2007 by Intevation GmbH <intevation@intevation.de>
3 # Copyright 2007 by Intevation GmbH <intevation@intevation.de>
4 #
4 #
5 # Author(s):
5 # Author(s):
6 # Thomas Arendsen Hein <thomas@intevation.de>
6 # Thomas Arendsen Hein <thomas@intevation.de>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 '''command to display child changesets (DEPRECATED)
11 '''command to display child changesets (DEPRECATED)
12
12
13 This extension is deprecated. You should use :hg:`log -r
13 This extension is deprecated. You should use :hg:`log -r
14 "children(REV)"` instead.
14 "children(REV)"` instead.
15 '''
15 '''
16
16
17 from __future__ import absolute_import
17 from __future__ import absolute_import
18
18
19 from mercurial.i18n import _
19 from mercurial.i18n import _
20 from mercurial import (
20 from mercurial import (
21 cmdutil,
21 cmdutil,
22 commands,
22 commands,
23 )
23 )
24
24
25 templateopts = commands.templateopts
25 templateopts = commands.templateopts
26
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29 # Note for extension authors: ONLY specify testedwith = 'internal' for
29 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 # be specifying the version(s) of Mercurial they are tested with, or
31 # be specifying the version(s) of Mercurial they are tested with, or
32 # leave the attribute unspecified.
32 # leave the attribute unspecified.
33 testedwith = 'internal'
33 testedwith = 'ships-with-hg-core'
34
34
35 @command('children',
35 @command('children',
36 [('r', 'rev', '',
36 [('r', 'rev', '',
37 _('show children of the specified revision'), _('REV')),
37 _('show children of the specified revision'), _('REV')),
38 ] + templateopts,
38 ] + templateopts,
39 _('hg children [-r REV] [FILE]'),
39 _('hg children [-r REV] [FILE]'),
40 inferrepo=True)
40 inferrepo=True)
41 def children(ui, repo, file_=None, **opts):
41 def children(ui, repo, file_=None, **opts):
42 """show the children of the given or working directory revision
42 """show the children of the given or working directory revision
43
43
44 Print the children of the working directory's revisions. If a
44 Print the children of the working directory's revisions. If a
45 revision is given via -r/--rev, the children of that revision will
45 revision is given via -r/--rev, the children of that revision will
46 be printed. If a file argument is given, revision in which the
46 be printed. If a file argument is given, revision in which the
47 file was last changed (after the working directory revision or the
47 file was last changed (after the working directory revision or the
48 argument to --rev if given) is printed.
48 argument to --rev if given) is printed.
49
49
50 Please use :hg:`log` instead::
50 Please use :hg:`log` instead::
51
51
52 hg children => hg log -r "children()"
52 hg children => hg log -r "children()"
53 hg children -r REV => hg log -r "children(REV)"
53 hg children -r REV => hg log -r "children(REV)"
54
54
55 See :hg:`help log` and :hg:`help revsets.children`.
55 See :hg:`help log` and :hg:`help revsets.children`.
56
56
57 """
57 """
58 rev = opts.get('rev')
58 rev = opts.get('rev')
59 if file_:
59 if file_:
60 fctx = repo.filectx(file_, changeid=rev)
60 fctx = repo.filectx(file_, changeid=rev)
61 childctxs = [fcctx.changectx() for fcctx in fctx.children()]
61 childctxs = [fcctx.changectx() for fcctx in fctx.children()]
62 else:
62 else:
63 ctx = repo[rev]
63 ctx = repo[rev]
64 childctxs = ctx.children()
64 childctxs = ctx.children()
65
65
66 displayer = cmdutil.show_changeset(ui, repo, opts)
66 displayer = cmdutil.show_changeset(ui, repo, opts)
67 for cctx in childctxs:
67 for cctx in childctxs:
68 displayer.show(cctx)
68 displayer.show(cctx)
69 displayer.close()
69 displayer.close()
@@ -1,211 +1,211 b''
1 # churn.py - create a graph of revisions count grouped by template
1 # churn.py - create a graph of revisions count grouped by template
2 #
2 #
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''command to display statistics about repository history'''
9 '''command to display statistics about repository history'''
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import datetime
13 import datetime
14 import os
14 import os
15 import time
15 import time
16
16
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial import (
18 from mercurial import (
19 cmdutil,
19 cmdutil,
20 commands,
20 commands,
21 encoding,
21 encoding,
22 patch,
22 patch,
23 scmutil,
23 scmutil,
24 util,
24 util,
25 )
25 )
26
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29 # Note for extension authors: ONLY specify testedwith = 'internal' for
29 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 # be specifying the version(s) of Mercurial they are tested with, or
31 # be specifying the version(s) of Mercurial they are tested with, or
32 # leave the attribute unspecified.
32 # leave the attribute unspecified.
33 testedwith = 'internal'
33 testedwith = 'ships-with-hg-core'
34
34
35 def maketemplater(ui, repo, tmpl):
35 def maketemplater(ui, repo, tmpl):
36 return cmdutil.changeset_templater(ui, repo, False, None, tmpl, None, False)
36 return cmdutil.changeset_templater(ui, repo, False, None, tmpl, None, False)
37
37
38 def changedlines(ui, repo, ctx1, ctx2, fns):
38 def changedlines(ui, repo, ctx1, ctx2, fns):
39 added, removed = 0, 0
39 added, removed = 0, 0
40 fmatch = scmutil.matchfiles(repo, fns)
40 fmatch = scmutil.matchfiles(repo, fns)
41 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
41 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
42 for l in diff.split('\n'):
42 for l in diff.split('\n'):
43 if l.startswith("+") and not l.startswith("+++ "):
43 if l.startswith("+") and not l.startswith("+++ "):
44 added += 1
44 added += 1
45 elif l.startswith("-") and not l.startswith("--- "):
45 elif l.startswith("-") and not l.startswith("--- "):
46 removed += 1
46 removed += 1
47 return (added, removed)
47 return (added, removed)
48
48
49 def countrate(ui, repo, amap, *pats, **opts):
49 def countrate(ui, repo, amap, *pats, **opts):
50 """Calculate stats"""
50 """Calculate stats"""
51 if opts.get('dateformat'):
51 if opts.get('dateformat'):
52 def getkey(ctx):
52 def getkey(ctx):
53 t, tz = ctx.date()
53 t, tz = ctx.date()
54 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
54 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
55 return date.strftime(opts['dateformat'])
55 return date.strftime(opts['dateformat'])
56 else:
56 else:
57 tmpl = opts.get('oldtemplate') or opts.get('template')
57 tmpl = opts.get('oldtemplate') or opts.get('template')
58 tmpl = maketemplater(ui, repo, tmpl)
58 tmpl = maketemplater(ui, repo, tmpl)
59 def getkey(ctx):
59 def getkey(ctx):
60 ui.pushbuffer()
60 ui.pushbuffer()
61 tmpl.show(ctx)
61 tmpl.show(ctx)
62 return ui.popbuffer()
62 return ui.popbuffer()
63
63
64 state = {'count': 0}
64 state = {'count': 0}
65 rate = {}
65 rate = {}
66 df = False
66 df = False
67 if opts.get('date'):
67 if opts.get('date'):
68 df = util.matchdate(opts['date'])
68 df = util.matchdate(opts['date'])
69
69
70 m = scmutil.match(repo[None], pats, opts)
70 m = scmutil.match(repo[None], pats, opts)
71 def prep(ctx, fns):
71 def prep(ctx, fns):
72 rev = ctx.rev()
72 rev = ctx.rev()
73 if df and not df(ctx.date()[0]): # doesn't match date format
73 if df and not df(ctx.date()[0]): # doesn't match date format
74 return
74 return
75
75
76 key = getkey(ctx).strip()
76 key = getkey(ctx).strip()
77 key = amap.get(key, key) # alias remap
77 key = amap.get(key, key) # alias remap
78 if opts.get('changesets'):
78 if opts.get('changesets'):
79 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
79 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
80 else:
80 else:
81 parents = ctx.parents()
81 parents = ctx.parents()
82 if len(parents) > 1:
82 if len(parents) > 1:
83 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
83 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
84 return
84 return
85
85
86 ctx1 = parents[0]
86 ctx1 = parents[0]
87 lines = changedlines(ui, repo, ctx1, ctx, fns)
87 lines = changedlines(ui, repo, ctx1, ctx, fns)
88 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
88 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
89
89
90 state['count'] += 1
90 state['count'] += 1
91 ui.progress(_('analyzing'), state['count'], total=len(repo),
91 ui.progress(_('analyzing'), state['count'], total=len(repo),
92 unit=_('revisions'))
92 unit=_('revisions'))
93
93
94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
95 continue
95 continue
96
96
97 ui.progress(_('analyzing'), None)
97 ui.progress(_('analyzing'), None)
98
98
99 return rate
99 return rate
100
100
101
101
102 @command('churn',
102 @command('churn',
103 [('r', 'rev', [],
103 [('r', 'rev', [],
104 _('count rate for the specified revision or revset'), _('REV')),
104 _('count rate for the specified revision or revset'), _('REV')),
105 ('d', 'date', '',
105 ('d', 'date', '',
106 _('count rate for revisions matching date spec'), _('DATE')),
106 _('count rate for revisions matching date spec'), _('DATE')),
107 ('t', 'oldtemplate', '',
107 ('t', 'oldtemplate', '',
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
109 ('T', 'template', '{author|email}',
109 ('T', 'template', '{author|email}',
110 _('template to group changesets'), _('TEMPLATE')),
110 _('template to group changesets'), _('TEMPLATE')),
111 ('f', 'dateformat', '',
111 ('f', 'dateformat', '',
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
113 ('c', 'changesets', False, _('count rate by number of changesets')),
113 ('c', 'changesets', False, _('count rate by number of changesets')),
114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
115 ('', 'diffstat', False, _('display added/removed lines separately')),
115 ('', 'diffstat', False, _('display added/removed lines separately')),
116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
117 ] + commands.walkopts,
117 ] + commands.walkopts,
118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
119 inferrepo=True)
119 inferrepo=True)
120 def churn(ui, repo, *pats, **opts):
120 def churn(ui, repo, *pats, **opts):
121 '''histogram of changes to the repository
121 '''histogram of changes to the repository
122
122
123 This command will display a histogram representing the number
123 This command will display a histogram representing the number
124 of changed lines or revisions, grouped according to the given
124 of changed lines or revisions, grouped according to the given
125 template. The default template will group changes by author.
125 template. The default template will group changes by author.
126 The --dateformat option may be used to group the results by
126 The --dateformat option may be used to group the results by
127 date instead.
127 date instead.
128
128
129 Statistics are based on the number of changed lines, or
129 Statistics are based on the number of changed lines, or
130 alternatively the number of matching revisions if the
130 alternatively the number of matching revisions if the
131 --changesets option is specified.
131 --changesets option is specified.
132
132
133 Examples::
133 Examples::
134
134
135 # display count of changed lines for every committer
135 # display count of changed lines for every committer
136 hg churn -t "{author|email}"
136 hg churn -t "{author|email}"
137
137
138 # display daily activity graph
138 # display daily activity graph
139 hg churn -f "%H" -s -c
139 hg churn -f "%H" -s -c
140
140
141 # display activity of developers by month
141 # display activity of developers by month
142 hg churn -f "%Y-%m" -s -c
142 hg churn -f "%Y-%m" -s -c
143
143
144 # display count of lines changed in every year
144 # display count of lines changed in every year
145 hg churn -f "%Y" -s
145 hg churn -f "%Y" -s
146
146
147 It is possible to map alternate email addresses to a main address
147 It is possible to map alternate email addresses to a main address
148 by providing a file using the following format::
148 by providing a file using the following format::
149
149
150 <alias email> = <actual email>
150 <alias email> = <actual email>
151
151
152 Such a file may be specified with the --aliases option, otherwise
152 Such a file may be specified with the --aliases option, otherwise
153 a .hgchurn file will be looked for in the working directory root.
153 a .hgchurn file will be looked for in the working directory root.
154 Aliases will be split from the rightmost "=".
154 Aliases will be split from the rightmost "=".
155 '''
155 '''
156 def pad(s, l):
156 def pad(s, l):
157 return s + " " * (l - encoding.colwidth(s))
157 return s + " " * (l - encoding.colwidth(s))
158
158
159 amap = {}
159 amap = {}
160 aliases = opts.get('aliases')
160 aliases = opts.get('aliases')
161 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
161 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
162 aliases = repo.wjoin('.hgchurn')
162 aliases = repo.wjoin('.hgchurn')
163 if aliases:
163 if aliases:
164 for l in open(aliases, "r"):
164 for l in open(aliases, "r"):
165 try:
165 try:
166 alias, actual = l.rsplit('=' in l and '=' or None, 1)
166 alias, actual = l.rsplit('=' in l and '=' or None, 1)
167 amap[alias.strip()] = actual.strip()
167 amap[alias.strip()] = actual.strip()
168 except ValueError:
168 except ValueError:
169 l = l.strip()
169 l = l.strip()
170 if l:
170 if l:
171 ui.warn(_("skipping malformed alias: %s\n") % l)
171 ui.warn(_("skipping malformed alias: %s\n") % l)
172 continue
172 continue
173
173
174 rate = countrate(ui, repo, amap, *pats, **opts).items()
174 rate = countrate(ui, repo, amap, *pats, **opts).items()
175 if not rate:
175 if not rate:
176 return
176 return
177
177
178 if opts.get('sort'):
178 if opts.get('sort'):
179 rate.sort()
179 rate.sort()
180 else:
180 else:
181 rate.sort(key=lambda x: (-sum(x[1]), x))
181 rate.sort(key=lambda x: (-sum(x[1]), x))
182
182
183 # Be careful not to have a zero maxcount (issue833)
183 # Be careful not to have a zero maxcount (issue833)
184 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
184 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
185 maxname = max(len(k) for k, v in rate)
185 maxname = max(len(k) for k, v in rate)
186
186
187 ttywidth = ui.termwidth()
187 ttywidth = ui.termwidth()
188 ui.debug("assuming %i character terminal\n" % ttywidth)
188 ui.debug("assuming %i character terminal\n" % ttywidth)
189 width = ttywidth - maxname - 2 - 2 - 2
189 width = ttywidth - maxname - 2 - 2 - 2
190
190
191 if opts.get('diffstat'):
191 if opts.get('diffstat'):
192 width -= 15
192 width -= 15
193 def format(name, diffstat):
193 def format(name, diffstat):
194 added, removed = diffstat
194 added, removed = diffstat
195 return "%s %15s %s%s\n" % (pad(name, maxname),
195 return "%s %15s %s%s\n" % (pad(name, maxname),
196 '+%d/-%d' % (added, removed),
196 '+%d/-%d' % (added, removed),
197 ui.label('+' * charnum(added),
197 ui.label('+' * charnum(added),
198 'diffstat.inserted'),
198 'diffstat.inserted'),
199 ui.label('-' * charnum(removed),
199 ui.label('-' * charnum(removed),
200 'diffstat.deleted'))
200 'diffstat.deleted'))
201 else:
201 else:
202 width -= 6
202 width -= 6
203 def format(name, count):
203 def format(name, count):
204 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
204 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
205 '*' * charnum(sum(count)))
205 '*' * charnum(sum(count)))
206
206
207 def charnum(count):
207 def charnum(count):
208 return int(round(count * width / maxcount))
208 return int(round(count * width / maxcount))
209
209
210 for name, count in rate:
210 for name, count in rate:
211 ui.write(format(name, count))
211 ui.write(format(name, count))
@@ -1,186 +1,186 b''
1 # This software may be used and distributed according to the terms of the
1 # This software may be used and distributed according to the terms of the
2 # GNU General Public License version 2 or any later version.
2 # GNU General Public License version 2 or any later version.
3
3
4 """advertise pre-generated bundles to seed clones
4 """advertise pre-generated bundles to seed clones
5
5
6 "clonebundles" is a server-side extension used to advertise the existence
6 "clonebundles" is a server-side extension used to advertise the existence
7 of pre-generated, externally hosted bundle files to clients that are
7 of pre-generated, externally hosted bundle files to clients that are
8 cloning so that cloning can be faster, more reliable, and require less
8 cloning so that cloning can be faster, more reliable, and require less
9 resources on the server.
9 resources on the server.
10
10
11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
12 the server, in response to a client's request to clone, dynamically generates
12 the server, in response to a client's request to clone, dynamically generates
13 a bundle containing the entire repository content and sends it to the client.
13 a bundle containing the entire repository content and sends it to the client.
14 There is no caching on the server and the server will have to redundantly
14 There is no caching on the server and the server will have to redundantly
15 generate the same outgoing bundle in response to each clone request. For
15 generate the same outgoing bundle in response to each clone request. For
16 servers with large repositories or with high clone volume, the load from
16 servers with large repositories or with high clone volume, the load from
17 clones can make scaling the server challenging and costly.
17 clones can make scaling the server challenging and costly.
18
18
19 This extension provides server operators the ability to offload potentially
19 This extension provides server operators the ability to offload potentially
20 expensive clone load to an external service. Here's how it works.
20 expensive clone load to an external service. Here's how it works.
21
21
22 1. A server operator establishes a mechanism for making bundle files available
22 1. A server operator establishes a mechanism for making bundle files available
23 on a hosting service where Mercurial clients can fetch them.
23 on a hosting service where Mercurial clients can fetch them.
24 2. A manifest file listing available bundle URLs and some optional metadata
24 2. A manifest file listing available bundle URLs and some optional metadata
25 is added to the Mercurial repository on the server.
25 is added to the Mercurial repository on the server.
26 3. A client initiates a clone against a clone bundles aware server.
26 3. A client initiates a clone against a clone bundles aware server.
27 4. The client sees the server is advertising clone bundles and fetches the
27 4. The client sees the server is advertising clone bundles and fetches the
28 manifest listing available bundles.
28 manifest listing available bundles.
29 5. The client filters and sorts the available bundles based on what it
29 5. The client filters and sorts the available bundles based on what it
30 supports and prefers.
30 supports and prefers.
31 6. The client downloads and applies an available bundle from the
31 6. The client downloads and applies an available bundle from the
32 server-specified URL.
32 server-specified URL.
33 7. The client reconnects to the original server and performs the equivalent
33 7. The client reconnects to the original server and performs the equivalent
34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
35 repository could have been updated between when the bundle was created
35 repository could have been updated between when the bundle was created
36 and when the client started the clone.)
36 and when the client started the clone.)
37
37
38 Instead of the server generating full repository bundles for every clone
38 Instead of the server generating full repository bundles for every clone
39 request, it generates full bundles once and they are subsequently reused to
39 request, it generates full bundles once and they are subsequently reused to
40 bootstrap new clones. The server may still transfer data at clone time.
40 bootstrap new clones. The server may still transfer data at clone time.
41 However, this is only data that has been added/changed since the bundle was
41 However, this is only data that has been added/changed since the bundle was
42 created. For large, established repositories, this can reduce server load for
42 created. For large, established repositories, this can reduce server load for
43 clones to less than 1% of original.
43 clones to less than 1% of original.
44
44
45 To work, this extension requires the following of server operators:
45 To work, this extension requires the following of server operators:
46
46
47 * Generating bundle files of repository content (typically periodically,
47 * Generating bundle files of repository content (typically periodically,
48 such as once per day).
48 such as once per day).
49 * A file server that clients have network access to and that Python knows
49 * A file server that clients have network access to and that Python knows
50 how to talk to through its normal URL handling facility (typically an
50 how to talk to through its normal URL handling facility (typically an
51 HTTP server).
51 HTTP server).
52 * A process for keeping the bundles manifest in sync with available bundle
52 * A process for keeping the bundles manifest in sync with available bundle
53 files.
53 files.
54
54
55 Strictly speaking, using a static file hosting server isn't required: a server
55 Strictly speaking, using a static file hosting server isn't required: a server
56 operator could use a dynamic service for retrieving bundle data. However,
56 operator could use a dynamic service for retrieving bundle data. However,
57 static file hosting services are simple and scalable and should be sufficient
57 static file hosting services are simple and scalable and should be sufficient
58 for most needs.
58 for most needs.
59
59
60 Bundle files can be generated with the :hg:`bundle` command. Typically
60 Bundle files can be generated with the :hg:`bundle` command. Typically
61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
62
62
63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
64 *streaming clone bundle*. These are bundle files that are extremely efficient
64 *streaming clone bundle*. These are bundle files that are extremely efficient
65 to produce and consume (read: fast). However, they are larger than
65 to produce and consume (read: fast). However, they are larger than
66 traditional bundle formats and require that clients support the exact set
66 traditional bundle formats and require that clients support the exact set
67 of repository data store formats in use by the repository that created them.
67 of repository data store formats in use by the repository that created them.
68 Typically, a newer server can serve data that is compatible with older clients.
68 Typically, a newer server can serve data that is compatible with older clients.
69 However, *streaming clone bundles* don't have this guarantee. **Server
69 However, *streaming clone bundles* don't have this guarantee. **Server
70 operators need to be aware that newer versions of Mercurial may produce
70 operators need to be aware that newer versions of Mercurial may produce
71 streaming clone bundles incompatible with older Mercurial versions.**
71 streaming clone bundles incompatible with older Mercurial versions.**
72
72
73 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
73 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
74 file containing the list of available bundle files suitable for seeding
74 file containing the list of available bundle files suitable for seeding
75 clones. If this file does not exist, the repository will not advertise the
75 clones. If this file does not exist, the repository will not advertise the
76 existence of clone bundles when clients connect.
76 existence of clone bundles when clients connect.
77
77
78 The manifest file contains a newline (\n) delimited list of entries.
78 The manifest file contains a newline (\n) delimited list of entries.
79
79
80 Each line in this file defines an available bundle. Lines have the format:
80 Each line in this file defines an available bundle. Lines have the format:
81
81
82 <URL> [<key>=<value>[ <key>=<value>]]
82 <URL> [<key>=<value>[ <key>=<value>]]
83
83
84 That is, a URL followed by an optional, space-delimited list of key=value
84 That is, a URL followed by an optional, space-delimited list of key=value
85 pairs describing additional properties of this bundle. Both keys and values
85 pairs describing additional properties of this bundle. Both keys and values
86 are URI encoded.
86 are URI encoded.
87
87
88 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
88 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
89 All non-uppercase keys can be used by site installations. An example use
89 All non-uppercase keys can be used by site installations. An example use
90 for custom properties is to use the *datacenter* attribute to define which
90 for custom properties is to use the *datacenter* attribute to define which
91 data center a file is hosted in. Clients could then prefer a server in the
91 data center a file is hosted in. Clients could then prefer a server in the
92 data center closest to them.
92 data center closest to them.
93
93
94 The following reserved keys are currently defined:
94 The following reserved keys are currently defined:
95
95
96 BUNDLESPEC
96 BUNDLESPEC
97 A "bundle specification" string that describes the type of the bundle.
97 A "bundle specification" string that describes the type of the bundle.
98
98
99 These are string values that are accepted by the "--type" argument of
99 These are string values that are accepted by the "--type" argument of
100 :hg:`bundle`.
100 :hg:`bundle`.
101
101
102 The values are parsed in strict mode, which means they must be of the
102 The values are parsed in strict mode, which means they must be of the
103 "<compression>-<type>" form. See
103 "<compression>-<type>" form. See
104 mercurial.exchange.parsebundlespec() for more details.
104 mercurial.exchange.parsebundlespec() for more details.
105
105
106 :hg:`debugbundle --spec` can be used to print the bundle specification
106 :hg:`debugbundle --spec` can be used to print the bundle specification
107 string for a bundle file. The output of this command can be used verbatim
107 string for a bundle file. The output of this command can be used verbatim
108 for the value of ``BUNDLESPEC`` (it is already escaped).
108 for the value of ``BUNDLESPEC`` (it is already escaped).
109
109
110 Clients will automatically filter out specifications that are unknown or
110 Clients will automatically filter out specifications that are unknown or
111 unsupported so they won't attempt to download something that likely won't
111 unsupported so they won't attempt to download something that likely won't
112 apply.
112 apply.
113
113
114 The actual value doesn't impact client behavior beyond filtering:
114 The actual value doesn't impact client behavior beyond filtering:
115 clients will still sniff the bundle type from the header of downloaded
115 clients will still sniff the bundle type from the header of downloaded
116 files.
116 files.
117
117
118 **Use of this key is highly recommended**, as it allows clients to
118 **Use of this key is highly recommended**, as it allows clients to
119 easily skip unsupported bundles. If this key is not defined, an old
119 easily skip unsupported bundles. If this key is not defined, an old
120 client may attempt to apply a bundle that it is incapable of reading.
120 client may attempt to apply a bundle that it is incapable of reading.
121
121
122 REQUIRESNI
122 REQUIRESNI
123 Whether Server Name Indication (SNI) is required to connect to the URL.
123 Whether Server Name Indication (SNI) is required to connect to the URL.
124 SNI allows servers to use multiple certificates on the same IP. It is
124 SNI allows servers to use multiple certificates on the same IP. It is
125 somewhat common in CDNs and other hosting providers. Older Python
125 somewhat common in CDNs and other hosting providers. Older Python
126 versions do not support SNI. Defining this attribute enables clients
126 versions do not support SNI. Defining this attribute enables clients
127 with older Python versions to filter this entry without experiencing
127 with older Python versions to filter this entry without experiencing
128 an opaque SSL failure at connection time.
128 an opaque SSL failure at connection time.
129
129
130 If this is defined, it is important to advertise a non-SNI fallback
130 If this is defined, it is important to advertise a non-SNI fallback
131 URL or clients running old Python releases may not be able to clone
131 URL or clients running old Python releases may not be able to clone
132 with the clonebundles facility.
132 with the clonebundles facility.
133
133
134 Value should be "true".
134 Value should be "true".
135
135
136 Manifests can contain multiple entries. Assuming metadata is defined, clients
136 Manifests can contain multiple entries. Assuming metadata is defined, clients
137 will filter entries from the manifest that they don't support. The remaining
137 will filter entries from the manifest that they don't support. The remaining
138 entries are optionally sorted by client preferences
138 entries are optionally sorted by client preferences
139 (``experimental.clonebundleprefers`` config option). The client then attempts
139 (``experimental.clonebundleprefers`` config option). The client then attempts
140 to fetch the bundle at the first URL in the remaining list.
140 to fetch the bundle at the first URL in the remaining list.
141
141
142 **Errors when downloading a bundle will fail the entire clone operation:
142 **Errors when downloading a bundle will fail the entire clone operation:
143 clients do not automatically fall back to a traditional clone.** The reason
143 clients do not automatically fall back to a traditional clone.** The reason
144 for this is that if a server is using clone bundles, it is probably doing so
144 for this is that if a server is using clone bundles, it is probably doing so
145 because the feature is necessary to help it scale. In other words, there
145 because the feature is necessary to help it scale. In other words, there
146 is an assumption that clone load will be offloaded to another service and
146 is an assumption that clone load will be offloaded to another service and
147 that the Mercurial server isn't responsible for serving this clone load.
147 that the Mercurial server isn't responsible for serving this clone load.
148 If that other service experiences issues and clients start mass falling back to
148 If that other service experiences issues and clients start mass falling back to
149 the original Mercurial server, the added clone load could overwhelm the server
149 the original Mercurial server, the added clone load could overwhelm the server
150 due to unexpected load and effectively take it offline. Not having clients
150 due to unexpected load and effectively take it offline. Not having clients
151 automatically fall back to cloning from the original server mitigates this
151 automatically fall back to cloning from the original server mitigates this
152 scenario.
152 scenario.
153
153
154 Because there is no automatic Mercurial server fallback on failure of the
154 Because there is no automatic Mercurial server fallback on failure of the
155 bundle hosting service, it is important for server operators to view the bundle
155 bundle hosting service, it is important for server operators to view the bundle
156 hosting service as an extension of the Mercurial server in terms of
156 hosting service as an extension of the Mercurial server in terms of
157 availability and service level agreements: if the bundle hosting service goes
157 availability and service level agreements: if the bundle hosting service goes
158 down, so does the ability for clients to clone. Note: clients will see a
158 down, so does the ability for clients to clone. Note: clients will see a
159 message informing them how to bypass the clone bundles facility when a failure
159 message informing them how to bypass the clone bundles facility when a failure
160 occurs. So server operators should prepare for some people to follow these
160 occurs. So server operators should prepare for some people to follow these
161 instructions when a failure occurs, thus driving more load to the original
161 instructions when a failure occurs, thus driving more load to the original
162 Mercurial server when the bundle hosting service fails.
162 Mercurial server when the bundle hosting service fails.
163 """
163 """
164
164
165 from __future__ import absolute_import
165 from __future__ import absolute_import
166
166
167 from mercurial import (
167 from mercurial import (
168 extensions,
168 extensions,
169 wireproto,
169 wireproto,
170 )
170 )
171
171
172 testedwith = 'internal'
172 testedwith = 'ships-with-hg-core'
173
173
174 def capabilities(orig, repo, proto):
174 def capabilities(orig, repo, proto):
175 caps = orig(repo, proto)
175 caps = orig(repo, proto)
176
176
177 # Only advertise if a manifest exists. This does add some I/O to requests.
177 # Only advertise if a manifest exists. This does add some I/O to requests.
178 # But this should be cheaper than a wasted network round trip due to
178 # But this should be cheaper than a wasted network round trip due to
179 # missing file.
179 # missing file.
180 if repo.opener.exists('clonebundles.manifest'):
180 if repo.opener.exists('clonebundles.manifest'):
181 caps.append('clonebundles')
181 caps.append('clonebundles')
182
182
183 return caps
183 return caps
184
184
185 def extsetup(ui):
185 def extsetup(ui):
186 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
186 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
@@ -1,665 +1,689 b''
1 # color.py color output for Mercurial commands
1 # color.py color output for Mercurial commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.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 '''colorize output from some commands
8 '''colorize output from some commands
9
9
10 The color extension colorizes output from several Mercurial commands.
10 The color extension colorizes output from several Mercurial commands.
11 For example, the diff command shows additions in green and deletions
11 For example, the diff command shows additions in green and deletions
12 in red, while the status command shows modified files in magenta. Many
12 in red, while the status command shows modified files in magenta. Many
13 other commands have analogous colors. It is possible to customize
13 other commands have analogous colors. It is possible to customize
14 these colors.
14 these colors.
15
15
16 Effects
16 Effects
17 -------
17 -------
18
18
19 Other effects in addition to color, like bold and underlined text, are
19 Other effects in addition to color, like bold and underlined text, are
20 also available. By default, the terminfo database is used to find the
20 also available. By default, the terminfo database is used to find the
21 terminal codes used to change color and effect. If terminfo is not
21 terminal codes used to change color and effect. If terminfo is not
22 available, then effects are rendered with the ECMA-48 SGR control
22 available, then effects are rendered with the ECMA-48 SGR control
23 function (aka ANSI escape codes).
23 function (aka ANSI escape codes).
24
24
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 'underline'. How each is rendered depends on the terminal emulator.
28 'underline'. How each is rendered depends on the terminal emulator.
29 Some may not be available for a given terminal type, and will be
29 Some may not be available for a given terminal type, and will be
30 silently ignored.
30 silently ignored.
31
31
32 If the terminfo entry for your terminal is missing codes for an effect
33 or has the wrong codes, you can add or override those codes in your
34 configuration::
35
36 [color]
37 terminfo.dim = \E[2m
38
39 where '\E' is substituted with an escape character.
40
32 Labels
41 Labels
33 ------
42 ------
34
43
35 Text receives color effects depending on the labels that it has. Many
44 Text receives color effects depending on the labels that it has. Many
36 default Mercurial commands emit labelled text. You can also define
45 default Mercurial commands emit labelled text. You can also define
37 your own labels in templates using the label function, see :hg:`help
46 your own labels in templates using the label function, see :hg:`help
38 templates`. A single portion of text may have more than one label. In
47 templates`. A single portion of text may have more than one label. In
39 that case, effects given to the last label will override any other
48 that case, effects given to the last label will override any other
40 effects. This includes the special "none" effect, which nullifies
49 effects. This includes the special "none" effect, which nullifies
41 other effects.
50 other effects.
42
51
43 Labels are normally invisible. In order to see these labels and their
52 Labels are normally invisible. In order to see these labels and their
44 position in the text, use the global --color=debug option. The same
53 position in the text, use the global --color=debug option. The same
45 anchor text may be associated to multiple labels, e.g.
54 anchor text may be associated to multiple labels, e.g.
46
55
47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
56 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48
57
49 The following are the default effects for some default labels. Default
58 The following are the default effects for some default labels. Default
50 effects may be overridden from your configuration file::
59 effects may be overridden from your configuration file::
51
60
52 [color]
61 [color]
53 status.modified = blue bold underline red_background
62 status.modified = blue bold underline red_background
54 status.added = green bold
63 status.added = green bold
55 status.removed = red bold blue_background
64 status.removed = red bold blue_background
56 status.deleted = cyan bold underline
65 status.deleted = cyan bold underline
57 status.unknown = magenta bold underline
66 status.unknown = magenta bold underline
58 status.ignored = black bold
67 status.ignored = black bold
59
68
60 # 'none' turns off all effects
69 # 'none' turns off all effects
61 status.clean = none
70 status.clean = none
62 status.copied = none
71 status.copied = none
63
72
64 qseries.applied = blue bold underline
73 qseries.applied = blue bold underline
65 qseries.unapplied = black bold
74 qseries.unapplied = black bold
66 qseries.missing = red bold
75 qseries.missing = red bold
67
76
68 diff.diffline = bold
77 diff.diffline = bold
69 diff.extended = cyan bold
78 diff.extended = cyan bold
70 diff.file_a = red bold
79 diff.file_a = red bold
71 diff.file_b = green bold
80 diff.file_b = green bold
72 diff.hunk = magenta
81 diff.hunk = magenta
73 diff.deleted = red
82 diff.deleted = red
74 diff.inserted = green
83 diff.inserted = green
75 diff.changed = white
84 diff.changed = white
76 diff.tab =
85 diff.tab =
77 diff.trailingwhitespace = bold red_background
86 diff.trailingwhitespace = bold red_background
78
87
79 # Blank so it inherits the style of the surrounding label
88 # Blank so it inherits the style of the surrounding label
80 changeset.public =
89 changeset.public =
81 changeset.draft =
90 changeset.draft =
82 changeset.secret =
91 changeset.secret =
83
92
84 resolve.unresolved = red bold
93 resolve.unresolved = red bold
85 resolve.resolved = green bold
94 resolve.resolved = green bold
86
95
87 bookmarks.active = green
96 bookmarks.active = green
88
97
89 branches.active = none
98 branches.active = none
90 branches.closed = black bold
99 branches.closed = black bold
91 branches.current = green
100 branches.current = green
92 branches.inactive = none
101 branches.inactive = none
93
102
94 tags.normal = green
103 tags.normal = green
95 tags.local = black bold
104 tags.local = black bold
96
105
97 rebase.rebased = blue
106 rebase.rebased = blue
98 rebase.remaining = red bold
107 rebase.remaining = red bold
99
108
100 shelve.age = cyan
109 shelve.age = cyan
101 shelve.newest = green bold
110 shelve.newest = green bold
102 shelve.name = blue bold
111 shelve.name = blue bold
103
112
104 histedit.remaining = red bold
113 histedit.remaining = red bold
105
114
106 Custom colors
115 Custom colors
107 -------------
116 -------------
108
117
109 Because there are only eight standard colors, this module allows you
118 Because there are only eight standard colors, this module allows you
110 to define color names for other color slots which might be available
119 to define color names for other color slots which might be available
111 for your terminal type, assuming terminfo mode. For instance::
120 for your terminal type, assuming terminfo mode. For instance::
112
121
113 color.brightblue = 12
122 color.brightblue = 12
114 color.pink = 207
123 color.pink = 207
115 color.orange = 202
124 color.orange = 202
116
125
117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
126 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 that have brighter colors defined in the upper eight) and, 'pink' and
127 that have brighter colors defined in the upper eight) and, 'pink' and
119 'orange' to colors in 256-color xterm's default color cube. These
128 'orange' to colors in 256-color xterm's default color cube. These
120 defined colors may then be used as any of the pre-defined eight,
129 defined colors may then be used as any of the pre-defined eight,
121 including appending '_background' to set the background to that color.
130 including appending '_background' to set the background to that color.
122
131
123 Modes
132 Modes
124 -----
133 -----
125
134
126 By default, the color extension will use ANSI mode (or win32 mode on
135 By default, the color extension will use ANSI mode (or win32 mode on
127 Windows) if it detects a terminal. To override auto mode (to enable
136 Windows) if it detects a terminal. To override auto mode (to enable
128 terminfo mode, for example), set the following configuration option::
137 terminfo mode, for example), set the following configuration option::
129
138
130 [color]
139 [color]
131 mode = terminfo
140 mode = terminfo
132
141
133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
142 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 disable color.
143 disable color.
135
144
136 Note that on some systems, terminfo mode may cause problems when using
145 Note that on some systems, terminfo mode may cause problems when using
137 color with the pager extension and less -R. less with the -R option
146 color with the pager extension and less -R. less with the -R option
138 will only display ECMA-48 color codes, and terminfo mode may sometimes
147 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 emit codes that less doesn't understand. You can work around this by
148 emit codes that less doesn't understand. You can work around this by
140 either using ansi mode (or auto mode), or by using less -r (which will
149 either using ansi mode (or auto mode), or by using less -r (which will
141 pass through all terminal control codes, not just color control
150 pass through all terminal control codes, not just color control
142 codes).
151 codes).
143
152
144 On some systems (such as MSYS in Windows), the terminal may support
153 On some systems (such as MSYS in Windows), the terminal may support
145 a different color mode than the pager (activated via the "pager"
154 a different color mode than the pager (activated via the "pager"
146 extension). It is possible to define separate modes depending on whether
155 extension). It is possible to define separate modes depending on whether
147 the pager is active::
156 the pager is active::
148
157
149 [color]
158 [color]
150 mode = auto
159 mode = auto
151 pagermode = ansi
160 pagermode = ansi
152
161
153 If ``pagermode`` is not defined, the ``mode`` will be used.
162 If ``pagermode`` is not defined, the ``mode`` will be used.
154 '''
163 '''
155
164
156 from __future__ import absolute_import
165 from __future__ import absolute_import
157
166
158 import os
167 import os
159
168
160 from mercurial.i18n import _
169 from mercurial.i18n import _
161 from mercurial import (
170 from mercurial import (
162 cmdutil,
171 cmdutil,
163 commands,
172 commands,
164 dispatch,
173 dispatch,
165 extensions,
174 extensions,
166 subrepo,
175 subrepo,
167 ui as uimod,
176 ui as uimod,
168 util,
177 util,
169 )
178 )
170
179
171 cmdtable = {}
180 cmdtable = {}
172 command = cmdutil.command(cmdtable)
181 command = cmdutil.command(cmdtable)
173 # Note for extension authors: ONLY specify testedwith = 'internal' for
182 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
174 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
183 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
175 # be specifying the version(s) of Mercurial they are tested with, or
184 # be specifying the version(s) of Mercurial they are tested with, or
176 # leave the attribute unspecified.
185 # leave the attribute unspecified.
177 testedwith = 'internal'
186 testedwith = 'ships-with-hg-core'
178
187
179 # start and stop parameters for effects
188 # start and stop parameters for effects
180 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
189 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
181 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
190 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
182 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
191 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
183 'black_background': 40, 'red_background': 41,
192 'black_background': 40, 'red_background': 41,
184 'green_background': 42, 'yellow_background': 43,
193 'green_background': 42, 'yellow_background': 43,
185 'blue_background': 44, 'purple_background': 45,
194 'blue_background': 44, 'purple_background': 45,
186 'cyan_background': 46, 'white_background': 47}
195 'cyan_background': 46, 'white_background': 47}
187
196
188 def _terminfosetup(ui, mode):
197 def _terminfosetup(ui, mode):
189 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
198 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
190
199
191 global _terminfo_params
200 global _terminfo_params
192 # If we failed to load curses, we go ahead and return.
201 # If we failed to load curses, we go ahead and return.
193 if not _terminfo_params:
202 if not _terminfo_params:
194 return
203 return
195 # Otherwise, see what the config file says.
204 # Otherwise, see what the config file says.
196 if mode not in ('auto', 'terminfo'):
205 if mode not in ('auto', 'terminfo'):
197 return
206 return
198
207
199 _terminfo_params.update((key[6:], (False, int(val)))
208 _terminfo_params.update((key[6:], (False, int(val), ''))
200 for key, val in ui.configitems('color')
209 for key, val in ui.configitems('color')
201 if key.startswith('color.'))
210 if key.startswith('color.'))
211 _terminfo_params.update((key[9:], (True, '', val.replace('\\E', '\x1b')))
212 for key, val in ui.configitems('color')
213 if key.startswith('terminfo.'))
202
214
203 try:
215 try:
204 curses.setupterm()
216 curses.setupterm()
205 except curses.error as e:
217 except curses.error as e:
206 _terminfo_params = {}
218 _terminfo_params = {}
207 return
219 return
208
220
209 for key, (b, e) in _terminfo_params.items():
221 for key, (b, e, c) in _terminfo_params.items():
210 if not b:
222 if not b:
211 continue
223 continue
212 if not curses.tigetstr(e):
224 if not c and not curses.tigetstr(e):
213 # Most terminals don't support dim, invis, etc, so don't be
225 # Most terminals don't support dim, invis, etc, so don't be
214 # noisy and use ui.debug().
226 # noisy and use ui.debug().
215 ui.debug("no terminfo entry for %s\n" % e)
227 ui.debug("no terminfo entry for %s\n" % e)
216 del _terminfo_params[key]
228 del _terminfo_params[key]
217 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
229 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
218 # Only warn about missing terminfo entries if we explicitly asked for
230 # Only warn about missing terminfo entries if we explicitly asked for
219 # terminfo mode.
231 # terminfo mode.
220 if mode == "terminfo":
232 if mode == "terminfo":
221 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
233 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
222 "ECMA-48 color\n"))
234 "ECMA-48 color\n"))
223 _terminfo_params = {}
235 _terminfo_params = {}
224
236
225 def _modesetup(ui, coloropt):
237 def _modesetup(ui, coloropt):
226 global _terminfo_params
238 global _terminfo_params
227
239
228 if coloropt == 'debug':
240 if coloropt == 'debug':
229 return 'debug'
241 return 'debug'
230
242
231 auto = (coloropt == 'auto')
243 auto = (coloropt == 'auto')
232 always = not auto and util.parsebool(coloropt)
244 always = not auto and util.parsebool(coloropt)
233 if not always and not auto:
245 if not always and not auto:
234 return None
246 return None
235
247
236 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
248 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
237
249
238 mode = ui.config('color', 'mode', 'auto')
250 mode = ui.config('color', 'mode', 'auto')
239
251
240 # If pager is active, color.pagermode overrides color.mode.
252 # If pager is active, color.pagermode overrides color.mode.
241 if getattr(ui, 'pageractive', False):
253 if getattr(ui, 'pageractive', False):
242 mode = ui.config('color', 'pagermode', mode)
254 mode = ui.config('color', 'pagermode', mode)
243
255
244 realmode = mode
256 realmode = mode
245 if mode == 'auto':
257 if mode == 'auto':
246 if os.name == 'nt':
258 if os.name == 'nt':
247 term = os.environ.get('TERM')
259 term = os.environ.get('TERM')
248 # TERM won't be defined in a vanilla cmd.exe environment.
260 # TERM won't be defined in a vanilla cmd.exe environment.
249
261
250 # UNIX-like environments on Windows such as Cygwin and MSYS will
262 # UNIX-like environments on Windows such as Cygwin and MSYS will
251 # set TERM. They appear to make a best effort attempt at setting it
263 # set TERM. They appear to make a best effort attempt at setting it
252 # to something appropriate. However, not all environments with TERM
264 # to something appropriate. However, not all environments with TERM
253 # defined support ANSI. Since "ansi" could result in terminal
265 # defined support ANSI. Since "ansi" could result in terminal
254 # gibberish, we error on the side of selecting "win32". However, if
266 # gibberish, we error on the side of selecting "win32". However, if
255 # w32effects is not defined, we almost certainly don't support
267 # w32effects is not defined, we almost certainly don't support
256 # "win32", so don't even try.
268 # "win32", so don't even try.
257 if (term and 'xterm' in term) or not w32effects:
269 if (term and 'xterm' in term) or not w32effects:
258 realmode = 'ansi'
270 realmode = 'ansi'
259 else:
271 else:
260 realmode = 'win32'
272 realmode = 'win32'
261 else:
273 else:
262 realmode = 'ansi'
274 realmode = 'ansi'
263
275
264 def modewarn():
276 def modewarn():
265 # only warn if color.mode was explicitly set and we're in
277 # only warn if color.mode was explicitly set and we're in
266 # an interactive terminal
278 # an interactive terminal
267 if mode == realmode and ui.interactive():
279 if mode == realmode and ui.interactive():
268 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
280 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
269
281
270 if realmode == 'win32':
282 if realmode == 'win32':
271 _terminfo_params = {}
283 _terminfo_params = {}
272 if not w32effects:
284 if not w32effects:
273 modewarn()
285 modewarn()
274 return None
286 return None
275 _effects.update(w32effects)
287 _effects.update(w32effects)
276 elif realmode == 'ansi':
288 elif realmode == 'ansi':
277 _terminfo_params = {}
289 _terminfo_params = {}
278 elif realmode == 'terminfo':
290 elif realmode == 'terminfo':
279 _terminfosetup(ui, mode)
291 _terminfosetup(ui, mode)
280 if not _terminfo_params:
292 if not _terminfo_params:
281 ## FIXME Shouldn't we return None in this case too?
293 ## FIXME Shouldn't we return None in this case too?
282 modewarn()
294 modewarn()
283 realmode = 'ansi'
295 realmode = 'ansi'
284 else:
296 else:
285 return None
297 return None
286
298
287 if always or (auto and formatted):
299 if always or (auto and formatted):
288 return realmode
300 return realmode
289 return None
301 return None
290
302
291 try:
303 try:
292 import curses
304 import curses
293 # Mapping from effect name to terminfo attribute name or color number.
305 # Mapping from effect name to terminfo attribute name (or raw code) or
294 # This will also force-load the curses module.
306 # color number. This will also force-load the curses module.
295 _terminfo_params = {'none': (True, 'sgr0'),
307 _terminfo_params = {'none': (True, 'sgr0', ''),
296 'standout': (True, 'smso'),
308 'standout': (True, 'smso', ''),
297 'underline': (True, 'smul'),
309 'underline': (True, 'smul', ''),
298 'reverse': (True, 'rev'),
310 'reverse': (True, 'rev', ''),
299 'inverse': (True, 'rev'),
311 'inverse': (True, 'rev', ''),
300 'blink': (True, 'blink'),
312 'blink': (True, 'blink', ''),
301 'dim': (True, 'dim'),
313 'dim': (True, 'dim', ''),
302 'bold': (True, 'bold'),
314 'bold': (True, 'bold', ''),
303 'invisible': (True, 'invis'),
315 'invisible': (True, 'invis', ''),
304 'italic': (True, 'sitm'),
316 'italic': (True, 'sitm', ''),
305 'black': (False, curses.COLOR_BLACK),
317 'black': (False, curses.COLOR_BLACK, ''),
306 'red': (False, curses.COLOR_RED),
318 'red': (False, curses.COLOR_RED, ''),
307 'green': (False, curses.COLOR_GREEN),
319 'green': (False, curses.COLOR_GREEN, ''),
308 'yellow': (False, curses.COLOR_YELLOW),
320 'yellow': (False, curses.COLOR_YELLOW, ''),
309 'blue': (False, curses.COLOR_BLUE),
321 'blue': (False, curses.COLOR_BLUE, ''),
310 'magenta': (False, curses.COLOR_MAGENTA),
322 'magenta': (False, curses.COLOR_MAGENTA, ''),
311 'cyan': (False, curses.COLOR_CYAN),
323 'cyan': (False, curses.COLOR_CYAN, ''),
312 'white': (False, curses.COLOR_WHITE)}
324 'white': (False, curses.COLOR_WHITE, '')}
313 except ImportError:
325 except ImportError:
314 _terminfo_params = {}
326 _terminfo_params = {}
315
327
316 _styles = {'grep.match': 'red bold',
328 _styles = {'grep.match': 'red bold',
317 'grep.linenumber': 'green',
329 'grep.linenumber': 'green',
318 'grep.rev': 'green',
330 'grep.rev': 'green',
319 'grep.change': 'green',
331 'grep.change': 'green',
320 'grep.sep': 'cyan',
332 'grep.sep': 'cyan',
321 'grep.filename': 'magenta',
333 'grep.filename': 'magenta',
322 'grep.user': 'magenta',
334 'grep.user': 'magenta',
323 'grep.date': 'magenta',
335 'grep.date': 'magenta',
324 'bookmarks.active': 'green',
336 'bookmarks.active': 'green',
325 'branches.active': 'none',
337 'branches.active': 'none',
326 'branches.closed': 'black bold',
338 'branches.closed': 'black bold',
327 'branches.current': 'green',
339 'branches.current': 'green',
328 'branches.inactive': 'none',
340 'branches.inactive': 'none',
329 'diff.changed': 'white',
341 'diff.changed': 'white',
330 'diff.deleted': 'red',
342 'diff.deleted': 'red',
331 'diff.diffline': 'bold',
343 'diff.diffline': 'bold',
332 'diff.extended': 'cyan bold',
344 'diff.extended': 'cyan bold',
333 'diff.file_a': 'red bold',
345 'diff.file_a': 'red bold',
334 'diff.file_b': 'green bold',
346 'diff.file_b': 'green bold',
335 'diff.hunk': 'magenta',
347 'diff.hunk': 'magenta',
336 'diff.inserted': 'green',
348 'diff.inserted': 'green',
337 'diff.tab': '',
349 'diff.tab': '',
338 'diff.trailingwhitespace': 'bold red_background',
350 'diff.trailingwhitespace': 'bold red_background',
339 'changeset.public' : '',
351 'changeset.public' : '',
340 'changeset.draft' : '',
352 'changeset.draft' : '',
341 'changeset.secret' : '',
353 'changeset.secret' : '',
342 'diffstat.deleted': 'red',
354 'diffstat.deleted': 'red',
343 'diffstat.inserted': 'green',
355 'diffstat.inserted': 'green',
344 'histedit.remaining': 'red bold',
356 'histedit.remaining': 'red bold',
345 'ui.prompt': 'yellow',
357 'ui.prompt': 'yellow',
346 'log.changeset': 'yellow',
358 'log.changeset': 'yellow',
347 'patchbomb.finalsummary': '',
359 'patchbomb.finalsummary': '',
348 'patchbomb.from': 'magenta',
360 'patchbomb.from': 'magenta',
349 'patchbomb.to': 'cyan',
361 'patchbomb.to': 'cyan',
350 'patchbomb.subject': 'green',
362 'patchbomb.subject': 'green',
351 'patchbomb.diffstats': '',
363 'patchbomb.diffstats': '',
352 'rebase.rebased': 'blue',
364 'rebase.rebased': 'blue',
353 'rebase.remaining': 'red bold',
365 'rebase.remaining': 'red bold',
354 'resolve.resolved': 'green bold',
366 'resolve.resolved': 'green bold',
355 'resolve.unresolved': 'red bold',
367 'resolve.unresolved': 'red bold',
356 'shelve.age': 'cyan',
368 'shelve.age': 'cyan',
357 'shelve.newest': 'green bold',
369 'shelve.newest': 'green bold',
358 'shelve.name': 'blue bold',
370 'shelve.name': 'blue bold',
359 'status.added': 'green bold',
371 'status.added': 'green bold',
360 'status.clean': 'none',
372 'status.clean': 'none',
361 'status.copied': 'none',
373 'status.copied': 'none',
362 'status.deleted': 'cyan bold underline',
374 'status.deleted': 'cyan bold underline',
363 'status.ignored': 'black bold',
375 'status.ignored': 'black bold',
364 'status.modified': 'blue bold',
376 'status.modified': 'blue bold',
365 'status.removed': 'red bold',
377 'status.removed': 'red bold',
366 'status.unknown': 'magenta bold underline',
378 'status.unknown': 'magenta bold underline',
367 'tags.normal': 'green',
379 'tags.normal': 'green',
368 'tags.local': 'black bold'}
380 'tags.local': 'black bold'}
369
381
370
382
371 def _effect_str(effect):
383 def _effect_str(effect):
372 '''Helper function for render_effects().'''
384 '''Helper function for render_effects().'''
373
385
374 bg = False
386 bg = False
375 if effect.endswith('_background'):
387 if effect.endswith('_background'):
376 bg = True
388 bg = True
377 effect = effect[:-11]
389 effect = effect[:-11]
378 attr, val = _terminfo_params[effect]
390 try:
391 attr, val, termcode = _terminfo_params[effect]
392 except KeyError:
393 return ''
379 if attr:
394 if attr:
380 return curses.tigetstr(val)
395 if termcode:
396 return termcode
397 else:
398 return curses.tigetstr(val)
381 elif bg:
399 elif bg:
382 return curses.tparm(curses.tigetstr('setab'), val)
400 return curses.tparm(curses.tigetstr('setab'), val)
383 else:
401 else:
384 return curses.tparm(curses.tigetstr('setaf'), val)
402 return curses.tparm(curses.tigetstr('setaf'), val)
385
403
386 def render_effects(text, effects):
404 def render_effects(text, effects):
387 'Wrap text in commands to turn on each effect.'
405 'Wrap text in commands to turn on each effect.'
388 if not text:
406 if not text:
389 return text
407 return text
390 if not _terminfo_params:
408 if not _terminfo_params:
391 start = [str(_effects[e]) for e in ['none'] + effects.split()]
409 start = [str(_effects[e]) for e in ['none'] + effects.split()]
392 start = '\033[' + ';'.join(start) + 'm'
410 start = '\033[' + ';'.join(start) + 'm'
393 stop = '\033[' + str(_effects['none']) + 'm'
411 stop = '\033[' + str(_effects['none']) + 'm'
394 else:
412 else:
395 start = ''.join(_effect_str(effect)
413 start = ''.join(_effect_str(effect)
396 for effect in ['none'] + effects.split())
414 for effect in ['none'] + effects.split())
397 stop = _effect_str('none')
415 stop = _effect_str('none')
398 return ''.join([start, text, stop])
416 return ''.join([start, text, stop])
399
417
400 def extstyles():
418 def extstyles():
401 for name, ext in extensions.extensions():
419 for name, ext in extensions.extensions():
402 _styles.update(getattr(ext, 'colortable', {}))
420 _styles.update(getattr(ext, 'colortable', {}))
403
421
404 def valideffect(effect):
422 def valideffect(effect):
405 'Determine if the effect is valid or not.'
423 'Determine if the effect is valid or not.'
406 good = False
424 good = False
407 if not _terminfo_params and effect in _effects:
425 if not _terminfo_params and effect in _effects:
408 good = True
426 good = True
409 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
427 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
410 good = True
428 good = True
411 return good
429 return good
412
430
413 def configstyles(ui):
431 def configstyles(ui):
414 for status, cfgeffects in ui.configitems('color'):
432 for status, cfgeffects in ui.configitems('color'):
415 if '.' not in status or status.startswith('color.'):
433 if '.' not in status or status.startswith(('color.', 'terminfo.')):
416 continue
434 continue
417 cfgeffects = ui.configlist('color', status)
435 cfgeffects = ui.configlist('color', status)
418 if cfgeffects:
436 if cfgeffects:
419 good = []
437 good = []
420 for e in cfgeffects:
438 for e in cfgeffects:
421 if valideffect(e):
439 if valideffect(e):
422 good.append(e)
440 good.append(e)
423 else:
441 else:
424 ui.warn(_("ignoring unknown color/effect %r "
442 ui.warn(_("ignoring unknown color/effect %r "
425 "(configured in color.%s)\n")
443 "(configured in color.%s)\n")
426 % (e, status))
444 % (e, status))
427 _styles[status] = ' '.join(good)
445 _styles[status] = ' '.join(good)
428
446
429 class colorui(uimod.ui):
447 class colorui(uimod.ui):
430 _colormode = 'ansi'
448 _colormode = 'ansi'
431 def write(self, *args, **opts):
449 def write(self, *args, **opts):
432 if self._colormode is None:
450 if self._colormode is None:
433 return super(colorui, self).write(*args, **opts)
451 return super(colorui, self).write(*args, **opts)
434
452
435 label = opts.get('label', '')
453 label = opts.get('label', '')
436 if self._buffers and not opts.get('prompt', False):
454 if self._buffers and not opts.get('prompt', False):
437 if self._bufferapplylabels:
455 if self._bufferapplylabels:
438 self._buffers[-1].extend(self.label(a, label) for a in args)
456 self._buffers[-1].extend(self.label(a, label) for a in args)
439 else:
457 else:
440 self._buffers[-1].extend(args)
458 self._buffers[-1].extend(args)
441 elif self._colormode == 'win32':
459 elif self._colormode == 'win32':
442 for a in args:
460 for a in args:
443 win32print(a, super(colorui, self).write, **opts)
461 win32print(a, super(colorui, self).write, **opts)
444 else:
462 else:
445 return super(colorui, self).write(
463 return super(colorui, self).write(
446 *[self.label(a, label) for a in args], **opts)
464 *[self.label(a, label) for a in args], **opts)
447
465
448 def write_err(self, *args, **opts):
466 def write_err(self, *args, **opts):
449 if self._colormode is None:
467 if self._colormode is None:
450 return super(colorui, self).write_err(*args, **opts)
468 return super(colorui, self).write_err(*args, **opts)
451
469
452 label = opts.get('label', '')
470 label = opts.get('label', '')
453 if self._bufferstates and self._bufferstates[-1][0]:
471 if self._bufferstates and self._bufferstates[-1][0]:
454 return self.write(*args, **opts)
472 return self.write(*args, **opts)
455 if self._colormode == 'win32':
473 if self._colormode == 'win32':
456 for a in args:
474 for a in args:
457 win32print(a, super(colorui, self).write_err, **opts)
475 win32print(a, super(colorui, self).write_err, **opts)
458 else:
476 else:
459 return super(colorui, self).write_err(
477 return super(colorui, self).write_err(
460 *[self.label(a, label) for a in args], **opts)
478 *[self.label(a, label) for a in args], **opts)
461
479
462 def showlabel(self, msg, label):
480 def showlabel(self, msg, label):
463 if label and msg:
481 if label and msg:
464 if msg[-1] == '\n':
482 if msg[-1] == '\n':
465 return "[%s|%s]\n" % (label, msg[:-1])
483 return "[%s|%s]\n" % (label, msg[:-1])
466 else:
484 else:
467 return "[%s|%s]" % (label, msg)
485 return "[%s|%s]" % (label, msg)
468 else:
486 else:
469 return msg
487 return msg
470
488
471 def label(self, msg, label):
489 def label(self, msg, label):
472 if self._colormode is None:
490 if self._colormode is None:
473 return super(colorui, self).label(msg, label)
491 return super(colorui, self).label(msg, label)
474
492
475 if self._colormode == 'debug':
493 if self._colormode == 'debug':
476 return self.showlabel(msg, label)
494 return self.showlabel(msg, label)
477
495
478 effects = []
496 effects = []
479 for l in label.split():
497 for l in label.split():
480 s = _styles.get(l, '')
498 s = _styles.get(l, '')
481 if s:
499 if s:
482 effects.append(s)
500 effects.append(s)
483 elif valideffect(l):
501 elif valideffect(l):
484 effects.append(l)
502 effects.append(l)
485 effects = ' '.join(effects)
503 effects = ' '.join(effects)
486 if effects:
504 if effects:
487 return '\n'.join([render_effects(s, effects)
505 return '\n'.join([render_effects(s, effects)
488 for s in msg.split('\n')])
506 for s in msg.split('\n')])
489 return msg
507 return msg
490
508
491 def uisetup(ui):
509 def uisetup(ui):
492 if ui.plain():
510 if ui.plain():
493 return
511 return
494 if not isinstance(ui, colorui):
512 if not isinstance(ui, colorui):
495 colorui.__bases__ = (ui.__class__,)
513 colorui.__bases__ = (ui.__class__,)
496 ui.__class__ = colorui
514 ui.__class__ = colorui
497 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
515 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
498 mode = _modesetup(ui_, opts['color'])
516 mode = _modesetup(ui_, opts['color'])
499 colorui._colormode = mode
517 colorui._colormode = mode
500 if mode and mode != 'debug':
518 if mode and mode != 'debug':
501 extstyles()
519 extstyles()
502 configstyles(ui_)
520 configstyles(ui_)
503 return orig(ui_, opts, cmd, cmdfunc)
521 return orig(ui_, opts, cmd, cmdfunc)
504 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
522 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
505 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
523 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
506 # insert the argument in the front,
524 # insert the argument in the front,
507 # the end of git diff arguments is used for paths
525 # the end of git diff arguments is used for paths
508 commands.insert(1, '--color')
526 commands.insert(1, '--color')
509 return orig(gitsub, commands, env, stream, cwd)
527 return orig(gitsub, commands, env, stream, cwd)
510 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
528 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
511 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
529 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
512
530
513 def extsetup(ui):
531 def extsetup(ui):
514 commands.globalopts.append(
532 commands.globalopts.append(
515 ('', 'color', 'auto',
533 ('', 'color', 'auto',
516 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
534 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
517 # and should not be translated
535 # and should not be translated
518 _("when to colorize (boolean, always, auto, never, or debug)"),
536 _("when to colorize (boolean, always, auto, never, or debug)"),
519 _('TYPE')))
537 _('TYPE')))
520
538
521 @command('debugcolor', [], 'hg debugcolor')
539 @command('debugcolor', [], 'hg debugcolor')
522 def debugcolor(ui, repo, **opts):
540 def debugcolor(ui, repo, **opts):
523 global _styles
541 global _styles
524 _styles = {}
542 _styles = {}
525 for effect in _effects.keys():
543 for effect in _effects.keys():
526 _styles[effect] = effect
544 _styles[effect] = effect
545 if _terminfo_params:
546 for k, v in ui.configitems('color'):
547 if k.startswith('color.'):
548 _styles[k] = k[6:]
549 elif k.startswith('terminfo.'):
550 _styles[k] = k[9:]
527 ui.write(('color mode: %s\n') % ui._colormode)
551 ui.write(('color mode: %s\n') % ui._colormode)
528 ui.write(_('available colors:\n'))
552 ui.write(_('available colors:\n'))
529 for label, colors in _styles.items():
553 for colorname, label in _styles.items():
530 ui.write(('%s\n') % colors, label=label)
554 ui.write(('%s\n') % colorname, label=label)
531
555
532 if os.name != 'nt':
556 if os.name != 'nt':
533 w32effects = None
557 w32effects = None
534 else:
558 else:
535 import ctypes
559 import ctypes
536 import re
560 import re
537
561
538 _kernel32 = ctypes.windll.kernel32
562 _kernel32 = ctypes.windll.kernel32
539
563
540 _WORD = ctypes.c_ushort
564 _WORD = ctypes.c_ushort
541
565
542 _INVALID_HANDLE_VALUE = -1
566 _INVALID_HANDLE_VALUE = -1
543
567
544 class _COORD(ctypes.Structure):
568 class _COORD(ctypes.Structure):
545 _fields_ = [('X', ctypes.c_short),
569 _fields_ = [('X', ctypes.c_short),
546 ('Y', ctypes.c_short)]
570 ('Y', ctypes.c_short)]
547
571
548 class _SMALL_RECT(ctypes.Structure):
572 class _SMALL_RECT(ctypes.Structure):
549 _fields_ = [('Left', ctypes.c_short),
573 _fields_ = [('Left', ctypes.c_short),
550 ('Top', ctypes.c_short),
574 ('Top', ctypes.c_short),
551 ('Right', ctypes.c_short),
575 ('Right', ctypes.c_short),
552 ('Bottom', ctypes.c_short)]
576 ('Bottom', ctypes.c_short)]
553
577
554 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
578 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
555 _fields_ = [('dwSize', _COORD),
579 _fields_ = [('dwSize', _COORD),
556 ('dwCursorPosition', _COORD),
580 ('dwCursorPosition', _COORD),
557 ('wAttributes', _WORD),
581 ('wAttributes', _WORD),
558 ('srWindow', _SMALL_RECT),
582 ('srWindow', _SMALL_RECT),
559 ('dwMaximumWindowSize', _COORD)]
583 ('dwMaximumWindowSize', _COORD)]
560
584
561 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
585 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
562 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
586 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
563
587
564 _FOREGROUND_BLUE = 0x0001
588 _FOREGROUND_BLUE = 0x0001
565 _FOREGROUND_GREEN = 0x0002
589 _FOREGROUND_GREEN = 0x0002
566 _FOREGROUND_RED = 0x0004
590 _FOREGROUND_RED = 0x0004
567 _FOREGROUND_INTENSITY = 0x0008
591 _FOREGROUND_INTENSITY = 0x0008
568
592
569 _BACKGROUND_BLUE = 0x0010
593 _BACKGROUND_BLUE = 0x0010
570 _BACKGROUND_GREEN = 0x0020
594 _BACKGROUND_GREEN = 0x0020
571 _BACKGROUND_RED = 0x0040
595 _BACKGROUND_RED = 0x0040
572 _BACKGROUND_INTENSITY = 0x0080
596 _BACKGROUND_INTENSITY = 0x0080
573
597
574 _COMMON_LVB_REVERSE_VIDEO = 0x4000
598 _COMMON_LVB_REVERSE_VIDEO = 0x4000
575 _COMMON_LVB_UNDERSCORE = 0x8000
599 _COMMON_LVB_UNDERSCORE = 0x8000
576
600
577 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
601 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
578 w32effects = {
602 w32effects = {
579 'none': -1,
603 'none': -1,
580 'black': 0,
604 'black': 0,
581 'red': _FOREGROUND_RED,
605 'red': _FOREGROUND_RED,
582 'green': _FOREGROUND_GREEN,
606 'green': _FOREGROUND_GREEN,
583 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
607 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
584 'blue': _FOREGROUND_BLUE,
608 'blue': _FOREGROUND_BLUE,
585 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
609 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
586 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
610 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
587 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
611 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
588 'bold': _FOREGROUND_INTENSITY,
612 'bold': _FOREGROUND_INTENSITY,
589 'black_background': 0x100, # unused value > 0x0f
613 'black_background': 0x100, # unused value > 0x0f
590 'red_background': _BACKGROUND_RED,
614 'red_background': _BACKGROUND_RED,
591 'green_background': _BACKGROUND_GREEN,
615 'green_background': _BACKGROUND_GREEN,
592 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
616 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
593 'blue_background': _BACKGROUND_BLUE,
617 'blue_background': _BACKGROUND_BLUE,
594 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
618 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
595 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
619 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
596 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
620 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
597 _BACKGROUND_BLUE),
621 _BACKGROUND_BLUE),
598 'bold_background': _BACKGROUND_INTENSITY,
622 'bold_background': _BACKGROUND_INTENSITY,
599 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
623 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
600 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
624 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
601 }
625 }
602
626
603 passthrough = set([_FOREGROUND_INTENSITY,
627 passthrough = set([_FOREGROUND_INTENSITY,
604 _BACKGROUND_INTENSITY,
628 _BACKGROUND_INTENSITY,
605 _COMMON_LVB_UNDERSCORE,
629 _COMMON_LVB_UNDERSCORE,
606 _COMMON_LVB_REVERSE_VIDEO])
630 _COMMON_LVB_REVERSE_VIDEO])
607
631
608 stdout = _kernel32.GetStdHandle(
632 stdout = _kernel32.GetStdHandle(
609 _STD_OUTPUT_HANDLE) # don't close the handle returned
633 _STD_OUTPUT_HANDLE) # don't close the handle returned
610 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
634 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
611 w32effects = None
635 w32effects = None
612 else:
636 else:
613 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
637 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
614 if not _kernel32.GetConsoleScreenBufferInfo(
638 if not _kernel32.GetConsoleScreenBufferInfo(
615 stdout, ctypes.byref(csbi)):
639 stdout, ctypes.byref(csbi)):
616 # stdout may not support GetConsoleScreenBufferInfo()
640 # stdout may not support GetConsoleScreenBufferInfo()
617 # when called from subprocess or redirected
641 # when called from subprocess or redirected
618 w32effects = None
642 w32effects = None
619 else:
643 else:
620 origattr = csbi.wAttributes
644 origattr = csbi.wAttributes
621 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
645 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
622 re.MULTILINE | re.DOTALL)
646 re.MULTILINE | re.DOTALL)
623
647
624 def win32print(text, orig, **opts):
648 def win32print(text, orig, **opts):
625 label = opts.get('label', '')
649 label = opts.get('label', '')
626 attr = origattr
650 attr = origattr
627
651
628 def mapcolor(val, attr):
652 def mapcolor(val, attr):
629 if val == -1:
653 if val == -1:
630 return origattr
654 return origattr
631 elif val in passthrough:
655 elif val in passthrough:
632 return attr | val
656 return attr | val
633 elif val > 0x0f:
657 elif val > 0x0f:
634 return (val & 0x70) | (attr & 0x8f)
658 return (val & 0x70) | (attr & 0x8f)
635 else:
659 else:
636 return (val & 0x07) | (attr & 0xf8)
660 return (val & 0x07) | (attr & 0xf8)
637
661
638 # determine console attributes based on labels
662 # determine console attributes based on labels
639 for l in label.split():
663 for l in label.split():
640 style = _styles.get(l, '')
664 style = _styles.get(l, '')
641 for effect in style.split():
665 for effect in style.split():
642 try:
666 try:
643 attr = mapcolor(w32effects[effect], attr)
667 attr = mapcolor(w32effects[effect], attr)
644 except KeyError:
668 except KeyError:
645 # w32effects could not have certain attributes so we skip
669 # w32effects could not have certain attributes so we skip
646 # them if not found
670 # them if not found
647 pass
671 pass
648 # hack to ensure regexp finds data
672 # hack to ensure regexp finds data
649 if not text.startswith('\033['):
673 if not text.startswith('\033['):
650 text = '\033[m' + text
674 text = '\033[m' + text
651
675
652 # Look for ANSI-like codes embedded in text
676 # Look for ANSI-like codes embedded in text
653 m = re.match(ansire, text)
677 m = re.match(ansire, text)
654
678
655 try:
679 try:
656 while m:
680 while m:
657 for sattr in m.group(1).split(';'):
681 for sattr in m.group(1).split(';'):
658 if sattr:
682 if sattr:
659 attr = mapcolor(int(sattr), attr)
683 attr = mapcolor(int(sattr), attr)
660 _kernel32.SetConsoleTextAttribute(stdout, attr)
684 _kernel32.SetConsoleTextAttribute(stdout, attr)
661 orig(m.group(2), **opts)
685 orig(m.group(2), **opts)
662 m = re.match(ansire, m.group(3))
686 m = re.match(ansire, m.group(3))
663 finally:
687 finally:
664 # Explicitly reset original attributes
688 # Explicitly reset original attributes
665 _kernel32.SetConsoleTextAttribute(stdout, origattr)
689 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,458 +1,458 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
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 '''import revisions from foreign VCS repositories into Mercurial'''
8 '''import revisions from foreign VCS repositories into Mercurial'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 cmdutil,
14 cmdutil,
15 registrar,
15 registrar,
16 )
16 )
17
17
18 from . import (
18 from . import (
19 convcmd,
19 convcmd,
20 cvsps,
20 cvsps,
21 subversion,
21 subversion,
22 )
22 )
23
23
24 cmdtable = {}
24 cmdtable = {}
25 command = cmdutil.command(cmdtable)
25 command = cmdutil.command(cmdtable)
26 # Note for extension authors: ONLY specify testedwith = 'internal' for
26 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
28 # be specifying the version(s) of Mercurial they are tested with, or
28 # be specifying the version(s) of Mercurial they are tested with, or
29 # leave the attribute unspecified.
29 # leave the attribute unspecified.
30 testedwith = 'internal'
30 testedwith = 'ships-with-hg-core'
31
31
32 # Commands definition was moved elsewhere to ease demandload job.
32 # Commands definition was moved elsewhere to ease demandload job.
33
33
34 @command('convert',
34 @command('convert',
35 [('', 'authors', '',
35 [('', 'authors', '',
36 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
36 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
37 _('FILE')),
37 _('FILE')),
38 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
38 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
39 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
39 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
40 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
40 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
41 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
41 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
42 ('', 'filemap', '', _('remap file names using contents of file'),
42 ('', 'filemap', '', _('remap file names using contents of file'),
43 _('FILE')),
43 _('FILE')),
44 ('', 'full', None,
44 ('', 'full', None,
45 _('apply filemap changes by converting all files again')),
45 _('apply filemap changes by converting all files again')),
46 ('', 'splicemap', '', _('splice synthesized history into place'),
46 ('', 'splicemap', '', _('splice synthesized history into place'),
47 _('FILE')),
47 _('FILE')),
48 ('', 'branchmap', '', _('change branch names while converting'),
48 ('', 'branchmap', '', _('change branch names while converting'),
49 _('FILE')),
49 _('FILE')),
50 ('', 'branchsort', None, _('try to sort changesets by branches')),
50 ('', 'branchsort', None, _('try to sort changesets by branches')),
51 ('', 'datesort', None, _('try to sort changesets by date')),
51 ('', 'datesort', None, _('try to sort changesets by date')),
52 ('', 'sourcesort', None, _('preserve source changesets order')),
52 ('', 'sourcesort', None, _('preserve source changesets order')),
53 ('', 'closesort', None, _('try to reorder closed revisions'))],
53 ('', 'closesort', None, _('try to reorder closed revisions'))],
54 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
54 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
55 norepo=True)
55 norepo=True)
56 def convert(ui, src, dest=None, revmapfile=None, **opts):
56 def convert(ui, src, dest=None, revmapfile=None, **opts):
57 """convert a foreign SCM repository to a Mercurial one.
57 """convert a foreign SCM repository to a Mercurial one.
58
58
59 Accepted source formats [identifiers]:
59 Accepted source formats [identifiers]:
60
60
61 - Mercurial [hg]
61 - Mercurial [hg]
62 - CVS [cvs]
62 - CVS [cvs]
63 - Darcs [darcs]
63 - Darcs [darcs]
64 - git [git]
64 - git [git]
65 - Subversion [svn]
65 - Subversion [svn]
66 - Monotone [mtn]
66 - Monotone [mtn]
67 - GNU Arch [gnuarch]
67 - GNU Arch [gnuarch]
68 - Bazaar [bzr]
68 - Bazaar [bzr]
69 - Perforce [p4]
69 - Perforce [p4]
70
70
71 Accepted destination formats [identifiers]:
71 Accepted destination formats [identifiers]:
72
72
73 - Mercurial [hg]
73 - Mercurial [hg]
74 - Subversion [svn] (history on branches is not preserved)
74 - Subversion [svn] (history on branches is not preserved)
75
75
76 If no revision is given, all revisions will be converted.
76 If no revision is given, all revisions will be converted.
77 Otherwise, convert will only import up to the named revision
77 Otherwise, convert will only import up to the named revision
78 (given in a format understood by the source).
78 (given in a format understood by the source).
79
79
80 If no destination directory name is specified, it defaults to the
80 If no destination directory name is specified, it defaults to the
81 basename of the source with ``-hg`` appended. If the destination
81 basename of the source with ``-hg`` appended. If the destination
82 repository doesn't exist, it will be created.
82 repository doesn't exist, it will be created.
83
83
84 By default, all sources except Mercurial will use --branchsort.
84 By default, all sources except Mercurial will use --branchsort.
85 Mercurial uses --sourcesort to preserve original revision numbers
85 Mercurial uses --sourcesort to preserve original revision numbers
86 order. Sort modes have the following effects:
86 order. Sort modes have the following effects:
87
87
88 --branchsort convert from parent to child revision when possible,
88 --branchsort convert from parent to child revision when possible,
89 which means branches are usually converted one after
89 which means branches are usually converted one after
90 the other. It generates more compact repositories.
90 the other. It generates more compact repositories.
91
91
92 --datesort sort revisions by date. Converted repositories have
92 --datesort sort revisions by date. Converted repositories have
93 good-looking changelogs but are often an order of
93 good-looking changelogs but are often an order of
94 magnitude larger than the same ones generated by
94 magnitude larger than the same ones generated by
95 --branchsort.
95 --branchsort.
96
96
97 --sourcesort try to preserve source revisions order, only
97 --sourcesort try to preserve source revisions order, only
98 supported by Mercurial sources.
98 supported by Mercurial sources.
99
99
100 --closesort try to move closed revisions as close as possible
100 --closesort try to move closed revisions as close as possible
101 to parent branches, only supported by Mercurial
101 to parent branches, only supported by Mercurial
102 sources.
102 sources.
103
103
104 If ``REVMAP`` isn't given, it will be put in a default location
104 If ``REVMAP`` isn't given, it will be put in a default location
105 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
105 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
106 text file that maps each source commit ID to the destination ID
106 text file that maps each source commit ID to the destination ID
107 for that revision, like so::
107 for that revision, like so::
108
108
109 <source ID> <destination ID>
109 <source ID> <destination ID>
110
110
111 If the file doesn't exist, it's automatically created. It's
111 If the file doesn't exist, it's automatically created. It's
112 updated on each commit copied, so :hg:`convert` can be interrupted
112 updated on each commit copied, so :hg:`convert` can be interrupted
113 and can be run repeatedly to copy new commits.
113 and can be run repeatedly to copy new commits.
114
114
115 The authormap is a simple text file that maps each source commit
115 The authormap is a simple text file that maps each source commit
116 author to a destination commit author. It is handy for source SCMs
116 author to a destination commit author. It is handy for source SCMs
117 that use unix logins to identify authors (e.g.: CVS). One line per
117 that use unix logins to identify authors (e.g.: CVS). One line per
118 author mapping and the line format is::
118 author mapping and the line format is::
119
119
120 source author = destination author
120 source author = destination author
121
121
122 Empty lines and lines starting with a ``#`` are ignored.
122 Empty lines and lines starting with a ``#`` are ignored.
123
123
124 The filemap is a file that allows filtering and remapping of files
124 The filemap is a file that allows filtering and remapping of files
125 and directories. Each line can contain one of the following
125 and directories. Each line can contain one of the following
126 directives::
126 directives::
127
127
128 include path/to/file-or-dir
128 include path/to/file-or-dir
129
129
130 exclude path/to/file-or-dir
130 exclude path/to/file-or-dir
131
131
132 rename path/to/source path/to/destination
132 rename path/to/source path/to/destination
133
133
134 Comment lines start with ``#``. A specified path matches if it
134 Comment lines start with ``#``. A specified path matches if it
135 equals the full relative name of a file or one of its parent
135 equals the full relative name of a file or one of its parent
136 directories. The ``include`` or ``exclude`` directive with the
136 directories. The ``include`` or ``exclude`` directive with the
137 longest matching path applies, so line order does not matter.
137 longest matching path applies, so line order does not matter.
138
138
139 The ``include`` directive causes a file, or all files under a
139 The ``include`` directive causes a file, or all files under a
140 directory, to be included in the destination repository. The default
140 directory, to be included in the destination repository. The default
141 if there are no ``include`` statements is to include everything.
141 if there are no ``include`` statements is to include everything.
142 If there are any ``include`` statements, nothing else is included.
142 If there are any ``include`` statements, nothing else is included.
143 The ``exclude`` directive causes files or directories to
143 The ``exclude`` directive causes files or directories to
144 be omitted. The ``rename`` directive renames a file or directory if
144 be omitted. The ``rename`` directive renames a file or directory if
145 it is converted. To rename from a subdirectory into the root of
145 it is converted. To rename from a subdirectory into the root of
146 the repository, use ``.`` as the path to rename to.
146 the repository, use ``.`` as the path to rename to.
147
147
148 ``--full`` will make sure the converted changesets contain exactly
148 ``--full`` will make sure the converted changesets contain exactly
149 the right files with the right content. It will make a full
149 the right files with the right content. It will make a full
150 conversion of all files, not just the ones that have
150 conversion of all files, not just the ones that have
151 changed. Files that already are correct will not be changed. This
151 changed. Files that already are correct will not be changed. This
152 can be used to apply filemap changes when converting
152 can be used to apply filemap changes when converting
153 incrementally. This is currently only supported for Mercurial and
153 incrementally. This is currently only supported for Mercurial and
154 Subversion.
154 Subversion.
155
155
156 The splicemap is a file that allows insertion of synthetic
156 The splicemap is a file that allows insertion of synthetic
157 history, letting you specify the parents of a revision. This is
157 history, letting you specify the parents of a revision. This is
158 useful if you want to e.g. give a Subversion merge two parents, or
158 useful if you want to e.g. give a Subversion merge two parents, or
159 graft two disconnected series of history together. Each entry
159 graft two disconnected series of history together. Each entry
160 contains a key, followed by a space, followed by one or two
160 contains a key, followed by a space, followed by one or two
161 comma-separated values::
161 comma-separated values::
162
162
163 key parent1, parent2
163 key parent1, parent2
164
164
165 The key is the revision ID in the source
165 The key is the revision ID in the source
166 revision control system whose parents should be modified (same
166 revision control system whose parents should be modified (same
167 format as a key in .hg/shamap). The values are the revision IDs
167 format as a key in .hg/shamap). The values are the revision IDs
168 (in either the source or destination revision control system) that
168 (in either the source or destination revision control system) that
169 should be used as the new parents for that node. For example, if
169 should be used as the new parents for that node. For example, if
170 you have merged "release-1.0" into "trunk", then you should
170 you have merged "release-1.0" into "trunk", then you should
171 specify the revision on "trunk" as the first parent and the one on
171 specify the revision on "trunk" as the first parent and the one on
172 the "release-1.0" branch as the second.
172 the "release-1.0" branch as the second.
173
173
174 The branchmap is a file that allows you to rename a branch when it is
174 The branchmap is a file that allows you to rename a branch when it is
175 being brought in from whatever external repository. When used in
175 being brought in from whatever external repository. When used in
176 conjunction with a splicemap, it allows for a powerful combination
176 conjunction with a splicemap, it allows for a powerful combination
177 to help fix even the most badly mismanaged repositories and turn them
177 to help fix even the most badly mismanaged repositories and turn them
178 into nicely structured Mercurial repositories. The branchmap contains
178 into nicely structured Mercurial repositories. The branchmap contains
179 lines of the form::
179 lines of the form::
180
180
181 original_branch_name new_branch_name
181 original_branch_name new_branch_name
182
182
183 where "original_branch_name" is the name of the branch in the
183 where "original_branch_name" is the name of the branch in the
184 source repository, and "new_branch_name" is the name of the branch
184 source repository, and "new_branch_name" is the name of the branch
185 is the destination repository. No whitespace is allowed in the
185 is the destination repository. No whitespace is allowed in the
186 branch names. This can be used to (for instance) move code in one
186 branch names. This can be used to (for instance) move code in one
187 repository from "default" to a named branch.
187 repository from "default" to a named branch.
188
188
189 Mercurial Source
189 Mercurial Source
190 ################
190 ################
191
191
192 The Mercurial source recognizes the following configuration
192 The Mercurial source recognizes the following configuration
193 options, which you can set on the command line with ``--config``:
193 options, which you can set on the command line with ``--config``:
194
194
195 :convert.hg.ignoreerrors: ignore integrity errors when reading.
195 :convert.hg.ignoreerrors: ignore integrity errors when reading.
196 Use it to fix Mercurial repositories with missing revlogs, by
196 Use it to fix Mercurial repositories with missing revlogs, by
197 converting from and to Mercurial. Default is False.
197 converting from and to Mercurial. Default is False.
198
198
199 :convert.hg.saverev: store original revision ID in changeset
199 :convert.hg.saverev: store original revision ID in changeset
200 (forces target IDs to change). It takes a boolean argument and
200 (forces target IDs to change). It takes a boolean argument and
201 defaults to False.
201 defaults to False.
202
202
203 :convert.hg.startrev: specify the initial Mercurial revision.
203 :convert.hg.startrev: specify the initial Mercurial revision.
204 The default is 0.
204 The default is 0.
205
205
206 :convert.hg.revs: revset specifying the source revisions to convert.
206 :convert.hg.revs: revset specifying the source revisions to convert.
207
207
208 CVS Source
208 CVS Source
209 ##########
209 ##########
210
210
211 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
211 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
212 to indicate the starting point of what will be converted. Direct
212 to indicate the starting point of what will be converted. Direct
213 access to the repository files is not needed, unless of course the
213 access to the repository files is not needed, unless of course the
214 repository is ``:local:``. The conversion uses the top level
214 repository is ``:local:``. The conversion uses the top level
215 directory in the sandbox to find the CVS repository, and then uses
215 directory in the sandbox to find the CVS repository, and then uses
216 CVS rlog commands to find files to convert. This means that unless
216 CVS rlog commands to find files to convert. This means that unless
217 a filemap is given, all files under the starting directory will be
217 a filemap is given, all files under the starting directory will be
218 converted, and that any directory reorganization in the CVS
218 converted, and that any directory reorganization in the CVS
219 sandbox is ignored.
219 sandbox is ignored.
220
220
221 The following options can be used with ``--config``:
221 The following options can be used with ``--config``:
222
222
223 :convert.cvsps.cache: Set to False to disable remote log caching,
223 :convert.cvsps.cache: Set to False to disable remote log caching,
224 for testing and debugging purposes. Default is True.
224 for testing and debugging purposes. Default is True.
225
225
226 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
226 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
227 allowed between commits with identical user and log message in
227 allowed between commits with identical user and log message in
228 a single changeset. When very large files were checked in as
228 a single changeset. When very large files were checked in as
229 part of a changeset then the default may not be long enough.
229 part of a changeset then the default may not be long enough.
230 The default is 60.
230 The default is 60.
231
231
232 :convert.cvsps.mergeto: Specify a regular expression to which
232 :convert.cvsps.mergeto: Specify a regular expression to which
233 commit log messages are matched. If a match occurs, then the
233 commit log messages are matched. If a match occurs, then the
234 conversion process will insert a dummy revision merging the
234 conversion process will insert a dummy revision merging the
235 branch on which this log message occurs to the branch
235 branch on which this log message occurs to the branch
236 indicated in the regex. Default is ``{{mergetobranch
236 indicated in the regex. Default is ``{{mergetobranch
237 ([-\\w]+)}}``
237 ([-\\w]+)}}``
238
238
239 :convert.cvsps.mergefrom: Specify a regular expression to which
239 :convert.cvsps.mergefrom: Specify a regular expression to which
240 commit log messages are matched. If a match occurs, then the
240 commit log messages are matched. If a match occurs, then the
241 conversion process will add the most recent revision on the
241 conversion process will add the most recent revision on the
242 branch indicated in the regex as the second parent of the
242 branch indicated in the regex as the second parent of the
243 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
243 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
244
244
245 :convert.localtimezone: use local time (as determined by the TZ
245 :convert.localtimezone: use local time (as determined by the TZ
246 environment variable) for changeset date/times. The default
246 environment variable) for changeset date/times. The default
247 is False (use UTC).
247 is False (use UTC).
248
248
249 :hooks.cvslog: Specify a Python function to be called at the end of
249 :hooks.cvslog: Specify a Python function to be called at the end of
250 gathering the CVS log. The function is passed a list with the
250 gathering the CVS log. The function is passed a list with the
251 log entries, and can modify the entries in-place, or add or
251 log entries, and can modify the entries in-place, or add or
252 delete them.
252 delete them.
253
253
254 :hooks.cvschangesets: Specify a Python function to be called after
254 :hooks.cvschangesets: Specify a Python function to be called after
255 the changesets are calculated from the CVS log. The
255 the changesets are calculated from the CVS log. The
256 function is passed a list with the changeset entries, and can
256 function is passed a list with the changeset entries, and can
257 modify the changesets in-place, or add or delete them.
257 modify the changesets in-place, or add or delete them.
258
258
259 An additional "debugcvsps" Mercurial command allows the builtin
259 An additional "debugcvsps" Mercurial command allows the builtin
260 changeset merging code to be run without doing a conversion. Its
260 changeset merging code to be run without doing a conversion. Its
261 parameters and output are similar to that of cvsps 2.1. Please see
261 parameters and output are similar to that of cvsps 2.1. Please see
262 the command help for more details.
262 the command help for more details.
263
263
264 Subversion Source
264 Subversion Source
265 #################
265 #################
266
266
267 Subversion source detects classical trunk/branches/tags layouts.
267 Subversion source detects classical trunk/branches/tags layouts.
268 By default, the supplied ``svn://repo/path/`` source URL is
268 By default, the supplied ``svn://repo/path/`` source URL is
269 converted as a single branch. If ``svn://repo/path/trunk`` exists
269 converted as a single branch. If ``svn://repo/path/trunk`` exists
270 it replaces the default branch. If ``svn://repo/path/branches``
270 it replaces the default branch. If ``svn://repo/path/branches``
271 exists, its subdirectories are listed as possible branches. If
271 exists, its subdirectories are listed as possible branches. If
272 ``svn://repo/path/tags`` exists, it is looked for tags referencing
272 ``svn://repo/path/tags`` exists, it is looked for tags referencing
273 converted branches. Default ``trunk``, ``branches`` and ``tags``
273 converted branches. Default ``trunk``, ``branches`` and ``tags``
274 values can be overridden with following options. Set them to paths
274 values can be overridden with following options. Set them to paths
275 relative to the source URL, or leave them blank to disable auto
275 relative to the source URL, or leave them blank to disable auto
276 detection.
276 detection.
277
277
278 The following options can be set with ``--config``:
278 The following options can be set with ``--config``:
279
279
280 :convert.svn.branches: specify the directory containing branches.
280 :convert.svn.branches: specify the directory containing branches.
281 The default is ``branches``.
281 The default is ``branches``.
282
282
283 :convert.svn.tags: specify the directory containing tags. The
283 :convert.svn.tags: specify the directory containing tags. The
284 default is ``tags``.
284 default is ``tags``.
285
285
286 :convert.svn.trunk: specify the name of the trunk branch. The
286 :convert.svn.trunk: specify the name of the trunk branch. The
287 default is ``trunk``.
287 default is ``trunk``.
288
288
289 :convert.localtimezone: use local time (as determined by the TZ
289 :convert.localtimezone: use local time (as determined by the TZ
290 environment variable) for changeset date/times. The default
290 environment variable) for changeset date/times. The default
291 is False (use UTC).
291 is False (use UTC).
292
292
293 Source history can be retrieved starting at a specific revision,
293 Source history can be retrieved starting at a specific revision,
294 instead of being integrally converted. Only single branch
294 instead of being integrally converted. Only single branch
295 conversions are supported.
295 conversions are supported.
296
296
297 :convert.svn.startrev: specify start Subversion revision number.
297 :convert.svn.startrev: specify start Subversion revision number.
298 The default is 0.
298 The default is 0.
299
299
300 Git Source
300 Git Source
301 ##########
301 ##########
302
302
303 The Git importer converts commits from all reachable branches (refs
303 The Git importer converts commits from all reachable branches (refs
304 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
304 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
305 Branches are converted to bookmarks with the same name, with the
305 Branches are converted to bookmarks with the same name, with the
306 leading 'refs/heads' stripped. Git submodules are converted to Git
306 leading 'refs/heads' stripped. Git submodules are converted to Git
307 subrepos in Mercurial.
307 subrepos in Mercurial.
308
308
309 The following options can be set with ``--config``:
309 The following options can be set with ``--config``:
310
310
311 :convert.git.similarity: specify how similar files modified in a
311 :convert.git.similarity: specify how similar files modified in a
312 commit must be to be imported as renames or copies, as a
312 commit must be to be imported as renames or copies, as a
313 percentage between ``0`` (disabled) and ``100`` (files must be
313 percentage between ``0`` (disabled) and ``100`` (files must be
314 identical). For example, ``90`` means that a delete/add pair will
314 identical). For example, ``90`` means that a delete/add pair will
315 be imported as a rename if more than 90% of the file hasn't
315 be imported as a rename if more than 90% of the file hasn't
316 changed. The default is ``50``.
316 changed. The default is ``50``.
317
317
318 :convert.git.findcopiesharder: while detecting copies, look at all
318 :convert.git.findcopiesharder: while detecting copies, look at all
319 files in the working copy instead of just changed ones. This
319 files in the working copy instead of just changed ones. This
320 is very expensive for large projects, and is only effective when
320 is very expensive for large projects, and is only effective when
321 ``convert.git.similarity`` is greater than 0. The default is False.
321 ``convert.git.similarity`` is greater than 0. The default is False.
322
322
323 :convert.git.remoteprefix: remote refs are converted as bookmarks with
323 :convert.git.remoteprefix: remote refs are converted as bookmarks with
324 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
324 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
325 is 'remote'.
325 is 'remote'.
326
326
327 :convert.git.skipsubmodules: does not convert root level .gitmodules files
327 :convert.git.skipsubmodules: does not convert root level .gitmodules files
328 or files with 160000 mode indicating a submodule. Default is False.
328 or files with 160000 mode indicating a submodule. Default is False.
329
329
330 Perforce Source
330 Perforce Source
331 ###############
331 ###############
332
332
333 The Perforce (P4) importer can be given a p4 depot path or a
333 The Perforce (P4) importer can be given a p4 depot path or a
334 client specification as source. It will convert all files in the
334 client specification as source. It will convert all files in the
335 source to a flat Mercurial repository, ignoring labels, branches
335 source to a flat Mercurial repository, ignoring labels, branches
336 and integrations. Note that when a depot path is given you then
336 and integrations. Note that when a depot path is given you then
337 usually should specify a target directory, because otherwise the
337 usually should specify a target directory, because otherwise the
338 target may be named ``...-hg``.
338 target may be named ``...-hg``.
339
339
340 The following options can be set with ``--config``:
340 The following options can be set with ``--config``:
341
341
342 :convert.p4.encoding: specify the encoding to use when decoding standard
342 :convert.p4.encoding: specify the encoding to use when decoding standard
343 output of the Perforce command line tool. The default is default system
343 output of the Perforce command line tool. The default is default system
344 encoding.
344 encoding.
345
345
346 :convert.p4.startrev: specify initial Perforce revision (a
346 :convert.p4.startrev: specify initial Perforce revision (a
347 Perforce changelist number).
347 Perforce changelist number).
348
348
349 Mercurial Destination
349 Mercurial Destination
350 #####################
350 #####################
351
351
352 The Mercurial destination will recognize Mercurial subrepositories in the
352 The Mercurial destination will recognize Mercurial subrepositories in the
353 destination directory, and update the .hgsubstate file automatically if the
353 destination directory, and update the .hgsubstate file automatically if the
354 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
354 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
355 Converting a repository with subrepositories requires converting a single
355 Converting a repository with subrepositories requires converting a single
356 repository at a time, from the bottom up.
356 repository at a time, from the bottom up.
357
357
358 .. container:: verbose
358 .. container:: verbose
359
359
360 An example showing how to convert a repository with subrepositories::
360 An example showing how to convert a repository with subrepositories::
361
361
362 # so convert knows the type when it sees a non empty destination
362 # so convert knows the type when it sees a non empty destination
363 $ hg init converted
363 $ hg init converted
364
364
365 $ hg convert orig/sub1 converted/sub1
365 $ hg convert orig/sub1 converted/sub1
366 $ hg convert orig/sub2 converted/sub2
366 $ hg convert orig/sub2 converted/sub2
367 $ hg convert orig converted
367 $ hg convert orig converted
368
368
369 The following options are supported:
369 The following options are supported:
370
370
371 :convert.hg.clonebranches: dispatch source branches in separate
371 :convert.hg.clonebranches: dispatch source branches in separate
372 clones. The default is False.
372 clones. The default is False.
373
373
374 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
374 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
375 ``default``.
375 ``default``.
376
376
377 :convert.hg.usebranchnames: preserve branch names. The default is
377 :convert.hg.usebranchnames: preserve branch names. The default is
378 True.
378 True.
379
379
380 :convert.hg.sourcename: records the given string as a 'convert_source' extra
380 :convert.hg.sourcename: records the given string as a 'convert_source' extra
381 value on each commit made in the target repository. The default is None.
381 value on each commit made in the target repository. The default is None.
382
382
383 All Destinations
383 All Destinations
384 ################
384 ################
385
385
386 All destination types accept the following options:
386 All destination types accept the following options:
387
387
388 :convert.skiptags: does not convert tags from the source repo to the target
388 :convert.skiptags: does not convert tags from the source repo to the target
389 repo. The default is False.
389 repo. The default is False.
390 """
390 """
391 return convcmd.convert(ui, src, dest, revmapfile, **opts)
391 return convcmd.convert(ui, src, dest, revmapfile, **opts)
392
392
393 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
393 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
394 def debugsvnlog(ui, **opts):
394 def debugsvnlog(ui, **opts):
395 return subversion.debugsvnlog(ui, **opts)
395 return subversion.debugsvnlog(ui, **opts)
396
396
397 @command('debugcvsps',
397 @command('debugcvsps',
398 [
398 [
399 # Main options shared with cvsps-2.1
399 # Main options shared with cvsps-2.1
400 ('b', 'branches', [], _('only return changes on specified branches')),
400 ('b', 'branches', [], _('only return changes on specified branches')),
401 ('p', 'prefix', '', _('prefix to remove from file names')),
401 ('p', 'prefix', '', _('prefix to remove from file names')),
402 ('r', 'revisions', [],
402 ('r', 'revisions', [],
403 _('only return changes after or between specified tags')),
403 _('only return changes after or between specified tags')),
404 ('u', 'update-cache', None, _("update cvs log cache")),
404 ('u', 'update-cache', None, _("update cvs log cache")),
405 ('x', 'new-cache', None, _("create new cvs log cache")),
405 ('x', 'new-cache', None, _("create new cvs log cache")),
406 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
406 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
407 ('', 'root', '', _('specify cvsroot')),
407 ('', 'root', '', _('specify cvsroot')),
408 # Options specific to builtin cvsps
408 # Options specific to builtin cvsps
409 ('', 'parents', '', _('show parent changesets')),
409 ('', 'parents', '', _('show parent changesets')),
410 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
410 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
411 # Options that are ignored for compatibility with cvsps-2.1
411 # Options that are ignored for compatibility with cvsps-2.1
412 ('A', 'cvs-direct', None, _('ignored for compatibility')),
412 ('A', 'cvs-direct', None, _('ignored for compatibility')),
413 ],
413 ],
414 _('hg debugcvsps [OPTION]... [PATH]...'),
414 _('hg debugcvsps [OPTION]... [PATH]...'),
415 norepo=True)
415 norepo=True)
416 def debugcvsps(ui, *args, **opts):
416 def debugcvsps(ui, *args, **opts):
417 '''create changeset information from CVS
417 '''create changeset information from CVS
418
418
419 This command is intended as a debugging tool for the CVS to
419 This command is intended as a debugging tool for the CVS to
420 Mercurial converter, and can be used as a direct replacement for
420 Mercurial converter, and can be used as a direct replacement for
421 cvsps.
421 cvsps.
422
422
423 Hg debugcvsps reads the CVS rlog for current directory (or any
423 Hg debugcvsps reads the CVS rlog for current directory (or any
424 named directory) in the CVS repository, and converts the log to a
424 named directory) in the CVS repository, and converts the log to a
425 series of changesets based on matching commit log entries and
425 series of changesets based on matching commit log entries and
426 dates.'''
426 dates.'''
427 return cvsps.debugcvsps(ui, *args, **opts)
427 return cvsps.debugcvsps(ui, *args, **opts)
428
428
429 def kwconverted(ctx, name):
429 def kwconverted(ctx, name):
430 rev = ctx.extra().get('convert_revision', '')
430 rev = ctx.extra().get('convert_revision', '')
431 if rev.startswith('svn:'):
431 if rev.startswith('svn:'):
432 if name == 'svnrev':
432 if name == 'svnrev':
433 return str(subversion.revsplit(rev)[2])
433 return str(subversion.revsplit(rev)[2])
434 elif name == 'svnpath':
434 elif name == 'svnpath':
435 return subversion.revsplit(rev)[1]
435 return subversion.revsplit(rev)[1]
436 elif name == 'svnuuid':
436 elif name == 'svnuuid':
437 return subversion.revsplit(rev)[0]
437 return subversion.revsplit(rev)[0]
438 return rev
438 return rev
439
439
440 templatekeyword = registrar.templatekeyword()
440 templatekeyword = registrar.templatekeyword()
441
441
442 @templatekeyword('svnrev')
442 @templatekeyword('svnrev')
443 def kwsvnrev(repo, ctx, **args):
443 def kwsvnrev(repo, ctx, **args):
444 """String. Converted subversion revision number."""
444 """String. Converted subversion revision number."""
445 return kwconverted(ctx, 'svnrev')
445 return kwconverted(ctx, 'svnrev')
446
446
447 @templatekeyword('svnpath')
447 @templatekeyword('svnpath')
448 def kwsvnpath(repo, ctx, **args):
448 def kwsvnpath(repo, ctx, **args):
449 """String. Converted subversion revision project path."""
449 """String. Converted subversion revision project path."""
450 return kwconverted(ctx, 'svnpath')
450 return kwconverted(ctx, 'svnpath')
451
451
452 @templatekeyword('svnuuid')
452 @templatekeyword('svnuuid')
453 def kwsvnuuid(repo, ctx, **args):
453 def kwsvnuuid(repo, ctx, **args):
454 """String. Converted subversion revision repository identifier."""
454 """String. Converted subversion revision repository identifier."""
455 return kwconverted(ctx, 'svnuuid')
455 return kwconverted(ctx, 'svnuuid')
456
456
457 # tell hggettext to extract docstrings from these functions:
457 # tell hggettext to extract docstrings from these functions:
458 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
458 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,1354 +1,1354 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import os
6 import os
7 import re
7 import re
8 import sys
8 import sys
9 import tempfile
9 import tempfile
10 import xml.dom.minidom
10 import xml.dom.minidom
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 encoding,
15 error,
15 error,
16 scmutil,
16 scmutil,
17 strutil,
17 strutil,
18 util,
18 util,
19 )
19 )
20
20
21 from . import common
21 from . import common
22
22
23 pickle = util.pickle
23 pickle = util.pickle
24 stringio = util.stringio
24 stringio = util.stringio
25 propertycache = util.propertycache
25 propertycache = util.propertycache
26 urlerr = util.urlerr
26 urlerr = util.urlerr
27 urlreq = util.urlreq
27 urlreq = util.urlreq
28
28
29 commandline = common.commandline
29 commandline = common.commandline
30 commit = common.commit
30 commit = common.commit
31 converter_sink = common.converter_sink
31 converter_sink = common.converter_sink
32 converter_source = common.converter_source
32 converter_source = common.converter_source
33 decodeargs = common.decodeargs
33 decodeargs = common.decodeargs
34 encodeargs = common.encodeargs
34 encodeargs = common.encodeargs
35 makedatetimestamp = common.makedatetimestamp
35 makedatetimestamp = common.makedatetimestamp
36 mapfile = common.mapfile
36 mapfile = common.mapfile
37 MissingTool = common.MissingTool
37 MissingTool = common.MissingTool
38 NoRepo = common.NoRepo
38 NoRepo = common.NoRepo
39
39
40 # Subversion stuff. Works best with very recent Python SVN bindings
40 # Subversion stuff. Works best with very recent Python SVN bindings
41 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
41 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
42 # these bindings.
42 # these bindings.
43
43
44 try:
44 try:
45 import svn
45 import svn
46 import svn.client
46 import svn.client
47 import svn.core
47 import svn.core
48 import svn.ra
48 import svn.ra
49 import svn.delta
49 import svn.delta
50 from . import transport
50 from . import transport
51 import warnings
51 import warnings
52 warnings.filterwarnings('ignore',
52 warnings.filterwarnings('ignore',
53 module='svn.core',
53 module='svn.core',
54 category=DeprecationWarning)
54 category=DeprecationWarning)
55 svn.core.SubversionException # trigger import to catch error
55 svn.core.SubversionException # trigger import to catch error
56
56
57 except ImportError:
57 except ImportError:
58 svn = None
58 svn = None
59
59
60 class SvnPathNotFound(Exception):
60 class SvnPathNotFound(Exception):
61 pass
61 pass
62
62
63 def revsplit(rev):
63 def revsplit(rev):
64 """Parse a revision string and return (uuid, path, revnum).
64 """Parse a revision string and return (uuid, path, revnum).
65 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
65 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
66 ... '/proj%20B/mytrunk/mytrunk@1')
66 ... '/proj%20B/mytrunk/mytrunk@1')
67 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
67 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
68 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
68 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
69 ('', '', 1)
69 ('', '', 1)
70 >>> revsplit('@7')
70 >>> revsplit('@7')
71 ('', '', 7)
71 ('', '', 7)
72 >>> revsplit('7')
72 >>> revsplit('7')
73 ('', '', 0)
73 ('', '', 0)
74 >>> revsplit('bad')
74 >>> revsplit('bad')
75 ('', '', 0)
75 ('', '', 0)
76 """
76 """
77 parts = rev.rsplit('@', 1)
77 parts = rev.rsplit('@', 1)
78 revnum = 0
78 revnum = 0
79 if len(parts) > 1:
79 if len(parts) > 1:
80 revnum = int(parts[1])
80 revnum = int(parts[1])
81 parts = parts[0].split('/', 1)
81 parts = parts[0].split('/', 1)
82 uuid = ''
82 uuid = ''
83 mod = ''
83 mod = ''
84 if len(parts) > 1 and parts[0].startswith('svn:'):
84 if len(parts) > 1 and parts[0].startswith('svn:'):
85 uuid = parts[0][4:]
85 uuid = parts[0][4:]
86 mod = '/' + parts[1]
86 mod = '/' + parts[1]
87 return uuid, mod, revnum
87 return uuid, mod, revnum
88
88
89 def quote(s):
89 def quote(s):
90 # As of svn 1.7, many svn calls expect "canonical" paths. In
90 # As of svn 1.7, many svn calls expect "canonical" paths. In
91 # theory, we should call svn.core.*canonicalize() on all paths
91 # theory, we should call svn.core.*canonicalize() on all paths
92 # before passing them to the API. Instead, we assume the base url
92 # before passing them to the API. Instead, we assume the base url
93 # is canonical and copy the behaviour of svn URL encoding function
93 # is canonical and copy the behaviour of svn URL encoding function
94 # so we can extend it safely with new components. The "safe"
94 # so we can extend it safely with new components. The "safe"
95 # characters were taken from the "svn_uri__char_validity" table in
95 # characters were taken from the "svn_uri__char_validity" table in
96 # libsvn_subr/path.c.
96 # libsvn_subr/path.c.
97 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
97 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
98
98
99 def geturl(path):
99 def geturl(path):
100 try:
100 try:
101 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
101 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
102 except svn.core.SubversionException:
102 except svn.core.SubversionException:
103 # svn.client.url_from_path() fails with local repositories
103 # svn.client.url_from_path() fails with local repositories
104 pass
104 pass
105 if os.path.isdir(path):
105 if os.path.isdir(path):
106 path = os.path.normpath(os.path.abspath(path))
106 path = os.path.normpath(os.path.abspath(path))
107 if os.name == 'nt':
107 if os.name == 'nt':
108 path = '/' + util.normpath(path)
108 path = '/' + util.normpath(path)
109 # Module URL is later compared with the repository URL returned
109 # Module URL is later compared with the repository URL returned
110 # by svn API, which is UTF-8.
110 # by svn API, which is UTF-8.
111 path = encoding.tolocal(path)
111 path = encoding.tolocal(path)
112 path = 'file://%s' % quote(path)
112 path = 'file://%s' % quote(path)
113 return svn.core.svn_path_canonicalize(path)
113 return svn.core.svn_path_canonicalize(path)
114
114
115 def optrev(number):
115 def optrev(number):
116 optrev = svn.core.svn_opt_revision_t()
116 optrev = svn.core.svn_opt_revision_t()
117 optrev.kind = svn.core.svn_opt_revision_number
117 optrev.kind = svn.core.svn_opt_revision_number
118 optrev.value.number = number
118 optrev.value.number = number
119 return optrev
119 return optrev
120
120
121 class changedpath(object):
121 class changedpath(object):
122 def __init__(self, p):
122 def __init__(self, p):
123 self.copyfrom_path = p.copyfrom_path
123 self.copyfrom_path = p.copyfrom_path
124 self.copyfrom_rev = p.copyfrom_rev
124 self.copyfrom_rev = p.copyfrom_rev
125 self.action = p.action
125 self.action = p.action
126
126
127 def get_log_child(fp, url, paths, start, end, limit=0,
127 def get_log_child(fp, url, paths, start, end, limit=0,
128 discover_changed_paths=True, strict_node_history=False):
128 discover_changed_paths=True, strict_node_history=False):
129 protocol = -1
129 protocol = -1
130 def receiver(orig_paths, revnum, author, date, message, pool):
130 def receiver(orig_paths, revnum, author, date, message, pool):
131 paths = {}
131 paths = {}
132 if orig_paths is not None:
132 if orig_paths is not None:
133 for k, v in orig_paths.iteritems():
133 for k, v in orig_paths.iteritems():
134 paths[k] = changedpath(v)
134 paths[k] = changedpath(v)
135 pickle.dump((paths, revnum, author, date, message),
135 pickle.dump((paths, revnum, author, date, message),
136 fp, protocol)
136 fp, protocol)
137
137
138 try:
138 try:
139 # Use an ra of our own so that our parent can consume
139 # Use an ra of our own so that our parent can consume
140 # our results without confusing the server.
140 # our results without confusing the server.
141 t = transport.SvnRaTransport(url=url)
141 t = transport.SvnRaTransport(url=url)
142 svn.ra.get_log(t.ra, paths, start, end, limit,
142 svn.ra.get_log(t.ra, paths, start, end, limit,
143 discover_changed_paths,
143 discover_changed_paths,
144 strict_node_history,
144 strict_node_history,
145 receiver)
145 receiver)
146 except IOError:
146 except IOError:
147 # Caller may interrupt the iteration
147 # Caller may interrupt the iteration
148 pickle.dump(None, fp, protocol)
148 pickle.dump(None, fp, protocol)
149 except Exception as inst:
149 except Exception as inst:
150 pickle.dump(str(inst), fp, protocol)
150 pickle.dump(str(inst), fp, protocol)
151 else:
151 else:
152 pickle.dump(None, fp, protocol)
152 pickle.dump(None, fp, protocol)
153 fp.close()
153 fp.close()
154 # With large history, cleanup process goes crazy and suddenly
154 # With large history, cleanup process goes crazy and suddenly
155 # consumes *huge* amount of memory. The output file being closed,
155 # consumes *huge* amount of memory. The output file being closed,
156 # there is no need for clean termination.
156 # there is no need for clean termination.
157 os._exit(0)
157 os._exit(0)
158
158
159 def debugsvnlog(ui, **opts):
159 def debugsvnlog(ui, **opts):
160 """Fetch SVN log in a subprocess and channel them back to parent to
160 """Fetch SVN log in a subprocess and channel them back to parent to
161 avoid memory collection issues.
161 avoid memory collection issues.
162 """
162 """
163 if svn is None:
163 if svn is None:
164 raise error.Abort(_('debugsvnlog could not load Subversion python '
164 raise error.Abort(_('debugsvnlog could not load Subversion python '
165 'bindings'))
165 'bindings'))
166
166
167 util.setbinary(sys.stdin)
167 util.setbinary(sys.stdin)
168 util.setbinary(sys.stdout)
168 util.setbinary(sys.stdout)
169 args = decodeargs(sys.stdin.read())
169 args = decodeargs(sys.stdin.read())
170 get_log_child(sys.stdout, *args)
170 get_log_child(sys.stdout, *args)
171
171
172 class logstream(object):
172 class logstream(object):
173 """Interruptible revision log iterator."""
173 """Interruptible revision log iterator."""
174 def __init__(self, stdout):
174 def __init__(self, stdout):
175 self._stdout = stdout
175 self._stdout = stdout
176
176
177 def __iter__(self):
177 def __iter__(self):
178 while True:
178 while True:
179 try:
179 try:
180 entry = pickle.load(self._stdout)
180 entry = pickle.load(self._stdout)
181 except EOFError:
181 except EOFError:
182 raise error.Abort(_('Mercurial failed to run itself, check'
182 raise error.Abort(_('Mercurial failed to run itself, check'
183 ' hg executable is in PATH'))
183 ' hg executable is in PATH'))
184 try:
184 try:
185 orig_paths, revnum, author, date, message = entry
185 orig_paths, revnum, author, date, message = entry
186 except (TypeError, ValueError):
186 except (TypeError, ValueError):
187 if entry is None:
187 if entry is None:
188 break
188 break
189 raise error.Abort(_("log stream exception '%s'") % entry)
189 raise error.Abort(_("log stream exception '%s'") % entry)
190 yield entry
190 yield entry
191
191
192 def close(self):
192 def close(self):
193 if self._stdout:
193 if self._stdout:
194 self._stdout.close()
194 self._stdout.close()
195 self._stdout = None
195 self._stdout = None
196
196
197 class directlogstream(list):
197 class directlogstream(list):
198 """Direct revision log iterator.
198 """Direct revision log iterator.
199 This can be used for debugging and development but it will probably leak
199 This can be used for debugging and development but it will probably leak
200 memory and is not suitable for real conversions."""
200 memory and is not suitable for real conversions."""
201 def __init__(self, url, paths, start, end, limit=0,
201 def __init__(self, url, paths, start, end, limit=0,
202 discover_changed_paths=True, strict_node_history=False):
202 discover_changed_paths=True, strict_node_history=False):
203
203
204 def receiver(orig_paths, revnum, author, date, message, pool):
204 def receiver(orig_paths, revnum, author, date, message, pool):
205 paths = {}
205 paths = {}
206 if orig_paths is not None:
206 if orig_paths is not None:
207 for k, v in orig_paths.iteritems():
207 for k, v in orig_paths.iteritems():
208 paths[k] = changedpath(v)
208 paths[k] = changedpath(v)
209 self.append((paths, revnum, author, date, message))
209 self.append((paths, revnum, author, date, message))
210
210
211 # Use an ra of our own so that our parent can consume
211 # Use an ra of our own so that our parent can consume
212 # our results without confusing the server.
212 # our results without confusing the server.
213 t = transport.SvnRaTransport(url=url)
213 t = transport.SvnRaTransport(url=url)
214 svn.ra.get_log(t.ra, paths, start, end, limit,
214 svn.ra.get_log(t.ra, paths, start, end, limit,
215 discover_changed_paths,
215 discover_changed_paths,
216 strict_node_history,
216 strict_node_history,
217 receiver)
217 receiver)
218
218
219 def close(self):
219 def close(self):
220 pass
220 pass
221
221
222 # Check to see if the given path is a local Subversion repo. Verify this by
222 # Check to see if the given path is a local Subversion repo. Verify this by
223 # looking for several svn-specific files and directories in the given
223 # looking for several svn-specific files and directories in the given
224 # directory.
224 # directory.
225 def filecheck(ui, path, proto):
225 def filecheck(ui, path, proto):
226 for x in ('locks', 'hooks', 'format', 'db'):
226 for x in ('locks', 'hooks', 'format', 'db'):
227 if not os.path.exists(os.path.join(path, x)):
227 if not os.path.exists(os.path.join(path, x)):
228 return False
228 return False
229 return True
229 return True
230
230
231 # Check to see if a given path is the root of an svn repo over http. We verify
231 # Check to see if a given path is the root of an svn repo over http. We verify
232 # this by requesting a version-controlled URL we know can't exist and looking
232 # this by requesting a version-controlled URL we know can't exist and looking
233 # for the svn-specific "not found" XML.
233 # for the svn-specific "not found" XML.
234 def httpcheck(ui, path, proto):
234 def httpcheck(ui, path, proto):
235 try:
235 try:
236 opener = urlreq.buildopener()
236 opener = urlreq.buildopener()
237 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
237 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
238 data = rsp.read()
238 data = rsp.read()
239 except urlerr.httperror as inst:
239 except urlerr.httperror as inst:
240 if inst.code != 404:
240 if inst.code != 404:
241 # Except for 404 we cannot know for sure this is not an svn repo
241 # Except for 404 we cannot know for sure this is not an svn repo
242 ui.warn(_('svn: cannot probe remote repository, assume it could '
242 ui.warn(_('svn: cannot probe remote repository, assume it could '
243 'be a subversion repository. Use --source-type if you '
243 'be a subversion repository. Use --source-type if you '
244 'know better.\n'))
244 'know better.\n'))
245 return True
245 return True
246 data = inst.fp.read()
246 data = inst.fp.read()
247 except Exception:
247 except Exception:
248 # Could be urlerr.urlerror if the URL is invalid or anything else.
248 # Could be urlerr.urlerror if the URL is invalid or anything else.
249 return False
249 return False
250 return '<m:human-readable errcode="160013">' in data
250 return '<m:human-readable errcode="160013">' in data
251
251
252 protomap = {'http': httpcheck,
252 protomap = {'http': httpcheck,
253 'https': httpcheck,
253 'https': httpcheck,
254 'file': filecheck,
254 'file': filecheck,
255 }
255 }
256 def issvnurl(ui, url):
256 def issvnurl(ui, url):
257 try:
257 try:
258 proto, path = url.split('://', 1)
258 proto, path = url.split('://', 1)
259 if proto == 'file':
259 if proto == 'file':
260 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
260 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
261 and path[2:6].lower() == '%3a/'):
261 and path[2:6].lower() == '%3a/'):
262 path = path[:2] + ':/' + path[6:]
262 path = path[:2] + ':/' + path[6:]
263 path = urlreq.url2pathname(path)
263 path = urlreq.url2pathname(path)
264 except ValueError:
264 except ValueError:
265 proto = 'file'
265 proto = 'file'
266 path = os.path.abspath(url)
266 path = os.path.abspath(url)
267 if proto == 'file':
267 if proto == 'file':
268 path = util.pconvert(path)
268 path = util.pconvert(path)
269 check = protomap.get(proto, lambda *args: False)
269 check = protomap.get(proto, lambda *args: False)
270 while '/' in path:
270 while '/' in path:
271 if check(ui, path, proto):
271 if check(ui, path, proto):
272 return True
272 return True
273 path = path.rsplit('/', 1)[0]
273 path = path.rsplit('/', 1)[0]
274 return False
274 return False
275
275
276 # SVN conversion code stolen from bzr-svn and tailor
276 # SVN conversion code stolen from bzr-svn and tailor
277 #
277 #
278 # Subversion looks like a versioned filesystem, branches structures
278 # Subversion looks like a versioned filesystem, branches structures
279 # are defined by conventions and not enforced by the tool. First,
279 # are defined by conventions and not enforced by the tool. First,
280 # we define the potential branches (modules) as "trunk" and "branches"
280 # we define the potential branches (modules) as "trunk" and "branches"
281 # children directories. Revisions are then identified by their
281 # children directories. Revisions are then identified by their
282 # module and revision number (and a repository identifier).
282 # module and revision number (and a repository identifier).
283 #
283 #
284 # The revision graph is really a tree (or a forest). By default, a
284 # The revision graph is really a tree (or a forest). By default, a
285 # revision parent is the previous revision in the same module. If the
285 # revision parent is the previous revision in the same module. If the
286 # module directory is copied/moved from another module then the
286 # module directory is copied/moved from another module then the
287 # revision is the module root and its parent the source revision in
287 # revision is the module root and its parent the source revision in
288 # the parent module. A revision has at most one parent.
288 # the parent module. A revision has at most one parent.
289 #
289 #
290 class svn_source(converter_source):
290 class svn_source(converter_source):
291 def __init__(self, ui, url, revs=None):
291 def __init__(self, ui, url, revs=None):
292 super(svn_source, self).__init__(ui, url, revs=revs)
292 super(svn_source, self).__init__(ui, url, revs=revs)
293
293
294 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
294 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
295 (os.path.exists(url) and
295 (os.path.exists(url) and
296 os.path.exists(os.path.join(url, '.svn'))) or
296 os.path.exists(os.path.join(url, '.svn'))) or
297 issvnurl(ui, url)):
297 issvnurl(ui, url)):
298 raise NoRepo(_("%s does not look like a Subversion repository")
298 raise NoRepo(_("%s does not look like a Subversion repository")
299 % url)
299 % url)
300 if svn is None:
300 if svn is None:
301 raise MissingTool(_('could not load Subversion python bindings'))
301 raise MissingTool(_('could not load Subversion python bindings'))
302
302
303 try:
303 try:
304 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
304 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
305 if version < (1, 4):
305 if version < (1, 4):
306 raise MissingTool(_('Subversion python bindings %d.%d found, '
306 raise MissingTool(_('Subversion python bindings %d.%d found, '
307 '1.4 or later required') % version)
307 '1.4 or later required') % version)
308 except AttributeError:
308 except AttributeError:
309 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
309 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
310 'or later required'))
310 'or later required'))
311
311
312 self.lastrevs = {}
312 self.lastrevs = {}
313
313
314 latest = None
314 latest = None
315 try:
315 try:
316 # Support file://path@rev syntax. Useful e.g. to convert
316 # Support file://path@rev syntax. Useful e.g. to convert
317 # deleted branches.
317 # deleted branches.
318 at = url.rfind('@')
318 at = url.rfind('@')
319 if at >= 0:
319 if at >= 0:
320 latest = int(url[at + 1:])
320 latest = int(url[at + 1:])
321 url = url[:at]
321 url = url[:at]
322 except ValueError:
322 except ValueError:
323 pass
323 pass
324 self.url = geturl(url)
324 self.url = geturl(url)
325 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
325 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
326 try:
326 try:
327 self.transport = transport.SvnRaTransport(url=self.url)
327 self.transport = transport.SvnRaTransport(url=self.url)
328 self.ra = self.transport.ra
328 self.ra = self.transport.ra
329 self.ctx = self.transport.client
329 self.ctx = self.transport.client
330 self.baseurl = svn.ra.get_repos_root(self.ra)
330 self.baseurl = svn.ra.get_repos_root(self.ra)
331 # Module is either empty or a repository path starting with
331 # Module is either empty or a repository path starting with
332 # a slash and not ending with a slash.
332 # a slash and not ending with a slash.
333 self.module = urlreq.unquote(self.url[len(self.baseurl):])
333 self.module = urlreq.unquote(self.url[len(self.baseurl):])
334 self.prevmodule = None
334 self.prevmodule = None
335 self.rootmodule = self.module
335 self.rootmodule = self.module
336 self.commits = {}
336 self.commits = {}
337 self.paths = {}
337 self.paths = {}
338 self.uuid = svn.ra.get_uuid(self.ra)
338 self.uuid = svn.ra.get_uuid(self.ra)
339 except svn.core.SubversionException:
339 except svn.core.SubversionException:
340 ui.traceback()
340 ui.traceback()
341 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
341 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
342 svn.core.SVN_VER_MINOR,
342 svn.core.SVN_VER_MINOR,
343 svn.core.SVN_VER_MICRO)
343 svn.core.SVN_VER_MICRO)
344 raise NoRepo(_("%s does not look like a Subversion repository "
344 raise NoRepo(_("%s does not look like a Subversion repository "
345 "to libsvn version %s")
345 "to libsvn version %s")
346 % (self.url, svnversion))
346 % (self.url, svnversion))
347
347
348 if revs:
348 if revs:
349 if len(revs) > 1:
349 if len(revs) > 1:
350 raise error.Abort(_('subversion source does not support '
350 raise error.Abort(_('subversion source does not support '
351 'specifying multiple revisions'))
351 'specifying multiple revisions'))
352 try:
352 try:
353 latest = int(revs[0])
353 latest = int(revs[0])
354 except ValueError:
354 except ValueError:
355 raise error.Abort(_('svn: revision %s is not an integer') %
355 raise error.Abort(_('svn: revision %s is not an integer') %
356 revs[0])
356 revs[0])
357
357
358 self.trunkname = self.ui.config('convert', 'svn.trunk',
358 self.trunkname = self.ui.config('convert', 'svn.trunk',
359 'trunk').strip('/')
359 'trunk').strip('/')
360 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
360 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
361 try:
361 try:
362 self.startrev = int(self.startrev)
362 self.startrev = int(self.startrev)
363 if self.startrev < 0:
363 if self.startrev < 0:
364 self.startrev = 0
364 self.startrev = 0
365 except ValueError:
365 except ValueError:
366 raise error.Abort(_('svn: start revision %s is not an integer')
366 raise error.Abort(_('svn: start revision %s is not an integer')
367 % self.startrev)
367 % self.startrev)
368
368
369 try:
369 try:
370 self.head = self.latest(self.module, latest)
370 self.head = self.latest(self.module, latest)
371 except SvnPathNotFound:
371 except SvnPathNotFound:
372 self.head = None
372 self.head = None
373 if not self.head:
373 if not self.head:
374 raise error.Abort(_('no revision found in module %s')
374 raise error.Abort(_('no revision found in module %s')
375 % self.module)
375 % self.module)
376 self.last_changed = self.revnum(self.head)
376 self.last_changed = self.revnum(self.head)
377
377
378 self._changescache = (None, None)
378 self._changescache = (None, None)
379
379
380 if os.path.exists(os.path.join(url, '.svn/entries')):
380 if os.path.exists(os.path.join(url, '.svn/entries')):
381 self.wc = url
381 self.wc = url
382 else:
382 else:
383 self.wc = None
383 self.wc = None
384 self.convertfp = None
384 self.convertfp = None
385
385
386 def setrevmap(self, revmap):
386 def setrevmap(self, revmap):
387 lastrevs = {}
387 lastrevs = {}
388 for revid in revmap.iterkeys():
388 for revid in revmap.iterkeys():
389 uuid, module, revnum = revsplit(revid)
389 uuid, module, revnum = revsplit(revid)
390 lastrevnum = lastrevs.setdefault(module, revnum)
390 lastrevnum = lastrevs.setdefault(module, revnum)
391 if revnum > lastrevnum:
391 if revnum > lastrevnum:
392 lastrevs[module] = revnum
392 lastrevs[module] = revnum
393 self.lastrevs = lastrevs
393 self.lastrevs = lastrevs
394
394
395 def exists(self, path, optrev):
395 def exists(self, path, optrev):
396 try:
396 try:
397 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
397 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
398 optrev, False, self.ctx)
398 optrev, False, self.ctx)
399 return True
399 return True
400 except svn.core.SubversionException:
400 except svn.core.SubversionException:
401 return False
401 return False
402
402
403 def getheads(self):
403 def getheads(self):
404
404
405 def isdir(path, revnum):
405 def isdir(path, revnum):
406 kind = self._checkpath(path, revnum)
406 kind = self._checkpath(path, revnum)
407 return kind == svn.core.svn_node_dir
407 return kind == svn.core.svn_node_dir
408
408
409 def getcfgpath(name, rev):
409 def getcfgpath(name, rev):
410 cfgpath = self.ui.config('convert', 'svn.' + name)
410 cfgpath = self.ui.config('convert', 'svn.' + name)
411 if cfgpath is not None and cfgpath.strip() == '':
411 if cfgpath is not None and cfgpath.strip() == '':
412 return None
412 return None
413 path = (cfgpath or name).strip('/')
413 path = (cfgpath or name).strip('/')
414 if not self.exists(path, rev):
414 if not self.exists(path, rev):
415 if self.module.endswith(path) and name == 'trunk':
415 if self.module.endswith(path) and name == 'trunk':
416 # we are converting from inside this directory
416 # we are converting from inside this directory
417 return None
417 return None
418 if cfgpath:
418 if cfgpath:
419 raise error.Abort(_('expected %s to be at %r, but not found'
419 raise error.Abort(_('expected %s to be at %r, but not found'
420 ) % (name, path))
420 ) % (name, path))
421 return None
421 return None
422 self.ui.note(_('found %s at %r\n') % (name, path))
422 self.ui.note(_('found %s at %r\n') % (name, path))
423 return path
423 return path
424
424
425 rev = optrev(self.last_changed)
425 rev = optrev(self.last_changed)
426 oldmodule = ''
426 oldmodule = ''
427 trunk = getcfgpath('trunk', rev)
427 trunk = getcfgpath('trunk', rev)
428 self.tags = getcfgpath('tags', rev)
428 self.tags = getcfgpath('tags', rev)
429 branches = getcfgpath('branches', rev)
429 branches = getcfgpath('branches', rev)
430
430
431 # If the project has a trunk or branches, we will extract heads
431 # If the project has a trunk or branches, we will extract heads
432 # from them. We keep the project root otherwise.
432 # from them. We keep the project root otherwise.
433 if trunk:
433 if trunk:
434 oldmodule = self.module or ''
434 oldmodule = self.module or ''
435 self.module += '/' + trunk
435 self.module += '/' + trunk
436 self.head = self.latest(self.module, self.last_changed)
436 self.head = self.latest(self.module, self.last_changed)
437 if not self.head:
437 if not self.head:
438 raise error.Abort(_('no revision found in module %s')
438 raise error.Abort(_('no revision found in module %s')
439 % self.module)
439 % self.module)
440
440
441 # First head in the list is the module's head
441 # First head in the list is the module's head
442 self.heads = [self.head]
442 self.heads = [self.head]
443 if self.tags is not None:
443 if self.tags is not None:
444 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
444 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
445
445
446 # Check if branches bring a few more heads to the list
446 # Check if branches bring a few more heads to the list
447 if branches:
447 if branches:
448 rpath = self.url.strip('/')
448 rpath = self.url.strip('/')
449 branchnames = svn.client.ls(rpath + '/' + quote(branches),
449 branchnames = svn.client.ls(rpath + '/' + quote(branches),
450 rev, False, self.ctx)
450 rev, False, self.ctx)
451 for branch in sorted(branchnames):
451 for branch in sorted(branchnames):
452 module = '%s/%s/%s' % (oldmodule, branches, branch)
452 module = '%s/%s/%s' % (oldmodule, branches, branch)
453 if not isdir(module, self.last_changed):
453 if not isdir(module, self.last_changed):
454 continue
454 continue
455 brevid = self.latest(module, self.last_changed)
455 brevid = self.latest(module, self.last_changed)
456 if not brevid:
456 if not brevid:
457 self.ui.note(_('ignoring empty branch %s\n') % branch)
457 self.ui.note(_('ignoring empty branch %s\n') % branch)
458 continue
458 continue
459 self.ui.note(_('found branch %s at %d\n') %
459 self.ui.note(_('found branch %s at %d\n') %
460 (branch, self.revnum(brevid)))
460 (branch, self.revnum(brevid)))
461 self.heads.append(brevid)
461 self.heads.append(brevid)
462
462
463 if self.startrev and self.heads:
463 if self.startrev and self.heads:
464 if len(self.heads) > 1:
464 if len(self.heads) > 1:
465 raise error.Abort(_('svn: start revision is not supported '
465 raise error.Abort(_('svn: start revision is not supported '
466 'with more than one branch'))
466 'with more than one branch'))
467 revnum = self.revnum(self.heads[0])
467 revnum = self.revnum(self.heads[0])
468 if revnum < self.startrev:
468 if revnum < self.startrev:
469 raise error.Abort(
469 raise error.Abort(
470 _('svn: no revision found after start revision %d')
470 _('svn: no revision found after start revision %d')
471 % self.startrev)
471 % self.startrev)
472
472
473 return self.heads
473 return self.heads
474
474
475 def _getchanges(self, rev, full):
475 def _getchanges(self, rev, full):
476 (paths, parents) = self.paths[rev]
476 (paths, parents) = self.paths[rev]
477 copies = {}
477 copies = {}
478 if parents:
478 if parents:
479 files, self.removed, copies = self.expandpaths(rev, paths, parents)
479 files, self.removed, copies = self.expandpaths(rev, paths, parents)
480 if full or not parents:
480 if full or not parents:
481 # Perform a full checkout on roots
481 # Perform a full checkout on roots
482 uuid, module, revnum = revsplit(rev)
482 uuid, module, revnum = revsplit(rev)
483 entries = svn.client.ls(self.baseurl + quote(module),
483 entries = svn.client.ls(self.baseurl + quote(module),
484 optrev(revnum), True, self.ctx)
484 optrev(revnum), True, self.ctx)
485 files = [n for n, e in entries.iteritems()
485 files = [n for n, e in entries.iteritems()
486 if e.kind == svn.core.svn_node_file]
486 if e.kind == svn.core.svn_node_file]
487 self.removed = set()
487 self.removed = set()
488
488
489 files.sort()
489 files.sort()
490 files = zip(files, [rev] * len(files))
490 files = zip(files, [rev] * len(files))
491 return (files, copies)
491 return (files, copies)
492
492
493 def getchanges(self, rev, full):
493 def getchanges(self, rev, full):
494 # reuse cache from getchangedfiles
494 # reuse cache from getchangedfiles
495 if self._changescache[0] == rev and not full:
495 if self._changescache[0] == rev and not full:
496 (files, copies) = self._changescache[1]
496 (files, copies) = self._changescache[1]
497 else:
497 else:
498 (files, copies) = self._getchanges(rev, full)
498 (files, copies) = self._getchanges(rev, full)
499 # caller caches the result, so free it here to release memory
499 # caller caches the result, so free it here to release memory
500 del self.paths[rev]
500 del self.paths[rev]
501 return (files, copies, set())
501 return (files, copies, set())
502
502
503 def getchangedfiles(self, rev, i):
503 def getchangedfiles(self, rev, i):
504 # called from filemap - cache computed values for reuse in getchanges
504 # called from filemap - cache computed values for reuse in getchanges
505 (files, copies) = self._getchanges(rev, False)
505 (files, copies) = self._getchanges(rev, False)
506 self._changescache = (rev, (files, copies))
506 self._changescache = (rev, (files, copies))
507 return [f[0] for f in files]
507 return [f[0] for f in files]
508
508
509 def getcommit(self, rev):
509 def getcommit(self, rev):
510 if rev not in self.commits:
510 if rev not in self.commits:
511 uuid, module, revnum = revsplit(rev)
511 uuid, module, revnum = revsplit(rev)
512 self.module = module
512 self.module = module
513 self.reparent(module)
513 self.reparent(module)
514 # We assume that:
514 # We assume that:
515 # - requests for revisions after "stop" come from the
515 # - requests for revisions after "stop" come from the
516 # revision graph backward traversal. Cache all of them
516 # revision graph backward traversal. Cache all of them
517 # down to stop, they will be used eventually.
517 # down to stop, they will be used eventually.
518 # - requests for revisions before "stop" come to get
518 # - requests for revisions before "stop" come to get
519 # isolated branches parents. Just fetch what is needed.
519 # isolated branches parents. Just fetch what is needed.
520 stop = self.lastrevs.get(module, 0)
520 stop = self.lastrevs.get(module, 0)
521 if revnum < stop:
521 if revnum < stop:
522 stop = revnum + 1
522 stop = revnum + 1
523 self._fetch_revisions(revnum, stop)
523 self._fetch_revisions(revnum, stop)
524 if rev not in self.commits:
524 if rev not in self.commits:
525 raise error.Abort(_('svn: revision %s not found') % revnum)
525 raise error.Abort(_('svn: revision %s not found') % revnum)
526 revcommit = self.commits[rev]
526 revcommit = self.commits[rev]
527 # caller caches the result, so free it here to release memory
527 # caller caches the result, so free it here to release memory
528 del self.commits[rev]
528 del self.commits[rev]
529 return revcommit
529 return revcommit
530
530
531 def checkrevformat(self, revstr, mapname='splicemap'):
531 def checkrevformat(self, revstr, mapname='splicemap'):
532 """ fails if revision format does not match the correct format"""
532 """ fails if revision format does not match the correct format"""
533 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
533 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
534 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
534 r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
535 '{12,12}(.*)\@[0-9]+$',revstr):
535 r'{12,12}(.*)\@[0-9]+$',revstr):
536 raise error.Abort(_('%s entry %s is not a valid revision'
536 raise error.Abort(_('%s entry %s is not a valid revision'
537 ' identifier') % (mapname, revstr))
537 ' identifier') % (mapname, revstr))
538
538
539 def numcommits(self):
539 def numcommits(self):
540 return int(self.head.rsplit('@', 1)[1]) - self.startrev
540 return int(self.head.rsplit('@', 1)[1]) - self.startrev
541
541
542 def gettags(self):
542 def gettags(self):
543 tags = {}
543 tags = {}
544 if self.tags is None:
544 if self.tags is None:
545 return tags
545 return tags
546
546
547 # svn tags are just a convention, project branches left in a
547 # svn tags are just a convention, project branches left in a
548 # 'tags' directory. There is no other relationship than
548 # 'tags' directory. There is no other relationship than
549 # ancestry, which is expensive to discover and makes them hard
549 # ancestry, which is expensive to discover and makes them hard
550 # to update incrementally. Worse, past revisions may be
550 # to update incrementally. Worse, past revisions may be
551 # referenced by tags far away in the future, requiring a deep
551 # referenced by tags far away in the future, requiring a deep
552 # history traversal on every calculation. Current code
552 # history traversal on every calculation. Current code
553 # performs a single backward traversal, tracking moves within
553 # performs a single backward traversal, tracking moves within
554 # the tags directory (tag renaming) and recording a new tag
554 # the tags directory (tag renaming) and recording a new tag
555 # everytime a project is copied from outside the tags
555 # everytime a project is copied from outside the tags
556 # directory. It also lists deleted tags, this behaviour may
556 # directory. It also lists deleted tags, this behaviour may
557 # change in the future.
557 # change in the future.
558 pendings = []
558 pendings = []
559 tagspath = self.tags
559 tagspath = self.tags
560 start = svn.ra.get_latest_revnum(self.ra)
560 start = svn.ra.get_latest_revnum(self.ra)
561 stream = self._getlog([self.tags], start, self.startrev)
561 stream = self._getlog([self.tags], start, self.startrev)
562 try:
562 try:
563 for entry in stream:
563 for entry in stream:
564 origpaths, revnum, author, date, message = entry
564 origpaths, revnum, author, date, message = entry
565 if not origpaths:
565 if not origpaths:
566 origpaths = []
566 origpaths = []
567 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
567 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
568 in origpaths.iteritems() if e.copyfrom_path]
568 in origpaths.iteritems() if e.copyfrom_path]
569 # Apply moves/copies from more specific to general
569 # Apply moves/copies from more specific to general
570 copies.sort(reverse=True)
570 copies.sort(reverse=True)
571
571
572 srctagspath = tagspath
572 srctagspath = tagspath
573 if copies and copies[-1][2] == tagspath:
573 if copies and copies[-1][2] == tagspath:
574 # Track tags directory moves
574 # Track tags directory moves
575 srctagspath = copies.pop()[0]
575 srctagspath = copies.pop()[0]
576
576
577 for source, sourcerev, dest in copies:
577 for source, sourcerev, dest in copies:
578 if not dest.startswith(tagspath + '/'):
578 if not dest.startswith(tagspath + '/'):
579 continue
579 continue
580 for tag in pendings:
580 for tag in pendings:
581 if tag[0].startswith(dest):
581 if tag[0].startswith(dest):
582 tagpath = source + tag[0][len(dest):]
582 tagpath = source + tag[0][len(dest):]
583 tag[:2] = [tagpath, sourcerev]
583 tag[:2] = [tagpath, sourcerev]
584 break
584 break
585 else:
585 else:
586 pendings.append([source, sourcerev, dest])
586 pendings.append([source, sourcerev, dest])
587
587
588 # Filter out tags with children coming from different
588 # Filter out tags with children coming from different
589 # parts of the repository like:
589 # parts of the repository like:
590 # /tags/tag.1 (from /trunk:10)
590 # /tags/tag.1 (from /trunk:10)
591 # /tags/tag.1/foo (from /branches/foo:12)
591 # /tags/tag.1/foo (from /branches/foo:12)
592 # Here/tags/tag.1 discarded as well as its children.
592 # Here/tags/tag.1 discarded as well as its children.
593 # It happens with tools like cvs2svn. Such tags cannot
593 # It happens with tools like cvs2svn. Such tags cannot
594 # be represented in mercurial.
594 # be represented in mercurial.
595 addeds = dict((p, e.copyfrom_path) for p, e
595 addeds = dict((p, e.copyfrom_path) for p, e
596 in origpaths.iteritems()
596 in origpaths.iteritems()
597 if e.action == 'A' and e.copyfrom_path)
597 if e.action == 'A' and e.copyfrom_path)
598 badroots = set()
598 badroots = set()
599 for destroot in addeds:
599 for destroot in addeds:
600 for source, sourcerev, dest in pendings:
600 for source, sourcerev, dest in pendings:
601 if (not dest.startswith(destroot + '/')
601 if (not dest.startswith(destroot + '/')
602 or source.startswith(addeds[destroot] + '/')):
602 or source.startswith(addeds[destroot] + '/')):
603 continue
603 continue
604 badroots.add(destroot)
604 badroots.add(destroot)
605 break
605 break
606
606
607 for badroot in badroots:
607 for badroot in badroots:
608 pendings = [p for p in pendings if p[2] != badroot
608 pendings = [p for p in pendings if p[2] != badroot
609 and not p[2].startswith(badroot + '/')]
609 and not p[2].startswith(badroot + '/')]
610
610
611 # Tell tag renamings from tag creations
611 # Tell tag renamings from tag creations
612 renamings = []
612 renamings = []
613 for source, sourcerev, dest in pendings:
613 for source, sourcerev, dest in pendings:
614 tagname = dest.split('/')[-1]
614 tagname = dest.split('/')[-1]
615 if source.startswith(srctagspath):
615 if source.startswith(srctagspath):
616 renamings.append([source, sourcerev, tagname])
616 renamings.append([source, sourcerev, tagname])
617 continue
617 continue
618 if tagname in tags:
618 if tagname in tags:
619 # Keep the latest tag value
619 # Keep the latest tag value
620 continue
620 continue
621 # From revision may be fake, get one with changes
621 # From revision may be fake, get one with changes
622 try:
622 try:
623 tagid = self.latest(source, sourcerev)
623 tagid = self.latest(source, sourcerev)
624 if tagid and tagname not in tags:
624 if tagid and tagname not in tags:
625 tags[tagname] = tagid
625 tags[tagname] = tagid
626 except SvnPathNotFound:
626 except SvnPathNotFound:
627 # It happens when we are following directories
627 # It happens when we are following directories
628 # we assumed were copied with their parents
628 # we assumed were copied with their parents
629 # but were really created in the tag
629 # but were really created in the tag
630 # directory.
630 # directory.
631 pass
631 pass
632 pendings = renamings
632 pendings = renamings
633 tagspath = srctagspath
633 tagspath = srctagspath
634 finally:
634 finally:
635 stream.close()
635 stream.close()
636 return tags
636 return tags
637
637
638 def converted(self, rev, destrev):
638 def converted(self, rev, destrev):
639 if not self.wc:
639 if not self.wc:
640 return
640 return
641 if self.convertfp is None:
641 if self.convertfp is None:
642 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
642 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
643 'a')
643 'a')
644 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
644 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
645 self.convertfp.flush()
645 self.convertfp.flush()
646
646
647 def revid(self, revnum, module=None):
647 def revid(self, revnum, module=None):
648 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
648 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
649
649
650 def revnum(self, rev):
650 def revnum(self, rev):
651 return int(rev.split('@')[-1])
651 return int(rev.split('@')[-1])
652
652
653 def latest(self, path, stop=None):
653 def latest(self, path, stop=None):
654 """Find the latest revid affecting path, up to stop revision
654 """Find the latest revid affecting path, up to stop revision
655 number. If stop is None, default to repository latest
655 number. If stop is None, default to repository latest
656 revision. It may return a revision in a different module,
656 revision. It may return a revision in a different module,
657 since a branch may be moved without a change being
657 since a branch may be moved without a change being
658 reported. Return None if computed module does not belong to
658 reported. Return None if computed module does not belong to
659 rootmodule subtree.
659 rootmodule subtree.
660 """
660 """
661 def findchanges(path, start, stop=None):
661 def findchanges(path, start, stop=None):
662 stream = self._getlog([path], start, stop or 1)
662 stream = self._getlog([path], start, stop or 1)
663 try:
663 try:
664 for entry in stream:
664 for entry in stream:
665 paths, revnum, author, date, message = entry
665 paths, revnum, author, date, message = entry
666 if stop is None and paths:
666 if stop is None and paths:
667 # We do not know the latest changed revision,
667 # We do not know the latest changed revision,
668 # keep the first one with changed paths.
668 # keep the first one with changed paths.
669 break
669 break
670 if revnum <= stop:
670 if revnum <= stop:
671 break
671 break
672
672
673 for p in paths:
673 for p in paths:
674 if (not path.startswith(p) or
674 if (not path.startswith(p) or
675 not paths[p].copyfrom_path):
675 not paths[p].copyfrom_path):
676 continue
676 continue
677 newpath = paths[p].copyfrom_path + path[len(p):]
677 newpath = paths[p].copyfrom_path + path[len(p):]
678 self.ui.debug("branch renamed from %s to %s at %d\n" %
678 self.ui.debug("branch renamed from %s to %s at %d\n" %
679 (path, newpath, revnum))
679 (path, newpath, revnum))
680 path = newpath
680 path = newpath
681 break
681 break
682 if not paths:
682 if not paths:
683 revnum = None
683 revnum = None
684 return revnum, path
684 return revnum, path
685 finally:
685 finally:
686 stream.close()
686 stream.close()
687
687
688 if not path.startswith(self.rootmodule):
688 if not path.startswith(self.rootmodule):
689 # Requests on foreign branches may be forbidden at server level
689 # Requests on foreign branches may be forbidden at server level
690 self.ui.debug('ignoring foreign branch %r\n' % path)
690 self.ui.debug('ignoring foreign branch %r\n' % path)
691 return None
691 return None
692
692
693 if stop is None:
693 if stop is None:
694 stop = svn.ra.get_latest_revnum(self.ra)
694 stop = svn.ra.get_latest_revnum(self.ra)
695 try:
695 try:
696 prevmodule = self.reparent('')
696 prevmodule = self.reparent('')
697 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
697 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
698 self.reparent(prevmodule)
698 self.reparent(prevmodule)
699 except svn.core.SubversionException:
699 except svn.core.SubversionException:
700 dirent = None
700 dirent = None
701 if not dirent:
701 if not dirent:
702 raise SvnPathNotFound(_('%s not found up to revision %d')
702 raise SvnPathNotFound(_('%s not found up to revision %d')
703 % (path, stop))
703 % (path, stop))
704
704
705 # stat() gives us the previous revision on this line of
705 # stat() gives us the previous revision on this line of
706 # development, but it might be in *another module*. Fetch the
706 # development, but it might be in *another module*. Fetch the
707 # log and detect renames down to the latest revision.
707 # log and detect renames down to the latest revision.
708 revnum, realpath = findchanges(path, stop, dirent.created_rev)
708 revnum, realpath = findchanges(path, stop, dirent.created_rev)
709 if revnum is None:
709 if revnum is None:
710 # Tools like svnsync can create empty revision, when
710 # Tools like svnsync can create empty revision, when
711 # synchronizing only a subtree for instance. These empty
711 # synchronizing only a subtree for instance. These empty
712 # revisions created_rev still have their original values
712 # revisions created_rev still have their original values
713 # despite all changes having disappeared and can be
713 # despite all changes having disappeared and can be
714 # returned by ra.stat(), at least when stating the root
714 # returned by ra.stat(), at least when stating the root
715 # module. In that case, do not trust created_rev and scan
715 # module. In that case, do not trust created_rev and scan
716 # the whole history.
716 # the whole history.
717 revnum, realpath = findchanges(path, stop)
717 revnum, realpath = findchanges(path, stop)
718 if revnum is None:
718 if revnum is None:
719 self.ui.debug('ignoring empty branch %r\n' % realpath)
719 self.ui.debug('ignoring empty branch %r\n' % realpath)
720 return None
720 return None
721
721
722 if not realpath.startswith(self.rootmodule):
722 if not realpath.startswith(self.rootmodule):
723 self.ui.debug('ignoring foreign branch %r\n' % realpath)
723 self.ui.debug('ignoring foreign branch %r\n' % realpath)
724 return None
724 return None
725 return self.revid(revnum, realpath)
725 return self.revid(revnum, realpath)
726
726
727 def reparent(self, module):
727 def reparent(self, module):
728 """Reparent the svn transport and return the previous parent."""
728 """Reparent the svn transport and return the previous parent."""
729 if self.prevmodule == module:
729 if self.prevmodule == module:
730 return module
730 return module
731 svnurl = self.baseurl + quote(module)
731 svnurl = self.baseurl + quote(module)
732 prevmodule = self.prevmodule
732 prevmodule = self.prevmodule
733 if prevmodule is None:
733 if prevmodule is None:
734 prevmodule = ''
734 prevmodule = ''
735 self.ui.debug("reparent to %s\n" % svnurl)
735 self.ui.debug("reparent to %s\n" % svnurl)
736 svn.ra.reparent(self.ra, svnurl)
736 svn.ra.reparent(self.ra, svnurl)
737 self.prevmodule = module
737 self.prevmodule = module
738 return prevmodule
738 return prevmodule
739
739
740 def expandpaths(self, rev, paths, parents):
740 def expandpaths(self, rev, paths, parents):
741 changed, removed = set(), set()
741 changed, removed = set(), set()
742 copies = {}
742 copies = {}
743
743
744 new_module, revnum = revsplit(rev)[1:]
744 new_module, revnum = revsplit(rev)[1:]
745 if new_module != self.module:
745 if new_module != self.module:
746 self.module = new_module
746 self.module = new_module
747 self.reparent(self.module)
747 self.reparent(self.module)
748
748
749 for i, (path, ent) in enumerate(paths):
749 for i, (path, ent) in enumerate(paths):
750 self.ui.progress(_('scanning paths'), i, item=path,
750 self.ui.progress(_('scanning paths'), i, item=path,
751 total=len(paths), unit=_('paths'))
751 total=len(paths), unit=_('paths'))
752 entrypath = self.getrelpath(path)
752 entrypath = self.getrelpath(path)
753
753
754 kind = self._checkpath(entrypath, revnum)
754 kind = self._checkpath(entrypath, revnum)
755 if kind == svn.core.svn_node_file:
755 if kind == svn.core.svn_node_file:
756 changed.add(self.recode(entrypath))
756 changed.add(self.recode(entrypath))
757 if not ent.copyfrom_path or not parents:
757 if not ent.copyfrom_path or not parents:
758 continue
758 continue
759 # Copy sources not in parent revisions cannot be
759 # Copy sources not in parent revisions cannot be
760 # represented, ignore their origin for now
760 # represented, ignore their origin for now
761 pmodule, prevnum = revsplit(parents[0])[1:]
761 pmodule, prevnum = revsplit(parents[0])[1:]
762 if ent.copyfrom_rev < prevnum:
762 if ent.copyfrom_rev < prevnum:
763 continue
763 continue
764 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
764 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
765 if not copyfrom_path:
765 if not copyfrom_path:
766 continue
766 continue
767 self.ui.debug("copied to %s from %s@%s\n" %
767 self.ui.debug("copied to %s from %s@%s\n" %
768 (entrypath, copyfrom_path, ent.copyfrom_rev))
768 (entrypath, copyfrom_path, ent.copyfrom_rev))
769 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
769 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
770 elif kind == 0: # gone, but had better be a deleted *file*
770 elif kind == 0: # gone, but had better be a deleted *file*
771 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
771 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
772 pmodule, prevnum = revsplit(parents[0])[1:]
772 pmodule, prevnum = revsplit(parents[0])[1:]
773 parentpath = pmodule + "/" + entrypath
773 parentpath = pmodule + "/" + entrypath
774 fromkind = self._checkpath(entrypath, prevnum, pmodule)
774 fromkind = self._checkpath(entrypath, prevnum, pmodule)
775
775
776 if fromkind == svn.core.svn_node_file:
776 if fromkind == svn.core.svn_node_file:
777 removed.add(self.recode(entrypath))
777 removed.add(self.recode(entrypath))
778 elif fromkind == svn.core.svn_node_dir:
778 elif fromkind == svn.core.svn_node_dir:
779 oroot = parentpath.strip('/')
779 oroot = parentpath.strip('/')
780 nroot = path.strip('/')
780 nroot = path.strip('/')
781 children = self._iterfiles(oroot, prevnum)
781 children = self._iterfiles(oroot, prevnum)
782 for childpath in children:
782 for childpath in children:
783 childpath = childpath.replace(oroot, nroot)
783 childpath = childpath.replace(oroot, nroot)
784 childpath = self.getrelpath("/" + childpath, pmodule)
784 childpath = self.getrelpath("/" + childpath, pmodule)
785 if childpath:
785 if childpath:
786 removed.add(self.recode(childpath))
786 removed.add(self.recode(childpath))
787 else:
787 else:
788 self.ui.debug('unknown path in revision %d: %s\n' % \
788 self.ui.debug('unknown path in revision %d: %s\n' % \
789 (revnum, path))
789 (revnum, path))
790 elif kind == svn.core.svn_node_dir:
790 elif kind == svn.core.svn_node_dir:
791 if ent.action == 'M':
791 if ent.action == 'M':
792 # If the directory just had a prop change,
792 # If the directory just had a prop change,
793 # then we shouldn't need to look for its children.
793 # then we shouldn't need to look for its children.
794 continue
794 continue
795 if ent.action == 'R' and parents:
795 if ent.action == 'R' and parents:
796 # If a directory is replacing a file, mark the previous
796 # If a directory is replacing a file, mark the previous
797 # file as deleted
797 # file as deleted
798 pmodule, prevnum = revsplit(parents[0])[1:]
798 pmodule, prevnum = revsplit(parents[0])[1:]
799 pkind = self._checkpath(entrypath, prevnum, pmodule)
799 pkind = self._checkpath(entrypath, prevnum, pmodule)
800 if pkind == svn.core.svn_node_file:
800 if pkind == svn.core.svn_node_file:
801 removed.add(self.recode(entrypath))
801 removed.add(self.recode(entrypath))
802 elif pkind == svn.core.svn_node_dir:
802 elif pkind == svn.core.svn_node_dir:
803 # We do not know what files were kept or removed,
803 # We do not know what files were kept or removed,
804 # mark them all as changed.
804 # mark them all as changed.
805 for childpath in self._iterfiles(pmodule, prevnum):
805 for childpath in self._iterfiles(pmodule, prevnum):
806 childpath = self.getrelpath("/" + childpath)
806 childpath = self.getrelpath("/" + childpath)
807 if childpath:
807 if childpath:
808 changed.add(self.recode(childpath))
808 changed.add(self.recode(childpath))
809
809
810 for childpath in self._iterfiles(path, revnum):
810 for childpath in self._iterfiles(path, revnum):
811 childpath = self.getrelpath("/" + childpath)
811 childpath = self.getrelpath("/" + childpath)
812 if childpath:
812 if childpath:
813 changed.add(self.recode(childpath))
813 changed.add(self.recode(childpath))
814
814
815 # Handle directory copies
815 # Handle directory copies
816 if not ent.copyfrom_path or not parents:
816 if not ent.copyfrom_path or not parents:
817 continue
817 continue
818 # Copy sources not in parent revisions cannot be
818 # Copy sources not in parent revisions cannot be
819 # represented, ignore their origin for now
819 # represented, ignore their origin for now
820 pmodule, prevnum = revsplit(parents[0])[1:]
820 pmodule, prevnum = revsplit(parents[0])[1:]
821 if ent.copyfrom_rev < prevnum:
821 if ent.copyfrom_rev < prevnum:
822 continue
822 continue
823 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
823 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
824 if not copyfrompath:
824 if not copyfrompath:
825 continue
825 continue
826 self.ui.debug("mark %s came from %s:%d\n"
826 self.ui.debug("mark %s came from %s:%d\n"
827 % (path, copyfrompath, ent.copyfrom_rev))
827 % (path, copyfrompath, ent.copyfrom_rev))
828 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
828 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
829 for childpath in children:
829 for childpath in children:
830 childpath = self.getrelpath("/" + childpath, pmodule)
830 childpath = self.getrelpath("/" + childpath, pmodule)
831 if not childpath:
831 if not childpath:
832 continue
832 continue
833 copytopath = path + childpath[len(copyfrompath):]
833 copytopath = path + childpath[len(copyfrompath):]
834 copytopath = self.getrelpath(copytopath)
834 copytopath = self.getrelpath(copytopath)
835 copies[self.recode(copytopath)] = self.recode(childpath)
835 copies[self.recode(copytopath)] = self.recode(childpath)
836
836
837 self.ui.progress(_('scanning paths'), None)
837 self.ui.progress(_('scanning paths'), None)
838 changed.update(removed)
838 changed.update(removed)
839 return (list(changed), removed, copies)
839 return (list(changed), removed, copies)
840
840
841 def _fetch_revisions(self, from_revnum, to_revnum):
841 def _fetch_revisions(self, from_revnum, to_revnum):
842 if from_revnum < to_revnum:
842 if from_revnum < to_revnum:
843 from_revnum, to_revnum = to_revnum, from_revnum
843 from_revnum, to_revnum = to_revnum, from_revnum
844
844
845 self.child_cset = None
845 self.child_cset = None
846
846
847 def parselogentry(orig_paths, revnum, author, date, message):
847 def parselogentry(orig_paths, revnum, author, date, message):
848 """Return the parsed commit object or None, and True if
848 """Return the parsed commit object or None, and True if
849 the revision is a branch root.
849 the revision is a branch root.
850 """
850 """
851 self.ui.debug("parsing revision %d (%d changes)\n" %
851 self.ui.debug("parsing revision %d (%d changes)\n" %
852 (revnum, len(orig_paths)))
852 (revnum, len(orig_paths)))
853
853
854 branched = False
854 branched = False
855 rev = self.revid(revnum)
855 rev = self.revid(revnum)
856 # branch log might return entries for a parent we already have
856 # branch log might return entries for a parent we already have
857
857
858 if rev in self.commits or revnum < to_revnum:
858 if rev in self.commits or revnum < to_revnum:
859 return None, branched
859 return None, branched
860
860
861 parents = []
861 parents = []
862 # check whether this revision is the start of a branch or part
862 # check whether this revision is the start of a branch or part
863 # of a branch renaming
863 # of a branch renaming
864 orig_paths = sorted(orig_paths.iteritems())
864 orig_paths = sorted(orig_paths.iteritems())
865 root_paths = [(p, e) for p, e in orig_paths
865 root_paths = [(p, e) for p, e in orig_paths
866 if self.module.startswith(p)]
866 if self.module.startswith(p)]
867 if root_paths:
867 if root_paths:
868 path, ent = root_paths[-1]
868 path, ent = root_paths[-1]
869 if ent.copyfrom_path:
869 if ent.copyfrom_path:
870 branched = True
870 branched = True
871 newpath = ent.copyfrom_path + self.module[len(path):]
871 newpath = ent.copyfrom_path + self.module[len(path):]
872 # ent.copyfrom_rev may not be the actual last revision
872 # ent.copyfrom_rev may not be the actual last revision
873 previd = self.latest(newpath, ent.copyfrom_rev)
873 previd = self.latest(newpath, ent.copyfrom_rev)
874 if previd is not None:
874 if previd is not None:
875 prevmodule, prevnum = revsplit(previd)[1:]
875 prevmodule, prevnum = revsplit(previd)[1:]
876 if prevnum >= self.startrev:
876 if prevnum >= self.startrev:
877 parents = [previd]
877 parents = [previd]
878 self.ui.note(
878 self.ui.note(
879 _('found parent of branch %s at %d: %s\n') %
879 _('found parent of branch %s at %d: %s\n') %
880 (self.module, prevnum, prevmodule))
880 (self.module, prevnum, prevmodule))
881 else:
881 else:
882 self.ui.debug("no copyfrom path, don't know what to do.\n")
882 self.ui.debug("no copyfrom path, don't know what to do.\n")
883
883
884 paths = []
884 paths = []
885 # filter out unrelated paths
885 # filter out unrelated paths
886 for path, ent in orig_paths:
886 for path, ent in orig_paths:
887 if self.getrelpath(path) is None:
887 if self.getrelpath(path) is None:
888 continue
888 continue
889 paths.append((path, ent))
889 paths.append((path, ent))
890
890
891 # Example SVN datetime. Includes microseconds.
891 # Example SVN datetime. Includes microseconds.
892 # ISO-8601 conformant
892 # ISO-8601 conformant
893 # '2007-01-04T17:35:00.902377Z'
893 # '2007-01-04T17:35:00.902377Z'
894 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
894 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
895 if self.ui.configbool('convert', 'localtimezone'):
895 if self.ui.configbool('convert', 'localtimezone'):
896 date = makedatetimestamp(date[0])
896 date = makedatetimestamp(date[0])
897
897
898 if message:
898 if message:
899 log = self.recode(message)
899 log = self.recode(message)
900 else:
900 else:
901 log = ''
901 log = ''
902
902
903 if author:
903 if author:
904 author = self.recode(author)
904 author = self.recode(author)
905 else:
905 else:
906 author = ''
906 author = ''
907
907
908 try:
908 try:
909 branch = self.module.split("/")[-1]
909 branch = self.module.split("/")[-1]
910 if branch == self.trunkname:
910 if branch == self.trunkname:
911 branch = None
911 branch = None
912 except IndexError:
912 except IndexError:
913 branch = None
913 branch = None
914
914
915 cset = commit(author=author,
915 cset = commit(author=author,
916 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
916 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
917 desc=log,
917 desc=log,
918 parents=parents,
918 parents=parents,
919 branch=branch,
919 branch=branch,
920 rev=rev)
920 rev=rev)
921
921
922 self.commits[rev] = cset
922 self.commits[rev] = cset
923 # The parents list is *shared* among self.paths and the
923 # The parents list is *shared* among self.paths and the
924 # commit object. Both will be updated below.
924 # commit object. Both will be updated below.
925 self.paths[rev] = (paths, cset.parents)
925 self.paths[rev] = (paths, cset.parents)
926 if self.child_cset and not self.child_cset.parents:
926 if self.child_cset and not self.child_cset.parents:
927 self.child_cset.parents[:] = [rev]
927 self.child_cset.parents[:] = [rev]
928 self.child_cset = cset
928 self.child_cset = cset
929 return cset, branched
929 return cset, branched
930
930
931 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
931 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
932 (self.module, from_revnum, to_revnum))
932 (self.module, from_revnum, to_revnum))
933
933
934 try:
934 try:
935 firstcset = None
935 firstcset = None
936 lastonbranch = False
936 lastonbranch = False
937 stream = self._getlog([self.module], from_revnum, to_revnum)
937 stream = self._getlog([self.module], from_revnum, to_revnum)
938 try:
938 try:
939 for entry in stream:
939 for entry in stream:
940 paths, revnum, author, date, message = entry
940 paths, revnum, author, date, message = entry
941 if revnum < self.startrev:
941 if revnum < self.startrev:
942 lastonbranch = True
942 lastonbranch = True
943 break
943 break
944 if not paths:
944 if not paths:
945 self.ui.debug('revision %d has no entries\n' % revnum)
945 self.ui.debug('revision %d has no entries\n' % revnum)
946 # If we ever leave the loop on an empty
946 # If we ever leave the loop on an empty
947 # revision, do not try to get a parent branch
947 # revision, do not try to get a parent branch
948 lastonbranch = lastonbranch or revnum == 0
948 lastonbranch = lastonbranch or revnum == 0
949 continue
949 continue
950 cset, lastonbranch = parselogentry(paths, revnum, author,
950 cset, lastonbranch = parselogentry(paths, revnum, author,
951 date, message)
951 date, message)
952 if cset:
952 if cset:
953 firstcset = cset
953 firstcset = cset
954 if lastonbranch:
954 if lastonbranch:
955 break
955 break
956 finally:
956 finally:
957 stream.close()
957 stream.close()
958
958
959 if not lastonbranch and firstcset and not firstcset.parents:
959 if not lastonbranch and firstcset and not firstcset.parents:
960 # The first revision of the sequence (the last fetched one)
960 # The first revision of the sequence (the last fetched one)
961 # has invalid parents if not a branch root. Find the parent
961 # has invalid parents if not a branch root. Find the parent
962 # revision now, if any.
962 # revision now, if any.
963 try:
963 try:
964 firstrevnum = self.revnum(firstcset.rev)
964 firstrevnum = self.revnum(firstcset.rev)
965 if firstrevnum > 1:
965 if firstrevnum > 1:
966 latest = self.latest(self.module, firstrevnum - 1)
966 latest = self.latest(self.module, firstrevnum - 1)
967 if latest:
967 if latest:
968 firstcset.parents.append(latest)
968 firstcset.parents.append(latest)
969 except SvnPathNotFound:
969 except SvnPathNotFound:
970 pass
970 pass
971 except svn.core.SubversionException as xxx_todo_changeme:
971 except svn.core.SubversionException as xxx_todo_changeme:
972 (inst, num) = xxx_todo_changeme.args
972 (inst, num) = xxx_todo_changeme.args
973 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
973 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
974 raise error.Abort(_('svn: branch has no revision %s')
974 raise error.Abort(_('svn: branch has no revision %s')
975 % to_revnum)
975 % to_revnum)
976 raise
976 raise
977
977
978 def getfile(self, file, rev):
978 def getfile(self, file, rev):
979 # TODO: ra.get_file transmits the whole file instead of diffs.
979 # TODO: ra.get_file transmits the whole file instead of diffs.
980 if file in self.removed:
980 if file in self.removed:
981 return None, None
981 return None, None
982 mode = ''
982 mode = ''
983 try:
983 try:
984 new_module, revnum = revsplit(rev)[1:]
984 new_module, revnum = revsplit(rev)[1:]
985 if self.module != new_module:
985 if self.module != new_module:
986 self.module = new_module
986 self.module = new_module
987 self.reparent(self.module)
987 self.reparent(self.module)
988 io = stringio()
988 io = stringio()
989 info = svn.ra.get_file(self.ra, file, revnum, io)
989 info = svn.ra.get_file(self.ra, file, revnum, io)
990 data = io.getvalue()
990 data = io.getvalue()
991 # ra.get_file() seems to keep a reference on the input buffer
991 # ra.get_file() seems to keep a reference on the input buffer
992 # preventing collection. Release it explicitly.
992 # preventing collection. Release it explicitly.
993 io.close()
993 io.close()
994 if isinstance(info, list):
994 if isinstance(info, list):
995 info = info[-1]
995 info = info[-1]
996 mode = ("svn:executable" in info) and 'x' or ''
996 mode = ("svn:executable" in info) and 'x' or ''
997 mode = ("svn:special" in info) and 'l' or mode
997 mode = ("svn:special" in info) and 'l' or mode
998 except svn.core.SubversionException as e:
998 except svn.core.SubversionException as e:
999 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
999 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1000 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1000 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1001 if e.apr_err in notfound: # File not found
1001 if e.apr_err in notfound: # File not found
1002 return None, None
1002 return None, None
1003 raise
1003 raise
1004 if mode == 'l':
1004 if mode == 'l':
1005 link_prefix = "link "
1005 link_prefix = "link "
1006 if data.startswith(link_prefix):
1006 if data.startswith(link_prefix):
1007 data = data[len(link_prefix):]
1007 data = data[len(link_prefix):]
1008 return data, mode
1008 return data, mode
1009
1009
1010 def _iterfiles(self, path, revnum):
1010 def _iterfiles(self, path, revnum):
1011 """Enumerate all files in path at revnum, recursively."""
1011 """Enumerate all files in path at revnum, recursively."""
1012 path = path.strip('/')
1012 path = path.strip('/')
1013 pool = svn.core.Pool()
1013 pool = svn.core.Pool()
1014 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1014 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1015 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1015 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1016 if path:
1016 if path:
1017 path += '/'
1017 path += '/'
1018 return ((path + p) for p, e in entries.iteritems()
1018 return ((path + p) for p, e in entries.iteritems()
1019 if e.kind == svn.core.svn_node_file)
1019 if e.kind == svn.core.svn_node_file)
1020
1020
1021 def getrelpath(self, path, module=None):
1021 def getrelpath(self, path, module=None):
1022 if module is None:
1022 if module is None:
1023 module = self.module
1023 module = self.module
1024 # Given the repository url of this wc, say
1024 # Given the repository url of this wc, say
1025 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1025 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1026 # extract the "entry" portion (a relative path) from what
1026 # extract the "entry" portion (a relative path) from what
1027 # svn log --xml says, i.e.
1027 # svn log --xml says, i.e.
1028 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1028 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1029 # that is to say "tests/PloneTestCase.py"
1029 # that is to say "tests/PloneTestCase.py"
1030 if path.startswith(module):
1030 if path.startswith(module):
1031 relative = path.rstrip('/')[len(module):]
1031 relative = path.rstrip('/')[len(module):]
1032 if relative.startswith('/'):
1032 if relative.startswith('/'):
1033 return relative[1:]
1033 return relative[1:]
1034 elif relative == '':
1034 elif relative == '':
1035 return relative
1035 return relative
1036
1036
1037 # The path is outside our tracked tree...
1037 # The path is outside our tracked tree...
1038 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1038 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1039 return None
1039 return None
1040
1040
1041 def _checkpath(self, path, revnum, module=None):
1041 def _checkpath(self, path, revnum, module=None):
1042 if module is not None:
1042 if module is not None:
1043 prevmodule = self.reparent('')
1043 prevmodule = self.reparent('')
1044 path = module + '/' + path
1044 path = module + '/' + path
1045 try:
1045 try:
1046 # ra.check_path does not like leading slashes very much, it leads
1046 # ra.check_path does not like leading slashes very much, it leads
1047 # to PROPFIND subversion errors
1047 # to PROPFIND subversion errors
1048 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1048 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1049 finally:
1049 finally:
1050 if module is not None:
1050 if module is not None:
1051 self.reparent(prevmodule)
1051 self.reparent(prevmodule)
1052
1052
1053 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1053 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1054 strict_node_history=False):
1054 strict_node_history=False):
1055 # Normalize path names, svn >= 1.5 only wants paths relative to
1055 # Normalize path names, svn >= 1.5 only wants paths relative to
1056 # supplied URL
1056 # supplied URL
1057 relpaths = []
1057 relpaths = []
1058 for p in paths:
1058 for p in paths:
1059 if not p.startswith('/'):
1059 if not p.startswith('/'):
1060 p = self.module + '/' + p
1060 p = self.module + '/' + p
1061 relpaths.append(p.strip('/'))
1061 relpaths.append(p.strip('/'))
1062 args = [self.baseurl, relpaths, start, end, limit,
1062 args = [self.baseurl, relpaths, start, end, limit,
1063 discover_changed_paths, strict_node_history]
1063 discover_changed_paths, strict_node_history]
1064 # developer config: convert.svn.debugsvnlog
1064 # developer config: convert.svn.debugsvnlog
1065 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1065 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1066 return directlogstream(*args)
1066 return directlogstream(*args)
1067 arg = encodeargs(args)
1067 arg = encodeargs(args)
1068 hgexe = util.hgexecutable()
1068 hgexe = util.hgexecutable()
1069 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1069 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1070 stdin, stdout = util.popen2(util.quotecommand(cmd))
1070 stdin, stdout = util.popen2(util.quotecommand(cmd))
1071 stdin.write(arg)
1071 stdin.write(arg)
1072 try:
1072 try:
1073 stdin.close()
1073 stdin.close()
1074 except IOError:
1074 except IOError:
1075 raise error.Abort(_('Mercurial failed to run itself, check'
1075 raise error.Abort(_('Mercurial failed to run itself, check'
1076 ' hg executable is in PATH'))
1076 ' hg executable is in PATH'))
1077 return logstream(stdout)
1077 return logstream(stdout)
1078
1078
1079 pre_revprop_change = '''#!/bin/sh
1079 pre_revprop_change = '''#!/bin/sh
1080
1080
1081 REPOS="$1"
1081 REPOS="$1"
1082 REV="$2"
1082 REV="$2"
1083 USER="$3"
1083 USER="$3"
1084 PROPNAME="$4"
1084 PROPNAME="$4"
1085 ACTION="$5"
1085 ACTION="$5"
1086
1086
1087 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1087 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1090
1090
1091 echo "Changing prohibited revision property" >&2
1091 echo "Changing prohibited revision property" >&2
1092 exit 1
1092 exit 1
1093 '''
1093 '''
1094
1094
1095 class svn_sink(converter_sink, commandline):
1095 class svn_sink(converter_sink, commandline):
1096 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1096 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1097 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1097 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1098
1098
1099 def prerun(self):
1099 def prerun(self):
1100 if self.wc:
1100 if self.wc:
1101 os.chdir(self.wc)
1101 os.chdir(self.wc)
1102
1102
1103 def postrun(self):
1103 def postrun(self):
1104 if self.wc:
1104 if self.wc:
1105 os.chdir(self.cwd)
1105 os.chdir(self.cwd)
1106
1106
1107 def join(self, name):
1107 def join(self, name):
1108 return os.path.join(self.wc, '.svn', name)
1108 return os.path.join(self.wc, '.svn', name)
1109
1109
1110 def revmapfile(self):
1110 def revmapfile(self):
1111 return self.join('hg-shamap')
1111 return self.join('hg-shamap')
1112
1112
1113 def authorfile(self):
1113 def authorfile(self):
1114 return self.join('hg-authormap')
1114 return self.join('hg-authormap')
1115
1115
1116 def __init__(self, ui, path):
1116 def __init__(self, ui, path):
1117
1117
1118 converter_sink.__init__(self, ui, path)
1118 converter_sink.__init__(self, ui, path)
1119 commandline.__init__(self, ui, 'svn')
1119 commandline.__init__(self, ui, 'svn')
1120 self.delete = []
1120 self.delete = []
1121 self.setexec = []
1121 self.setexec = []
1122 self.delexec = []
1122 self.delexec = []
1123 self.copies = []
1123 self.copies = []
1124 self.wc = None
1124 self.wc = None
1125 self.cwd = os.getcwd()
1125 self.cwd = os.getcwd()
1126
1126
1127 created = False
1127 created = False
1128 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1128 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1129 self.wc = os.path.realpath(path)
1129 self.wc = os.path.realpath(path)
1130 self.run0('update')
1130 self.run0('update')
1131 else:
1131 else:
1132 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1132 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1133 path = os.path.realpath(path)
1133 path = os.path.realpath(path)
1134 if os.path.isdir(os.path.dirname(path)):
1134 if os.path.isdir(os.path.dirname(path)):
1135 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1135 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1136 ui.status(_('initializing svn repository %r\n') %
1136 ui.status(_('initializing svn repository %r\n') %
1137 os.path.basename(path))
1137 os.path.basename(path))
1138 commandline(ui, 'svnadmin').run0('create', path)
1138 commandline(ui, 'svnadmin').run0('create', path)
1139 created = path
1139 created = path
1140 path = util.normpath(path)
1140 path = util.normpath(path)
1141 if not path.startswith('/'):
1141 if not path.startswith('/'):
1142 path = '/' + path
1142 path = '/' + path
1143 path = 'file://' + path
1143 path = 'file://' + path
1144
1144
1145 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1145 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1146 ui.status(_('initializing svn working copy %r\n')
1146 ui.status(_('initializing svn working copy %r\n')
1147 % os.path.basename(wcpath))
1147 % os.path.basename(wcpath))
1148 self.run0('checkout', path, wcpath)
1148 self.run0('checkout', path, wcpath)
1149
1149
1150 self.wc = wcpath
1150 self.wc = wcpath
1151 self.opener = scmutil.opener(self.wc)
1151 self.opener = scmutil.opener(self.wc)
1152 self.wopener = scmutil.opener(self.wc)
1152 self.wopener = scmutil.opener(self.wc)
1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1154 if util.checkexec(self.wc):
1154 if util.checkexec(self.wc):
1155 self.is_exec = util.isexec
1155 self.is_exec = util.isexec
1156 else:
1156 else:
1157 self.is_exec = None
1157 self.is_exec = None
1158
1158
1159 if created:
1159 if created:
1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1161 fp = open(hook, 'w')
1161 fp = open(hook, 'w')
1162 fp.write(pre_revprop_change)
1162 fp.write(pre_revprop_change)
1163 fp.close()
1163 fp.close()
1164 util.setflags(hook, False, True)
1164 util.setflags(hook, False, True)
1165
1165
1166 output = self.run0('info')
1166 output = self.run0('info')
1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1168
1168
1169 def wjoin(self, *names):
1169 def wjoin(self, *names):
1170 return os.path.join(self.wc, *names)
1170 return os.path.join(self.wc, *names)
1171
1171
1172 @propertycache
1172 @propertycache
1173 def manifest(self):
1173 def manifest(self):
1174 # As of svn 1.7, the "add" command fails when receiving
1174 # As of svn 1.7, the "add" command fails when receiving
1175 # already tracked entries, so we have to track and filter them
1175 # already tracked entries, so we have to track and filter them
1176 # ourselves.
1176 # ourselves.
1177 m = set()
1177 m = set()
1178 output = self.run0('ls', recursive=True, xml=True)
1178 output = self.run0('ls', recursive=True, xml=True)
1179 doc = xml.dom.minidom.parseString(output)
1179 doc = xml.dom.minidom.parseString(output)
1180 for e in doc.getElementsByTagName('entry'):
1180 for e in doc.getElementsByTagName('entry'):
1181 for n in e.childNodes:
1181 for n in e.childNodes:
1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1183 continue
1183 continue
1184 name = ''.join(c.data for c in n.childNodes
1184 name = ''.join(c.data for c in n.childNodes
1185 if c.nodeType == c.TEXT_NODE)
1185 if c.nodeType == c.TEXT_NODE)
1186 # Entries are compared with names coming from
1186 # Entries are compared with names coming from
1187 # mercurial, so bytes with undefined encoding. Our
1187 # mercurial, so bytes with undefined encoding. Our
1188 # best bet is to assume they are in local
1188 # best bet is to assume they are in local
1189 # encoding. They will be passed to command line calls
1189 # encoding. They will be passed to command line calls
1190 # later anyway, so they better be.
1190 # later anyway, so they better be.
1191 m.add(encoding.tolocal(name.encode('utf-8')))
1191 m.add(encoding.tolocal(name.encode('utf-8')))
1192 break
1192 break
1193 return m
1193 return m
1194
1194
1195 def putfile(self, filename, flags, data):
1195 def putfile(self, filename, flags, data):
1196 if 'l' in flags:
1196 if 'l' in flags:
1197 self.wopener.symlink(data, filename)
1197 self.wopener.symlink(data, filename)
1198 else:
1198 else:
1199 try:
1199 try:
1200 if os.path.islink(self.wjoin(filename)):
1200 if os.path.islink(self.wjoin(filename)):
1201 os.unlink(filename)
1201 os.unlink(filename)
1202 except OSError:
1202 except OSError:
1203 pass
1203 pass
1204 self.wopener.write(filename, data)
1204 self.wopener.write(filename, data)
1205
1205
1206 if self.is_exec:
1206 if self.is_exec:
1207 if self.is_exec(self.wjoin(filename)):
1207 if self.is_exec(self.wjoin(filename)):
1208 if 'x' not in flags:
1208 if 'x' not in flags:
1209 self.delexec.append(filename)
1209 self.delexec.append(filename)
1210 else:
1210 else:
1211 if 'x' in flags:
1211 if 'x' in flags:
1212 self.setexec.append(filename)
1212 self.setexec.append(filename)
1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1214
1214
1215 def _copyfile(self, source, dest):
1215 def _copyfile(self, source, dest):
1216 # SVN's copy command pukes if the destination file exists, but
1216 # SVN's copy command pukes if the destination file exists, but
1217 # our copyfile method expects to record a copy that has
1217 # our copyfile method expects to record a copy that has
1218 # already occurred. Cross the semantic gap.
1218 # already occurred. Cross the semantic gap.
1219 wdest = self.wjoin(dest)
1219 wdest = self.wjoin(dest)
1220 exists = os.path.lexists(wdest)
1220 exists = os.path.lexists(wdest)
1221 if exists:
1221 if exists:
1222 fd, tempname = tempfile.mkstemp(
1222 fd, tempname = tempfile.mkstemp(
1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1224 os.close(fd)
1224 os.close(fd)
1225 os.unlink(tempname)
1225 os.unlink(tempname)
1226 os.rename(wdest, tempname)
1226 os.rename(wdest, tempname)
1227 try:
1227 try:
1228 self.run0('copy', source, dest)
1228 self.run0('copy', source, dest)
1229 finally:
1229 finally:
1230 self.manifest.add(dest)
1230 self.manifest.add(dest)
1231 if exists:
1231 if exists:
1232 try:
1232 try:
1233 os.unlink(wdest)
1233 os.unlink(wdest)
1234 except OSError:
1234 except OSError:
1235 pass
1235 pass
1236 os.rename(tempname, wdest)
1236 os.rename(tempname, wdest)
1237
1237
1238 def dirs_of(self, files):
1238 def dirs_of(self, files):
1239 dirs = set()
1239 dirs = set()
1240 for f in files:
1240 for f in files:
1241 if os.path.isdir(self.wjoin(f)):
1241 if os.path.isdir(self.wjoin(f)):
1242 dirs.add(f)
1242 dirs.add(f)
1243 for i in strutil.rfindall(f, '/'):
1243 for i in strutil.rfindall(f, '/'):
1244 dirs.add(f[:i])
1244 dirs.add(f[:i])
1245 return dirs
1245 return dirs
1246
1246
1247 def add_dirs(self, files):
1247 def add_dirs(self, files):
1248 add_dirs = [d for d in sorted(self.dirs_of(files))
1248 add_dirs = [d for d in sorted(self.dirs_of(files))
1249 if d not in self.manifest]
1249 if d not in self.manifest]
1250 if add_dirs:
1250 if add_dirs:
1251 self.manifest.update(add_dirs)
1251 self.manifest.update(add_dirs)
1252 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1252 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1253 return add_dirs
1253 return add_dirs
1254
1254
1255 def add_files(self, files):
1255 def add_files(self, files):
1256 files = [f for f in files if f not in self.manifest]
1256 files = [f for f in files if f not in self.manifest]
1257 if files:
1257 if files:
1258 self.manifest.update(files)
1258 self.manifest.update(files)
1259 self.xargs(files, 'add', quiet=True)
1259 self.xargs(files, 'add', quiet=True)
1260 return files
1260 return files
1261
1261
1262 def addchild(self, parent, child):
1262 def addchild(self, parent, child):
1263 self.childmap[parent] = child
1263 self.childmap[parent] = child
1264
1264
1265 def revid(self, rev):
1265 def revid(self, rev):
1266 return u"svn:%s@%s" % (self.uuid, rev)
1266 return u"svn:%s@%s" % (self.uuid, rev)
1267
1267
1268 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1268 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1269 cleanp2):
1269 cleanp2):
1270 for parent in parents:
1270 for parent in parents:
1271 try:
1271 try:
1272 return self.revid(self.childmap[parent])
1272 return self.revid(self.childmap[parent])
1273 except KeyError:
1273 except KeyError:
1274 pass
1274 pass
1275
1275
1276 # Apply changes to working copy
1276 # Apply changes to working copy
1277 for f, v in files:
1277 for f, v in files:
1278 data, mode = source.getfile(f, v)
1278 data, mode = source.getfile(f, v)
1279 if data is None:
1279 if data is None:
1280 self.delete.append(f)
1280 self.delete.append(f)
1281 else:
1281 else:
1282 self.putfile(f, mode, data)
1282 self.putfile(f, mode, data)
1283 if f in copies:
1283 if f in copies:
1284 self.copies.append([copies[f], f])
1284 self.copies.append([copies[f], f])
1285 if full:
1285 if full:
1286 self.delete.extend(sorted(self.manifest.difference(files)))
1286 self.delete.extend(sorted(self.manifest.difference(files)))
1287 files = [f[0] for f in files]
1287 files = [f[0] for f in files]
1288
1288
1289 entries = set(self.delete)
1289 entries = set(self.delete)
1290 files = frozenset(files)
1290 files = frozenset(files)
1291 entries.update(self.add_dirs(files.difference(entries)))
1291 entries.update(self.add_dirs(files.difference(entries)))
1292 if self.copies:
1292 if self.copies:
1293 for s, d in self.copies:
1293 for s, d in self.copies:
1294 self._copyfile(s, d)
1294 self._copyfile(s, d)
1295 self.copies = []
1295 self.copies = []
1296 if self.delete:
1296 if self.delete:
1297 self.xargs(self.delete, 'delete')
1297 self.xargs(self.delete, 'delete')
1298 for f in self.delete:
1298 for f in self.delete:
1299 self.manifest.remove(f)
1299 self.manifest.remove(f)
1300 self.delete = []
1300 self.delete = []
1301 entries.update(self.add_files(files.difference(entries)))
1301 entries.update(self.add_files(files.difference(entries)))
1302 if self.delexec:
1302 if self.delexec:
1303 self.xargs(self.delexec, 'propdel', 'svn:executable')
1303 self.xargs(self.delexec, 'propdel', 'svn:executable')
1304 self.delexec = []
1304 self.delexec = []
1305 if self.setexec:
1305 if self.setexec:
1306 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1306 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1307 self.setexec = []
1307 self.setexec = []
1308
1308
1309 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1309 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1310 fp = os.fdopen(fd, 'w')
1310 fp = os.fdopen(fd, 'w')
1311 fp.write(commit.desc)
1311 fp.write(commit.desc)
1312 fp.close()
1312 fp.close()
1313 try:
1313 try:
1314 output = self.run0('commit',
1314 output = self.run0('commit',
1315 username=util.shortuser(commit.author),
1315 username=util.shortuser(commit.author),
1316 file=messagefile,
1316 file=messagefile,
1317 encoding='utf-8')
1317 encoding='utf-8')
1318 try:
1318 try:
1319 rev = self.commit_re.search(output).group(1)
1319 rev = self.commit_re.search(output).group(1)
1320 except AttributeError:
1320 except AttributeError:
1321 if parents and not files:
1321 if parents and not files:
1322 return parents[0]
1322 return parents[0]
1323 self.ui.warn(_('unexpected svn output:\n'))
1323 self.ui.warn(_('unexpected svn output:\n'))
1324 self.ui.warn(output)
1324 self.ui.warn(output)
1325 raise error.Abort(_('unable to cope with svn output'))
1325 raise error.Abort(_('unable to cope with svn output'))
1326 if commit.rev:
1326 if commit.rev:
1327 self.run('propset', 'hg:convert-rev', commit.rev,
1327 self.run('propset', 'hg:convert-rev', commit.rev,
1328 revprop=True, revision=rev)
1328 revprop=True, revision=rev)
1329 if commit.branch and commit.branch != 'default':
1329 if commit.branch and commit.branch != 'default':
1330 self.run('propset', 'hg:convert-branch', commit.branch,
1330 self.run('propset', 'hg:convert-branch', commit.branch,
1331 revprop=True, revision=rev)
1331 revprop=True, revision=rev)
1332 for parent in parents:
1332 for parent in parents:
1333 self.addchild(parent, rev)
1333 self.addchild(parent, rev)
1334 return self.revid(rev)
1334 return self.revid(rev)
1335 finally:
1335 finally:
1336 os.unlink(messagefile)
1336 os.unlink(messagefile)
1337
1337
1338 def puttags(self, tags):
1338 def puttags(self, tags):
1339 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1339 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1340 return None, None
1340 return None, None
1341
1341
1342 def hascommitfrommap(self, rev):
1342 def hascommitfrommap(self, rev):
1343 # We trust that revisions referenced in a map still is present
1343 # We trust that revisions referenced in a map still is present
1344 # TODO: implement something better if necessary and feasible
1344 # TODO: implement something better if necessary and feasible
1345 return True
1345 return True
1346
1346
1347 def hascommitforsplicemap(self, rev):
1347 def hascommitforsplicemap(self, rev):
1348 # This is not correct as one can convert to an existing subversion
1348 # This is not correct as one can convert to an existing subversion
1349 # repository and childmap would not list all revisions. Too bad.
1349 # repository and childmap would not list all revisions. Too bad.
1350 if rev in self.childmap:
1350 if rev in self.childmap:
1351 return True
1351 return True
1352 raise error.Abort(_('splice map revision %s not found in subversion '
1352 raise error.Abort(_('splice map revision %s not found in subversion '
1353 'child map (revision lookups are not implemented)')
1353 'child map (revision lookups are not implemented)')
1354 % rev)
1354 % rev)
@@ -1,363 +1,392 b''
1 """automatically manage newlines in repository files
1 """automatically manage newlines in repository files
2
2
3 This extension allows you to manage the type of line endings (CRLF or
3 This extension allows you to manage the type of line endings (CRLF or
4 LF) that are used in the repository and in the local working
4 LF) that are used in the repository and in the local working
5 directory. That way you can get CRLF line endings on Windows and LF on
5 directory. That way you can get CRLF line endings on Windows and LF on
6 Unix/Mac, thereby letting everybody use their OS native line endings.
6 Unix/Mac, thereby letting everybody use their OS native line endings.
7
7
8 The extension reads its configuration from a versioned ``.hgeol``
8 The extension reads its configuration from a versioned ``.hgeol``
9 configuration file found in the root of the working directory. The
9 configuration file found in the root of the working directory. The
10 ``.hgeol`` file use the same syntax as all other Mercurial
10 ``.hgeol`` file use the same syntax as all other Mercurial
11 configuration files. It uses two sections, ``[patterns]`` and
11 configuration files. It uses two sections, ``[patterns]`` and
12 ``[repository]``.
12 ``[repository]``.
13
13
14 The ``[patterns]`` section specifies how line endings should be
14 The ``[patterns]`` section specifies how line endings should be
15 converted between the working directory and the repository. The format is
15 converted between the working directory and the repository. The format is
16 specified by a file pattern. The first match is used, so put more
16 specified by a file pattern. The first match is used, so put more
17 specific patterns first. The available line endings are ``LF``,
17 specific patterns first. The available line endings are ``LF``,
18 ``CRLF``, and ``BIN``.
18 ``CRLF``, and ``BIN``.
19
19
20 Files with the declared format of ``CRLF`` or ``LF`` are always
20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 checked out and stored in the repository in that format and files
21 checked out and stored in the repository in that format and files
22 declared to be binary (``BIN``) are left unchanged. Additionally,
22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 ``native`` is an alias for checking out in the platform's default line
23 ``native`` is an alias for checking out in the platform's default line
24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 default behavior; it is only needed if you need to override a later,
26 default behavior; it is only needed if you need to override a later,
27 more general pattern.
27 more general pattern.
28
28
29 The optional ``[repository]`` section specifies the line endings to
29 The optional ``[repository]`` section specifies the line endings to
30 use for files stored in the repository. It has a single setting,
30 use for files stored in the repository. It has a single setting,
31 ``native``, which determines the storage line endings for files
31 ``native``, which determines the storage line endings for files
32 declared as ``native`` in the ``[patterns]`` section. It can be set to
32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 that on Windows, files configured as ``native`` (``CRLF`` by default)
34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 will be converted to ``LF`` when stored in the repository. Files
35 will be converted to ``LF`` when stored in the repository. Files
36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 are always stored as-is in the repository.
37 are always stored as-is in the repository.
38
38
39 Example versioned ``.hgeol`` file::
39 Example versioned ``.hgeol`` file::
40
40
41 [patterns]
41 [patterns]
42 **.py = native
42 **.py = native
43 **.vcproj = CRLF
43 **.vcproj = CRLF
44 **.txt = native
44 **.txt = native
45 Makefile = LF
45 Makefile = LF
46 **.jpg = BIN
46 **.jpg = BIN
47
47
48 [repository]
48 [repository]
49 native = LF
49 native = LF
50
50
51 .. note::
51 .. note::
52
52
53 The rules will first apply when files are touched in the working
53 The rules will first apply when files are touched in the working
54 directory, e.g. by updating to null and back to tip to touch all files.
54 directory, e.g. by updating to null and back to tip to touch all files.
55
55
56 The extension uses an optional ``[eol]`` section read from both the
56 The extension uses an optional ``[eol]`` section read from both the
57 normal Mercurial configuration files and the ``.hgeol`` file, with the
57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 latter overriding the former. You can use that section to control the
58 latter overriding the former. You can use that section to control the
59 overall behavior. There are three settings:
59 overall behavior. There are three settings:
60
60
61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 ``CRLF`` to override the default interpretation of ``native`` for
62 ``CRLF`` to override the default interpretation of ``native`` for
63 checkout. This can be used with :hg:`archive` on Unix, say, to
63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 generate an archive where files have line endings for Windows.
64 generate an archive where files have line endings for Windows.
65
65
66 - ``eol.only-consistent`` (default True) can be set to False to make
66 - ``eol.only-consistent`` (default True) can be set to False to make
67 the extension convert files with inconsistent EOLs. Inconsistent
67 the extension convert files with inconsistent EOLs. Inconsistent
68 means that there is both ``CRLF`` and ``LF`` present in the file.
68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 Such files are normally not touched under the assumption that they
69 Such files are normally not touched under the assumption that they
70 have mixed EOLs on purpose.
70 have mixed EOLs on purpose.
71
71
72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 ensure that converted files end with a EOL character (either ``\\n``
73 ensure that converted files end with a EOL character (either ``\\n``
74 or ``\\r\\n`` as per the configured patterns).
74 or ``\\r\\n`` as per the configured patterns).
75
75
76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 like the deprecated win32text extension does. This means that you can
77 like the deprecated win32text extension does. This means that you can
78 disable win32text and enable eol and your filters will still work. You
78 disable win32text and enable eol and your filters will still work. You
79 only need to these filters until you have prepared a ``.hgeol`` file.
79 only need to these filters until you have prepared a ``.hgeol`` file.
80
80
81 The ``win32text.forbid*`` hooks provided by the win32text extension
81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 have been unified into a single hook named ``eol.checkheadshook``. The
82 have been unified into a single hook named ``eol.checkheadshook``. The
83 hook will lookup the expected line endings from the ``.hgeol`` file,
83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 which means you must migrate to a ``.hgeol`` file first before using
84 which means you must migrate to a ``.hgeol`` file first before using
85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 invalid revisions will be pushed. To forbid them completely, use the
86 invalid revisions will be pushed. To forbid them completely, use the
87 ``eol.checkallhook`` hook. These hooks are best used as
87 ``eol.checkallhook`` hook. These hooks are best used as
88 ``pretxnchangegroup`` hooks.
88 ``pretxnchangegroup`` hooks.
89
89
90 See :hg:`help patterns` for more information about the glob patterns
90 See :hg:`help patterns` for more information about the glob patterns
91 used.
91 used.
92 """
92 """
93
93
94 from __future__ import absolute_import
94 from __future__ import absolute_import
95
95
96 import os
96 import os
97 import re
97 import re
98 from mercurial.i18n import _
98 from mercurial.i18n import _
99 from mercurial import (
99 from mercurial import (
100 config,
100 config,
101 error,
101 error,
102 extensions,
102 extensions,
103 match,
103 match,
104 util,
104 util,
105 )
105 )
106
106
107 # Note for extension authors: ONLY specify testedwith = 'internal' for
107 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
108 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
108 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
109 # be specifying the version(s) of Mercurial they are tested with, or
109 # be specifying the version(s) of Mercurial they are tested with, or
110 # leave the attribute unspecified.
110 # leave the attribute unspecified.
111 testedwith = 'internal'
111 testedwith = 'ships-with-hg-core'
112
112
113 # Matches a lone LF, i.e., one that is not part of CRLF.
113 # Matches a lone LF, i.e., one that is not part of CRLF.
114 singlelf = re.compile('(^|[^\r])\n')
114 singlelf = re.compile('(^|[^\r])\n')
115 # Matches a single EOL which can either be a CRLF where repeated CR
115 # Matches a single EOL which can either be a CRLF where repeated CR
116 # are removed or a LF. We do not care about old Macintosh files, so a
116 # are removed or a LF. We do not care about old Macintosh files, so a
117 # stray CR is an error.
117 # stray CR is an error.
118 eolre = re.compile('\r*\n')
118 eolre = re.compile('\r*\n')
119
119
120
120
121 def inconsistenteol(data):
121 def inconsistenteol(data):
122 return '\r\n' in data and singlelf.search(data)
122 return '\r\n' in data and singlelf.search(data)
123
123
124 def tolf(s, params, ui, **kwargs):
124 def tolf(s, params, ui, **kwargs):
125 """Filter to convert to LF EOLs."""
125 """Filter to convert to LF EOLs."""
126 if util.binary(s):
126 if util.binary(s):
127 return s
127 return s
128 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
128 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
129 return s
129 return s
130 if (ui.configbool('eol', 'fix-trailing-newline', False)
130 if (ui.configbool('eol', 'fix-trailing-newline', False)
131 and s and s[-1] != '\n'):
131 and s and s[-1] != '\n'):
132 s = s + '\n'
132 s = s + '\n'
133 return eolre.sub('\n', s)
133 return eolre.sub('\n', s)
134
134
135 def tocrlf(s, params, ui, **kwargs):
135 def tocrlf(s, params, ui, **kwargs):
136 """Filter to convert to CRLF EOLs."""
136 """Filter to convert to CRLF EOLs."""
137 if util.binary(s):
137 if util.binary(s):
138 return s
138 return s
139 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
139 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
140 return s
140 return s
141 if (ui.configbool('eol', 'fix-trailing-newline', False)
141 if (ui.configbool('eol', 'fix-trailing-newline', False)
142 and s and s[-1] != '\n'):
142 and s and s[-1] != '\n'):
143 s = s + '\n'
143 s = s + '\n'
144 return eolre.sub('\r\n', s)
144 return eolre.sub('\r\n', s)
145
145
146 def isbinary(s, params):
146 def isbinary(s, params):
147 """Filter to do nothing with the file."""
147 """Filter to do nothing with the file."""
148 return s
148 return s
149
149
150 filters = {
150 filters = {
151 'to-lf': tolf,
151 'to-lf': tolf,
152 'to-crlf': tocrlf,
152 'to-crlf': tocrlf,
153 'is-binary': isbinary,
153 'is-binary': isbinary,
154 # The following provide backwards compatibility with win32text
154 # The following provide backwards compatibility with win32text
155 'cleverencode:': tolf,
155 'cleverencode:': tolf,
156 'cleverdecode:': tocrlf
156 'cleverdecode:': tocrlf
157 }
157 }
158
158
159 class eolfile(object):
159 class eolfile(object):
160 def __init__(self, ui, root, data):
160 def __init__(self, ui, root, data):
161 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
161 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
162 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
162 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
163
163
164 self.cfg = config.config()
164 self.cfg = config.config()
165 # Our files should not be touched. The pattern must be
165 # Our files should not be touched. The pattern must be
166 # inserted first override a '** = native' pattern.
166 # inserted first override a '** = native' pattern.
167 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
167 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
168 # We can then parse the user's patterns.
168 # We can then parse the user's patterns.
169 self.cfg.parse('.hgeol', data)
169 self.cfg.parse('.hgeol', data)
170
170
171 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
171 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
172 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
172 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
173 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
173 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
174 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
174 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
175
175
176 include = []
176 include = []
177 exclude = []
177 exclude = []
178 self.patterns = []
178 for pattern, style in self.cfg.items('patterns'):
179 for pattern, style in self.cfg.items('patterns'):
179 key = style.upper()
180 key = style.upper()
180 if key == 'BIN':
181 if key == 'BIN':
181 exclude.append(pattern)
182 exclude.append(pattern)
182 else:
183 else:
183 include.append(pattern)
184 include.append(pattern)
185 m = match.match(root, '', [pattern])
186 self.patterns.append((pattern, key, m))
184 # This will match the files for which we need to care
187 # This will match the files for which we need to care
185 # about inconsistent newlines.
188 # about inconsistent newlines.
186 self.match = match.match(root, '', [], include, exclude)
189 self.match = match.match(root, '', [], include, exclude)
187
190
188 def copytoui(self, ui):
191 def copytoui(self, ui):
189 for pattern, style in self.cfg.items('patterns'):
192 for pattern, key, m in self.patterns:
190 key = style.upper()
191 try:
193 try:
192 ui.setconfig('decode', pattern, self._decode[key], 'eol')
194 ui.setconfig('decode', pattern, self._decode[key], 'eol')
193 ui.setconfig('encode', pattern, self._encode[key], 'eol')
195 ui.setconfig('encode', pattern, self._encode[key], 'eol')
194 except KeyError:
196 except KeyError:
195 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
197 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
196 % (style, self.cfg.source('patterns', pattern)))
198 % (key, self.cfg.source('patterns', pattern)))
197 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
199 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
198 for k, v in self.cfg.items('eol'):
200 for k, v in self.cfg.items('eol'):
199 ui.setconfig('eol', k, v, 'eol')
201 ui.setconfig('eol', k, v, 'eol')
200
202
201 def checkrev(self, repo, ctx, files):
203 def checkrev(self, repo, ctx, files):
202 failed = []
204 failed = []
203 for f in (files or ctx.files()):
205 for f in (files or ctx.files()):
204 if f not in ctx:
206 if f not in ctx:
205 continue
207 continue
206 for pattern, style in self.cfg.items('patterns'):
208 for pattern, key, m in self.patterns:
207 if not match.match(repo.root, '', [pattern])(f):
209 if not m(f):
208 continue
210 continue
209 target = self._encode[style.upper()]
211 target = self._encode[key]
210 data = ctx[f].data()
212 data = ctx[f].data()
211 if (target == "to-lf" and "\r\n" in data
213 if (target == "to-lf" and "\r\n" in data
212 or target == "to-crlf" and singlelf.search(data)):
214 or target == "to-crlf" and singlelf.search(data)):
213 failed.append((f, target, str(ctx)))
215 failed.append((f, target, str(ctx)))
214 break
216 break
215 return failed
217 return failed
216
218
217 def parseeol(ui, repo, nodes):
219 def parseeol(ui, repo, nodes):
218 try:
220 try:
219 for node in nodes:
221 for node in nodes:
220 try:
222 try:
221 if node is None:
223 if node is None:
222 # Cannot use workingctx.data() since it would load
224 # Cannot use workingctx.data() since it would load
223 # and cache the filters before we configure them.
225 # and cache the filters before we configure them.
224 data = repo.wfile('.hgeol').read()
226 data = repo.wfile('.hgeol').read()
225 else:
227 else:
226 data = repo[node]['.hgeol'].data()
228 data = repo[node]['.hgeol'].data()
227 return eolfile(ui, repo.root, data)
229 return eolfile(ui, repo.root, data)
228 except (IOError, LookupError):
230 except (IOError, LookupError):
229 pass
231 pass
230 except error.ParseError as inst:
232 except error.ParseError as inst:
231 ui.warn(_("warning: ignoring .hgeol file due to parse error "
233 ui.warn(_("warning: ignoring .hgeol file due to parse error "
232 "at %s: %s\n") % (inst.args[1], inst.args[0]))
234 "at %s: %s\n") % (inst.args[1], inst.args[0]))
233 return None
235 return None
234
236
235 def _checkhook(ui, repo, node, headsonly):
237 def _checkhook(ui, repo, node, headsonly):
236 # Get revisions to check and touched files at the same time
238 # Get revisions to check and touched files at the same time
237 files = set()
239 files = set()
238 revs = set()
240 revs = set()
239 for rev in xrange(repo[node].rev(), len(repo)):
241 for rev in xrange(repo[node].rev(), len(repo)):
240 revs.add(rev)
242 revs.add(rev)
241 if headsonly:
243 if headsonly:
242 ctx = repo[rev]
244 ctx = repo[rev]
243 files.update(ctx.files())
245 files.update(ctx.files())
244 for pctx in ctx.parents():
246 for pctx in ctx.parents():
245 revs.discard(pctx.rev())
247 revs.discard(pctx.rev())
246 failed = []
248 failed = []
247 for rev in revs:
249 for rev in revs:
248 ctx = repo[rev]
250 ctx = repo[rev]
249 eol = parseeol(ui, repo, [ctx.node()])
251 eol = parseeol(ui, repo, [ctx.node()])
250 if eol:
252 if eol:
251 failed.extend(eol.checkrev(repo, ctx, files))
253 failed.extend(eol.checkrev(repo, ctx, files))
252
254
253 if failed:
255 if failed:
254 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
256 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
255 msgs = []
257 msgs = []
256 for f, target, node in sorted(failed):
258 for f, target, node in sorted(failed):
257 msgs.append(_(" %s in %s should not have %s line endings") %
259 msgs.append(_(" %s in %s should not have %s line endings") %
258 (f, node, eols[target]))
260 (f, node, eols[target]))
259 raise error.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
261 raise error.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
260
262
261 def checkallhook(ui, repo, node, hooktype, **kwargs):
263 def checkallhook(ui, repo, node, hooktype, **kwargs):
262 """verify that files have expected EOLs"""
264 """verify that files have expected EOLs"""
263 _checkhook(ui, repo, node, False)
265 _checkhook(ui, repo, node, False)
264
266
265 def checkheadshook(ui, repo, node, hooktype, **kwargs):
267 def checkheadshook(ui, repo, node, hooktype, **kwargs):
266 """verify that files have expected EOLs"""
268 """verify that files have expected EOLs"""
267 _checkhook(ui, repo, node, True)
269 _checkhook(ui, repo, node, True)
268
270
269 # "checkheadshook" used to be called "hook"
271 # "checkheadshook" used to be called "hook"
270 hook = checkheadshook
272 hook = checkheadshook
271
273
272 def preupdate(ui, repo, hooktype, parent1, parent2):
274 def preupdate(ui, repo, hooktype, parent1, parent2):
273 repo.loadeol([parent1])
275 repo.loadeol([parent1])
274 return False
276 return False
275
277
276 def uisetup(ui):
278 def uisetup(ui):
277 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
279 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
278
280
279 def extsetup(ui):
281 def extsetup(ui):
280 try:
282 try:
281 extensions.find('win32text')
283 extensions.find('win32text')
282 ui.warn(_("the eol extension is incompatible with the "
284 ui.warn(_("the eol extension is incompatible with the "
283 "win32text extension\n"))
285 "win32text extension\n"))
284 except KeyError:
286 except KeyError:
285 pass
287 pass
286
288
287
289
288 def reposetup(ui, repo):
290 def reposetup(ui, repo):
289 uisetup(repo.ui)
291 uisetup(repo.ui)
290
292
291 if not repo.local():
293 if not repo.local():
292 return
294 return
293 for name, fn in filters.iteritems():
295 for name, fn in filters.iteritems():
294 repo.adddatafilter(name, fn)
296 repo.adddatafilter(name, fn)
295
297
296 ui.setconfig('patch', 'eol', 'auto', 'eol')
298 ui.setconfig('patch', 'eol', 'auto', 'eol')
297
299
298 class eolrepo(repo.__class__):
300 class eolrepo(repo.__class__):
299
301
300 def loadeol(self, nodes):
302 def loadeol(self, nodes):
301 eol = parseeol(self.ui, self, nodes)
303 eol = parseeol(self.ui, self, nodes)
302 if eol is None:
304 if eol is None:
303 return None
305 return None
304 eol.copytoui(self.ui)
306 eol.copytoui(self.ui)
305 return eol.match
307 return eol.match
306
308
307 def _hgcleardirstate(self):
309 def _hgcleardirstate(self):
308 self._eolfile = self.loadeol([None, 'tip'])
310 self._eolmatch = self.loadeol([None, 'tip'])
309 if not self._eolfile:
311 if not self._eolmatch:
310 self._eolfile = util.never
312 self._eolmatch = util.never
311 return
313 return
312
314
315 oldeol = None
313 try:
316 try:
314 cachemtime = os.path.getmtime(self.join("eol.cache"))
317 cachemtime = os.path.getmtime(self.join("eol.cache"))
315 except OSError:
318 except OSError:
316 cachemtime = 0
319 cachemtime = 0
320 else:
321 olddata = self.vfs.read("eol.cache")
322 if olddata:
323 oldeol = eolfile(self.ui, self.root, olddata)
317
324
318 try:
325 try:
319 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
326 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
320 except OSError:
327 except OSError:
321 eolmtime = 0
328 eolmtime = 0
322
329
323 if eolmtime > cachemtime:
330 if eolmtime > cachemtime:
324 self.ui.debug("eol: detected change in .hgeol\n")
331 self.ui.debug("eol: detected change in .hgeol\n")
332
333 hgeoldata = self.wvfs.read('.hgeol')
334 neweol = eolfile(self.ui, self.root, hgeoldata)
335
325 wlock = None
336 wlock = None
326 try:
337 try:
327 wlock = self.wlock()
338 wlock = self.wlock()
328 for f in self.dirstate:
339 for f in self.dirstate:
329 if self.dirstate[f] == 'n':
340 if self.dirstate[f] != 'n':
330 # all normal files need to be looked at
341 continue
331 # again since the new .hgeol file might no
342 if oldeol is not None:
332 # longer match a file it matched before
343 if not oldeol.match(f) and not neweol.match(f):
333 self.dirstate.normallookup(f)
344 continue
334 # Create or touch the cache to update mtime
345 oldkey = None
335 self.vfs("eol.cache", "w").close()
346 for pattern, key, m in oldeol.patterns:
336 wlock.release()
347 if m(f):
348 oldkey = key
349 break
350 newkey = None
351 for pattern, key, m in neweol.patterns:
352 if m(f):
353 newkey = key
354 break
355 if oldkey == newkey:
356 continue
357 # all normal files need to be looked at again since
358 # the new .hgeol file specify a different filter
359 self.dirstate.normallookup(f)
360 # Write the cache to update mtime and cache .hgeol
361 with self.vfs("eol.cache", "w") as f:
362 f.write(hgeoldata)
337 except error.LockUnavailable:
363 except error.LockUnavailable:
338 # If we cannot lock the repository and clear the
364 # If we cannot lock the repository and clear the
339 # dirstate, then a commit might not see all files
365 # dirstate, then a commit might not see all files
340 # as modified. But if we cannot lock the
366 # as modified. But if we cannot lock the
341 # repository, then we can also not make a commit,
367 # repository, then we can also not make a commit,
342 # so ignore the error.
368 # so ignore the error.
343 pass
369 pass
370 finally:
371 if wlock is not None:
372 wlock.release()
344
373
345 def commitctx(self, ctx, haserror=False):
374 def commitctx(self, ctx, haserror=False):
346 for f in sorted(ctx.added() + ctx.modified()):
375 for f in sorted(ctx.added() + ctx.modified()):
347 if not self._eolfile(f):
376 if not self._eolmatch(f):
348 continue
377 continue
349 fctx = ctx[f]
378 fctx = ctx[f]
350 if fctx is None:
379 if fctx is None:
351 continue
380 continue
352 data = fctx.data()
381 data = fctx.data()
353 if util.binary(data):
382 if util.binary(data):
354 # We should not abort here, since the user should
383 # We should not abort here, since the user should
355 # be able to say "** = native" to automatically
384 # be able to say "** = native" to automatically
356 # have all non-binary files taken care of.
385 # have all non-binary files taken care of.
357 continue
386 continue
358 if inconsistenteol(data):
387 if inconsistenteol(data):
359 raise error.Abort(_("inconsistent newline style "
388 raise error.Abort(_("inconsistent newline style "
360 "in %s\n") % f)
389 "in %s\n") % f)
361 return super(eolrepo, self).commitctx(ctx, haserror)
390 return super(eolrepo, self).commitctx(ctx, haserror)
362 repo.__class__ = eolrepo
391 repo.__class__ = eolrepo
363 repo._hgcleardirstate()
392 repo._hgcleardirstate()
@@ -1,384 +1,392 b''
1 # extdiff.py - external diff program support for mercurial
1 # extdiff.py - external diff program support for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 '''command to allow external programs to compare revisions
8 '''command to allow external programs to compare revisions
9
9
10 The extdiff Mercurial extension allows you to use external programs
10 The extdiff Mercurial extension allows you to use external programs
11 to compare revisions, or revision with working directory. The external
11 to compare revisions, or revision with working directory. The external
12 diff programs are called with a configurable set of options and two
12 diff programs are called with a configurable set of options and two
13 non-option arguments: paths to directories containing snapshots of
13 non-option arguments: paths to directories containing snapshots of
14 files to compare.
14 files to compare.
15
15
16 The extdiff extension also allows you to configure new diff commands, so
16 The extdiff extension also allows you to configure new diff commands, so
17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
18
18
19 [extdiff]
19 [extdiff]
20 # add new command that runs GNU diff(1) in 'context diff' mode
20 # add new command that runs GNU diff(1) in 'context diff' mode
21 cdiff = gdiff -Nprc5
21 cdiff = gdiff -Nprc5
22 ## or the old way:
22 ## or the old way:
23 #cmd.cdiff = gdiff
23 #cmd.cdiff = gdiff
24 #opts.cdiff = -Nprc5
24 #opts.cdiff = -Nprc5
25
25
26 # add new command called meld, runs meld (no need to name twice). If
26 # add new command called meld, runs meld (no need to name twice). If
27 # the meld executable is not available, the meld tool in [merge-tools]
27 # the meld executable is not available, the meld tool in [merge-tools]
28 # will be used, if available
28 # will be used, if available
29 meld =
29 meld =
30
30
31 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
31 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
32 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
32 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
33 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
33 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
34 # your .vimrc
34 # your .vimrc
35 vimdiff = gvim -f "+next" \\
35 vimdiff = gvim -f "+next" \\
36 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
36 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
37
37
38 Tool arguments can include variables that are expanded at runtime::
38 Tool arguments can include variables that are expanded at runtime::
39
39
40 $parent1, $plabel1 - filename, descriptive label of first parent
40 $parent1, $plabel1 - filename, descriptive label of first parent
41 $child, $clabel - filename, descriptive label of child revision
41 $child, $clabel - filename, descriptive label of child revision
42 $parent2, $plabel2 - filename, descriptive label of second parent
42 $parent2, $plabel2 - filename, descriptive label of second parent
43 $root - repository root
43 $root - repository root
44 $parent is an alias for $parent1.
44 $parent is an alias for $parent1.
45
45
46 The extdiff extension will look in your [diff-tools] and [merge-tools]
46 The extdiff extension will look in your [diff-tools] and [merge-tools]
47 sections for diff tool arguments, when none are specified in [extdiff].
47 sections for diff tool arguments, when none are specified in [extdiff].
48
48
49 ::
49 ::
50
50
51 [extdiff]
51 [extdiff]
52 kdiff3 =
52 kdiff3 =
53
53
54 [diff-tools]
54 [diff-tools]
55 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
55 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
56
56
57 You can use -I/-X and list of file or directory names like normal
57 You can use -I/-X and list of file or directory names like normal
58 :hg:`diff` command. The extdiff extension makes snapshots of only
58 :hg:`diff` command. The extdiff extension makes snapshots of only
59 needed files, so running the external diff program will actually be
59 needed files, so running the external diff program will actually be
60 pretty fast (at least faster than having to compare the entire tree).
60 pretty fast (at least faster than having to compare the entire tree).
61 '''
61 '''
62
62
63 from __future__ import absolute_import
63 from __future__ import absolute_import
64
64
65 import os
65 import os
66 import re
66 import re
67 import shlex
67 import shlex
68 import shutil
68 import shutil
69 import tempfile
69 import tempfile
70 from mercurial.i18n import _
70 from mercurial.i18n import _
71 from mercurial.node import (
71 from mercurial.node import (
72 nullid,
72 nullid,
73 short,
73 short,
74 )
74 )
75 from mercurial import (
75 from mercurial import (
76 archival,
76 archival,
77 cmdutil,
77 cmdutil,
78 commands,
78 commands,
79 error,
79 error,
80 filemerge,
80 filemerge,
81 scmutil,
81 scmutil,
82 util,
82 util,
83 )
83 )
84
84
85 cmdtable = {}
85 cmdtable = {}
86 command = cmdutil.command(cmdtable)
86 command = cmdutil.command(cmdtable)
87 # Note for extension authors: ONLY specify testedwith = 'internal' for
87 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
88 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
88 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
89 # be specifying the version(s) of Mercurial they are tested with, or
89 # be specifying the version(s) of Mercurial they are tested with, or
90 # leave the attribute unspecified.
90 # leave the attribute unspecified.
91 testedwith = 'internal'
91 testedwith = 'ships-with-hg-core'
92
92
93 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
93 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
94 '''snapshot files as of some revision
94 '''snapshot files as of some revision
95 if not using snapshot, -I/-X does not work and recursive diff
95 if not using snapshot, -I/-X does not work and recursive diff
96 in tools like kdiff3 and meld displays too many files.'''
96 in tools like kdiff3 and meld displays too many files.'''
97 dirname = os.path.basename(repo.root)
97 dirname = os.path.basename(repo.root)
98 if dirname == "":
98 if dirname == "":
99 dirname = "root"
99 dirname = "root"
100 if node is not None:
100 if node is not None:
101 dirname = '%s.%s' % (dirname, short(node))
101 dirname = '%s.%s' % (dirname, short(node))
102 base = os.path.join(tmproot, dirname)
102 base = os.path.join(tmproot, dirname)
103 os.mkdir(base)
103 os.mkdir(base)
104 fns_and_mtime = []
104 fns_and_mtime = []
105
105
106 if node is not None:
106 if node is not None:
107 ui.note(_('making snapshot of %d files from rev %s\n') %
107 ui.note(_('making snapshot of %d files from rev %s\n') %
108 (len(files), short(node)))
108 (len(files), short(node)))
109 else:
109 else:
110 ui.note(_('making snapshot of %d files from working directory\n') %
110 ui.note(_('making snapshot of %d files from working directory\n') %
111 (len(files)))
111 (len(files)))
112
112
113 if files:
113 if files:
114 repo.ui.setconfig("ui", "archivemeta", False)
114 repo.ui.setconfig("ui", "archivemeta", False)
115
115
116 archival.archive(repo, base, node, 'files',
116 archival.archive(repo, base, node, 'files',
117 matchfn=scmutil.matchfiles(repo, files),
117 matchfn=scmutil.matchfiles(repo, files),
118 subrepos=listsubrepos)
118 subrepos=listsubrepos)
119
119
120 for fn in sorted(files):
120 for fn in sorted(files):
121 wfn = util.pconvert(fn)
121 wfn = util.pconvert(fn)
122 ui.note(' %s\n' % wfn)
122 ui.note(' %s\n' % wfn)
123
123
124 if node is None:
124 if node is None:
125 dest = os.path.join(base, wfn)
125 dest = os.path.join(base, wfn)
126
126
127 fns_and_mtime.append((dest, repo.wjoin(fn),
127 fns_and_mtime.append((dest, repo.wjoin(fn),
128 os.lstat(dest).st_mtime))
128 os.lstat(dest).st_mtime))
129 return dirname, fns_and_mtime
129 return dirname, fns_and_mtime
130
130
131 def dodiff(ui, repo, cmdline, pats, opts):
131 def dodiff(ui, repo, cmdline, pats, opts):
132 '''Do the actual diff:
132 '''Do the actual diff:
133
133
134 - copy to a temp structure if diffing 2 internal revisions
134 - copy to a temp structure if diffing 2 internal revisions
135 - copy to a temp structure if diffing working revision with
135 - copy to a temp structure if diffing working revision with
136 another one and more than 1 file is changed
136 another one and more than 1 file is changed
137 - just invoke the diff for a single file in the working dir
137 - just invoke the diff for a single file in the working dir
138 '''
138 '''
139
139
140 revs = opts.get('rev')
140 revs = opts.get('rev')
141 change = opts.get('change')
141 change = opts.get('change')
142 do3way = '$parent2' in cmdline
142 do3way = '$parent2' in cmdline
143
143
144 if revs and change:
144 if revs and change:
145 msg = _('cannot specify --rev and --change at the same time')
145 msg = _('cannot specify --rev and --change at the same time')
146 raise error.Abort(msg)
146 raise error.Abort(msg)
147 elif change:
147 elif change:
148 node2 = scmutil.revsingle(repo, change, None).node()
148 node2 = scmutil.revsingle(repo, change, None).node()
149 node1a, node1b = repo.changelog.parents(node2)
149 node1a, node1b = repo.changelog.parents(node2)
150 else:
150 else:
151 node1a, node2 = scmutil.revpair(repo, revs)
151 node1a, node2 = scmutil.revpair(repo, revs)
152 if not revs:
152 if not revs:
153 node1b = repo.dirstate.p2()
153 node1b = repo.dirstate.p2()
154 else:
154 else:
155 node1b = nullid
155 node1b = nullid
156
156
157 # Disable 3-way merge if there is only one parent
157 # Disable 3-way merge if there is only one parent
158 if do3way:
158 if do3way:
159 if node1b == nullid:
159 if node1b == nullid:
160 do3way = False
160 do3way = False
161
161
162 subrepos=opts.get('subrepos')
162 subrepos=opts.get('subrepos')
163
163
164 matcher = scmutil.match(repo[node2], pats, opts)
164 matcher = scmutil.match(repo[node2], pats, opts)
165
165
166 if opts.get('patch'):
166 if opts.get('patch'):
167 if subrepos:
167 if subrepos:
168 raise error.Abort(_('--patch cannot be used with --subrepos'))
168 raise error.Abort(_('--patch cannot be used with --subrepos'))
169 if node2 is None:
169 if node2 is None:
170 raise error.Abort(_('--patch requires two revisions'))
170 raise error.Abort(_('--patch requires two revisions'))
171 else:
171 else:
172 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
172 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
173 listsubrepos=subrepos)[:3])
173 listsubrepos=subrepos)[:3])
174 if do3way:
174 if do3way:
175 mod_b, add_b, rem_b = map(set,
175 mod_b, add_b, rem_b = map(set,
176 repo.status(node1b, node2, matcher,
176 repo.status(node1b, node2, matcher,
177 listsubrepos=subrepos)[:3])
177 listsubrepos=subrepos)[:3])
178 else:
178 else:
179 mod_b, add_b, rem_b = set(), set(), set()
179 mod_b, add_b, rem_b = set(), set(), set()
180 modadd = mod_a | add_a | mod_b | add_b
180 modadd = mod_a | add_a | mod_b | add_b
181 common = modadd | rem_a | rem_b
181 common = modadd | rem_a | rem_b
182 if not common:
182 if not common:
183 return 0
183 return 0
184
184
185 tmproot = tempfile.mkdtemp(prefix='extdiff.')
185 tmproot = tempfile.mkdtemp(prefix='extdiff.')
186 try:
186 try:
187 if not opts.get('patch'):
187 if not opts.get('patch'):
188 # Always make a copy of node1a (and node1b, if applicable)
188 # Always make a copy of node1a (and node1b, if applicable)
189 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
189 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
190 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
190 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
191 subrepos)[0]
191 subrepos)[0]
192 rev1a = '@%d' % repo[node1a].rev()
192 rev1a = '@%d' % repo[node1a].rev()
193 if do3way:
193 if do3way:
194 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
194 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
195 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
195 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
196 subrepos)[0]
196 subrepos)[0]
197 rev1b = '@%d' % repo[node1b].rev()
197 rev1b = '@%d' % repo[node1b].rev()
198 else:
198 else:
199 dir1b = None
199 dir1b = None
200 rev1b = ''
200 rev1b = ''
201
201
202 fns_and_mtime = []
202 fns_and_mtime = []
203
203
204 # If node2 in not the wc or there is >1 change, copy it
204 # If node2 in not the wc or there is >1 change, copy it
205 dir2root = ''
205 dir2root = ''
206 rev2 = ''
206 rev2 = ''
207 if node2:
207 if node2:
208 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
208 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
209 rev2 = '@%d' % repo[node2].rev()
209 rev2 = '@%d' % repo[node2].rev()
210 elif len(common) > 1:
210 elif len(common) > 1:
211 #we only actually need to get the files to copy back to
211 #we only actually need to get the files to copy back to
212 #the working dir in this case (because the other cases
212 #the working dir in this case (because the other cases
213 #are: diffing 2 revisions or single file -- in which case
213 #are: diffing 2 revisions or single file -- in which case
214 #the file is already directly passed to the diff tool).
214 #the file is already directly passed to the diff tool).
215 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot,
215 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot,
216 subrepos)
216 subrepos)
217 else:
217 else:
218 # This lets the diff tool open the changed file directly
218 # This lets the diff tool open the changed file directly
219 dir2 = ''
219 dir2 = ''
220 dir2root = repo.root
220 dir2root = repo.root
221
221
222 label1a = rev1a
222 label1a = rev1a
223 label1b = rev1b
223 label1b = rev1b
224 label2 = rev2
224 label2 = rev2
225
225
226 # If only one change, diff the files instead of the directories
226 # If only one change, diff the files instead of the directories
227 # Handle bogus modifies correctly by checking if the files exist
227 # Handle bogus modifies correctly by checking if the files exist
228 if len(common) == 1:
228 if len(common) == 1:
229 common_file = util.localpath(common.pop())
229 common_file = util.localpath(common.pop())
230 dir1a = os.path.join(tmproot, dir1a, common_file)
230 dir1a = os.path.join(tmproot, dir1a, common_file)
231 label1a = common_file + rev1a
231 label1a = common_file + rev1a
232 if not os.path.isfile(dir1a):
232 if not os.path.isfile(dir1a):
233 dir1a = os.devnull
233 dir1a = os.devnull
234 if do3way:
234 if do3way:
235 dir1b = os.path.join(tmproot, dir1b, common_file)
235 dir1b = os.path.join(tmproot, dir1b, common_file)
236 label1b = common_file + rev1b
236 label1b = common_file + rev1b
237 if not os.path.isfile(dir1b):
237 if not os.path.isfile(dir1b):
238 dir1b = os.devnull
238 dir1b = os.devnull
239 dir2 = os.path.join(dir2root, dir2, common_file)
239 dir2 = os.path.join(dir2root, dir2, common_file)
240 label2 = common_file + rev2
240 label2 = common_file + rev2
241 else:
241 else:
242 template = 'hg-%h.patch'
242 template = 'hg-%h.patch'
243 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
243 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
244 template=repo.vfs.reljoin(tmproot, template),
244 template=repo.vfs.reljoin(tmproot, template),
245 match=matcher)
245 match=matcher)
246 label1a = cmdutil.makefilename(repo, template, node1a)
246 label1a = cmdutil.makefilename(repo, template, node1a)
247 label2 = cmdutil.makefilename(repo, template, node2)
247 label2 = cmdutil.makefilename(repo, template, node2)
248 dir1a = repo.vfs.reljoin(tmproot, label1a)
248 dir1a = repo.vfs.reljoin(tmproot, label1a)
249 dir2 = repo.vfs.reljoin(tmproot, label2)
249 dir2 = repo.vfs.reljoin(tmproot, label2)
250 dir1b = None
250 dir1b = None
251 label1b = None
251 label1b = None
252 fns_and_mtime = []
252 fns_and_mtime = []
253
253
254 # Function to quote file/dir names in the argument string.
254 # Function to quote file/dir names in the argument string.
255 # When not operating in 3-way mode, an empty string is
255 # When not operating in 3-way mode, an empty string is
256 # returned for parent2
256 # returned for parent2
257 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
257 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
258 'plabel1': label1a, 'plabel2': label1b,
258 'plabel1': label1a, 'plabel2': label1b,
259 'clabel': label2, 'child': dir2,
259 'clabel': label2, 'child': dir2,
260 'root': repo.root}
260 'root': repo.root}
261 def quote(match):
261 def quote(match):
262 pre = match.group(2)
262 pre = match.group(2)
263 key = match.group(3)
263 key = match.group(3)
264 if not do3way and key == 'parent2':
264 if not do3way and key == 'parent2':
265 return pre
265 return pre
266 return pre + util.shellquote(replace[key])
266 return pre + util.shellquote(replace[key])
267
267
268 # Match parent2 first, so 'parent1?' will match both parent1 and parent
268 # Match parent2 first, so 'parent1?' will match both parent1 and parent
269 regex = (r'''(['"]?)([^\s'"$]*)'''
269 regex = (r'''(['"]?)([^\s'"$]*)'''
270 r'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
270 r'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
271 if not do3way and not re.search(regex, cmdline):
271 if not do3way and not re.search(regex, cmdline):
272 cmdline += ' $parent1 $child'
272 cmdline += ' $parent1 $child'
273 cmdline = re.sub(regex, quote, cmdline)
273 cmdline = re.sub(regex, quote, cmdline)
274
274
275 ui.debug('running %r in %s\n' % (cmdline, tmproot))
275 ui.debug('running %r in %s\n' % (cmdline, tmproot))
276 ui.system(cmdline, cwd=tmproot)
276 ui.system(cmdline, cwd=tmproot)
277
277
278 for copy_fn, working_fn, mtime in fns_and_mtime:
278 for copy_fn, working_fn, mtime in fns_and_mtime:
279 if os.lstat(copy_fn).st_mtime != mtime:
279 if os.lstat(copy_fn).st_mtime != mtime:
280 ui.debug('file changed while diffing. '
280 ui.debug('file changed while diffing. '
281 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
281 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
282 util.copyfile(copy_fn, working_fn)
282 util.copyfile(copy_fn, working_fn)
283
283
284 return 1
284 return 1
285 finally:
285 finally:
286 ui.note(_('cleaning up temp directory\n'))
286 ui.note(_('cleaning up temp directory\n'))
287 shutil.rmtree(tmproot)
287 shutil.rmtree(tmproot)
288
288
289 extdiffopts = [
289 extdiffopts = [
290 ('o', 'option', [],
290 ('o', 'option', [],
291 _('pass option to comparison program'), _('OPT')),
291 _('pass option to comparison program'), _('OPT')),
292 ('r', 'rev', [], _('revision'), _('REV')),
292 ('r', 'rev', [], _('revision'), _('REV')),
293 ('c', 'change', '', _('change made by revision'), _('REV')),
293 ('c', 'change', '', _('change made by revision'), _('REV')),
294 ('', 'patch', None, _('compare patches for two revisions'))
294 ('', 'patch', None, _('compare patches for two revisions'))
295 ] + commands.walkopts + commands.subrepoopts
295 ] + commands.walkopts + commands.subrepoopts
296
296
297 @command('extdiff',
297 @command('extdiff',
298 [('p', 'program', '', _('comparison program to run'), _('CMD')),
298 [('p', 'program', '', _('comparison program to run'), _('CMD')),
299 ] + extdiffopts,
299 ] + extdiffopts,
300 _('hg extdiff [OPT]... [FILE]...'),
300 _('hg extdiff [OPT]... [FILE]...'),
301 inferrepo=True)
301 inferrepo=True)
302 def extdiff(ui, repo, *pats, **opts):
302 def extdiff(ui, repo, *pats, **opts):
303 '''use external program to diff repository (or selected files)
303 '''use external program to diff repository (or selected files)
304
304
305 Show differences between revisions for the specified files, using
305 Show differences between revisions for the specified files, using
306 an external program. The default program used is diff, with
306 an external program. The default program used is diff, with
307 default options "-Npru".
307 default options "-Npru".
308
308
309 To select a different program, use the -p/--program option. The
309 To select a different program, use the -p/--program option. The
310 program will be passed the names of two directories to compare. To
310 program will be passed the names of two directories to compare. To
311 pass additional options to the program, use -o/--option. These
311 pass additional options to the program, use -o/--option. These
312 will be passed before the names of the directories to compare.
312 will be passed before the names of the directories to compare.
313
313
314 When two revision arguments are given, then changes are shown
314 When two revision arguments are given, then changes are shown
315 between those revisions. If only one revision is specified then
315 between those revisions. If only one revision is specified then
316 that revision is compared to the working directory, and, when no
316 that revision is compared to the working directory, and, when no
317 revisions are specified, the working directory files are compared
317 revisions are specified, the working directory files are compared
318 to its parent.'''
318 to its parent.'''
319 program = opts.get('program')
319 program = opts.get('program')
320 option = opts.get('option')
320 option = opts.get('option')
321 if not program:
321 if not program:
322 program = 'diff'
322 program = 'diff'
323 option = option or ['-Npru']
323 option = option or ['-Npru']
324 cmdline = ' '.join(map(util.shellquote, [program] + option))
324 cmdline = ' '.join(map(util.shellquote, [program] + option))
325 return dodiff(ui, repo, cmdline, pats, opts)
325 return dodiff(ui, repo, cmdline, pats, opts)
326
326
327 class savedcmd(object):
328 """use external program to diff repository (or selected files)
329
330 Show differences between revisions for the specified files, using
331 the following program::
332
333 %(path)s
334
335 When two revision arguments are given, then changes are shown
336 between those revisions. If only one revision is specified then
337 that revision is compared to the working directory, and, when no
338 revisions are specified, the working directory files are compared
339 to its parent.
340 """
341
342 def __init__(self, path, cmdline):
343 # We can't pass non-ASCII through docstrings (and path is
344 # in an unknown encoding anyway)
345 docpath = path.encode("string-escape")
346 self.__doc__ = self.__doc__ % {'path': util.uirepr(docpath)}
347 self._cmdline = cmdline
348
349 def __call__(self, ui, repo, *pats, **opts):
350 options = ' '.join(map(util.shellquote, opts['option']))
351 if options:
352 options = ' ' + options
353 return dodiff(ui, repo, self._cmdline + options, pats, opts)
354
327 def uisetup(ui):
355 def uisetup(ui):
328 for cmd, path in ui.configitems('extdiff'):
356 for cmd, path in ui.configitems('extdiff'):
329 path = util.expandpath(path)
357 path = util.expandpath(path)
330 if cmd.startswith('cmd.'):
358 if cmd.startswith('cmd.'):
331 cmd = cmd[4:]
359 cmd = cmd[4:]
332 if not path:
360 if not path:
333 path = util.findexe(cmd)
361 path = util.findexe(cmd)
334 if path is None:
362 if path is None:
335 path = filemerge.findexternaltool(ui, cmd) or cmd
363 path = filemerge.findexternaltool(ui, cmd) or cmd
336 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
364 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
337 cmdline = util.shellquote(path)
365 cmdline = util.shellquote(path)
338 if diffopts:
366 if diffopts:
339 cmdline += ' ' + diffopts
367 cmdline += ' ' + diffopts
340 elif cmd.startswith('opts.'):
368 elif cmd.startswith('opts.'):
341 continue
369 continue
342 else:
370 else:
343 if path:
371 if path:
344 # case "cmd = path opts"
372 # case "cmd = path opts"
345 cmdline = path
373 cmdline = path
346 diffopts = len(shlex.split(cmdline)) > 1
374 diffopts = len(shlex.split(cmdline)) > 1
347 else:
375 else:
348 # case "cmd ="
376 # case "cmd ="
349 path = util.findexe(cmd)
377 path = util.findexe(cmd)
350 if path is None:
378 if path is None:
351 path = filemerge.findexternaltool(ui, cmd) or cmd
379 path = filemerge.findexternaltool(ui, cmd) or cmd
352 cmdline = util.shellquote(path)
380 cmdline = util.shellquote(path)
353 diffopts = False
381 diffopts = False
354 # look for diff arguments in [diff-tools] then [merge-tools]
382 # look for diff arguments in [diff-tools] then [merge-tools]
355 if not diffopts:
383 if not diffopts:
356 args = ui.config('diff-tools', cmd+'.diffargs') or \
384 args = ui.config('diff-tools', cmd+'.diffargs') or \
357 ui.config('merge-tools', cmd+'.diffargs')
385 ui.config('merge-tools', cmd+'.diffargs')
358 if args:
386 if args:
359 cmdline += ' ' + args
387 cmdline += ' ' + args
360 def save(cmdline):
388 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
361 '''use closure to save diff command to use'''
389 inferrepo=True)(savedcmd(path, cmdline))
362 def mydiff(ui, repo, *pats, **opts):
363 options = ' '.join(map(util.shellquote, opts['option']))
364 if options:
365 options = ' ' + options
366 return dodiff(ui, repo, cmdline + options, pats, opts)
367 # We can't pass non-ASCII through docstrings (and path is
368 # in an unknown encoding anyway)
369 docpath = path.encode("string-escape")
370 mydiff.__doc__ = '''\
371 use %(path)s to diff repository (or selected files)
372
390
373 Show differences between revisions for the specified files, using
391 # tell hggettext to extract docstrings from these functions:
374 the %(path)s program.
392 i18nfunctions = [savedcmd]
375
376 When two revision arguments are given, then changes are shown
377 between those revisions. If only one revision is specified then
378 that revision is compared to the working directory, and, when no
379 revisions are specified, the working directory files are compared
380 to its parent.\
381 ''' % {'path': util.uirepr(docpath)}
382 return mydiff
383 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
384 inferrepo=True)(save(cmdline))
@@ -1,165 +1,165 b''
1 # fetch.py - pull and merge remote changes
1 # fetch.py - pull and merge remote changes
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 '''pull, update and merge in one command (DEPRECATED)'''
8 '''pull, update and merge in one command (DEPRECATED)'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.node import (
13 from mercurial.node import (
14 short,
14 short,
15 )
15 )
16 from mercurial import (
16 from mercurial import (
17 cmdutil,
17 cmdutil,
18 commands,
18 commands,
19 error,
19 error,
20 exchange,
20 exchange,
21 hg,
21 hg,
22 lock,
22 lock,
23 util,
23 util,
24 )
24 )
25
25
26 release = lock.release
26 release = lock.release
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29 # Note for extension authors: ONLY specify testedwith = 'internal' for
29 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 # be specifying the version(s) of Mercurial they are tested with, or
31 # be specifying the version(s) of Mercurial they are tested with, or
32 # leave the attribute unspecified.
32 # leave the attribute unspecified.
33 testedwith = 'internal'
33 testedwith = 'ships-with-hg-core'
34
34
35 @command('fetch',
35 @command('fetch',
36 [('r', 'rev', [],
36 [('r', 'rev', [],
37 _('a specific revision you would like to pull'), _('REV')),
37 _('a specific revision you would like to pull'), _('REV')),
38 ('e', 'edit', None, _('invoke editor on commit messages')),
38 ('e', 'edit', None, _('invoke editor on commit messages')),
39 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
39 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
40 ('', 'switch-parent', None, _('switch parents when merging')),
40 ('', 'switch-parent', None, _('switch parents when merging')),
41 ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
41 ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
42 _('hg fetch [SOURCE]'))
42 _('hg fetch [SOURCE]'))
43 def fetch(ui, repo, source='default', **opts):
43 def fetch(ui, repo, source='default', **opts):
44 '''pull changes from a remote repository, merge new changes if needed.
44 '''pull changes from a remote repository, merge new changes if needed.
45
45
46 This finds all changes from the repository at the specified path
46 This finds all changes from the repository at the specified path
47 or URL and adds them to the local repository.
47 or URL and adds them to the local repository.
48
48
49 If the pulled changes add a new branch head, the head is
49 If the pulled changes add a new branch head, the head is
50 automatically merged, and the result of the merge is committed.
50 automatically merged, and the result of the merge is committed.
51 Otherwise, the working directory is updated to include the new
51 Otherwise, the working directory is updated to include the new
52 changes.
52 changes.
53
53
54 When a merge is needed, the working directory is first updated to
54 When a merge is needed, the working directory is first updated to
55 the newly pulled changes. Local changes are then merged into the
55 the newly pulled changes. Local changes are then merged into the
56 pulled changes. To switch the merge order, use --switch-parent.
56 pulled changes. To switch the merge order, use --switch-parent.
57
57
58 See :hg:`help dates` for a list of formats valid for -d/--date.
58 See :hg:`help dates` for a list of formats valid for -d/--date.
59
59
60 Returns 0 on success.
60 Returns 0 on success.
61 '''
61 '''
62
62
63 date = opts.get('date')
63 date = opts.get('date')
64 if date:
64 if date:
65 opts['date'] = util.parsedate(date)
65 opts['date'] = util.parsedate(date)
66
66
67 parent, _p2 = repo.dirstate.parents()
67 parent, _p2 = repo.dirstate.parents()
68 branch = repo.dirstate.branch()
68 branch = repo.dirstate.branch()
69 try:
69 try:
70 branchnode = repo.branchtip(branch)
70 branchnode = repo.branchtip(branch)
71 except error.RepoLookupError:
71 except error.RepoLookupError:
72 branchnode = None
72 branchnode = None
73 if parent != branchnode:
73 if parent != branchnode:
74 raise error.Abort(_('working directory not at branch tip'),
74 raise error.Abort(_('working directory not at branch tip'),
75 hint=_("use 'hg update' to check out branch tip"))
75 hint=_("use 'hg update' to check out branch tip"))
76
76
77 wlock = lock = None
77 wlock = lock = None
78 try:
78 try:
79 wlock = repo.wlock()
79 wlock = repo.wlock()
80 lock = repo.lock()
80 lock = repo.lock()
81
81
82 cmdutil.bailifchanged(repo)
82 cmdutil.bailifchanged(repo)
83
83
84 bheads = repo.branchheads(branch)
84 bheads = repo.branchheads(branch)
85 bheads = [head for head in bheads if len(repo[head].children()) == 0]
85 bheads = [head for head in bheads if len(repo[head].children()) == 0]
86 if len(bheads) > 1:
86 if len(bheads) > 1:
87 raise error.Abort(_('multiple heads in this branch '
87 raise error.Abort(_('multiple heads in this branch '
88 '(use "hg heads ." and "hg merge" to merge)'))
88 '(use "hg heads ." and "hg merge" to merge)'))
89
89
90 other = hg.peer(repo, opts, ui.expandpath(source))
90 other = hg.peer(repo, opts, ui.expandpath(source))
91 ui.status(_('pulling from %s\n') %
91 ui.status(_('pulling from %s\n') %
92 util.hidepassword(ui.expandpath(source)))
92 util.hidepassword(ui.expandpath(source)))
93 revs = None
93 revs = None
94 if opts['rev']:
94 if opts['rev']:
95 try:
95 try:
96 revs = [other.lookup(rev) for rev in opts['rev']]
96 revs = [other.lookup(rev) for rev in opts['rev']]
97 except error.CapabilityError:
97 except error.CapabilityError:
98 err = _("other repository doesn't support revision lookup, "
98 err = _("other repository doesn't support revision lookup, "
99 "so a rev cannot be specified.")
99 "so a rev cannot be specified.")
100 raise error.Abort(err)
100 raise error.Abort(err)
101
101
102 # Are there any changes at all?
102 # Are there any changes at all?
103 modheads = exchange.pull(repo, other, heads=revs).cgresult
103 modheads = exchange.pull(repo, other, heads=revs).cgresult
104 if modheads == 0:
104 if modheads == 0:
105 return 0
105 return 0
106
106
107 # Is this a simple fast-forward along the current branch?
107 # Is this a simple fast-forward along the current branch?
108 newheads = repo.branchheads(branch)
108 newheads = repo.branchheads(branch)
109 newchildren = repo.changelog.nodesbetween([parent], newheads)[2]
109 newchildren = repo.changelog.nodesbetween([parent], newheads)[2]
110 if len(newheads) == 1 and len(newchildren):
110 if len(newheads) == 1 and len(newchildren):
111 if newchildren[0] != parent:
111 if newchildren[0] != parent:
112 return hg.update(repo, newchildren[0])
112 return hg.update(repo, newchildren[0])
113 else:
113 else:
114 return 0
114 return 0
115
115
116 # Are there more than one additional branch heads?
116 # Are there more than one additional branch heads?
117 newchildren = [n for n in newchildren if n != parent]
117 newchildren = [n for n in newchildren if n != parent]
118 newparent = parent
118 newparent = parent
119 if newchildren:
119 if newchildren:
120 newparent = newchildren[0]
120 newparent = newchildren[0]
121 hg.clean(repo, newparent)
121 hg.clean(repo, newparent)
122 newheads = [n for n in newheads if n != newparent]
122 newheads = [n for n in newheads if n != newparent]
123 if len(newheads) > 1:
123 if len(newheads) > 1:
124 ui.status(_('not merging with %d other new branch heads '
124 ui.status(_('not merging with %d other new branch heads '
125 '(use "hg heads ." and "hg merge" to merge them)\n') %
125 '(use "hg heads ." and "hg merge" to merge them)\n') %
126 (len(newheads) - 1))
126 (len(newheads) - 1))
127 return 1
127 return 1
128
128
129 if not newheads:
129 if not newheads:
130 return 0
130 return 0
131
131
132 # Otherwise, let's merge.
132 # Otherwise, let's merge.
133 err = False
133 err = False
134 if newheads:
134 if newheads:
135 # By default, we consider the repository we're pulling
135 # By default, we consider the repository we're pulling
136 # *from* as authoritative, so we merge our changes into
136 # *from* as authoritative, so we merge our changes into
137 # theirs.
137 # theirs.
138 if opts['switch_parent']:
138 if opts['switch_parent']:
139 firstparent, secondparent = newparent, newheads[0]
139 firstparent, secondparent = newparent, newheads[0]
140 else:
140 else:
141 firstparent, secondparent = newheads[0], newparent
141 firstparent, secondparent = newheads[0], newparent
142 ui.status(_('updating to %d:%s\n') %
142 ui.status(_('updating to %d:%s\n') %
143 (repo.changelog.rev(firstparent),
143 (repo.changelog.rev(firstparent),
144 short(firstparent)))
144 short(firstparent)))
145 hg.clean(repo, firstparent)
145 hg.clean(repo, firstparent)
146 ui.status(_('merging with %d:%s\n') %
146 ui.status(_('merging with %d:%s\n') %
147 (repo.changelog.rev(secondparent), short(secondparent)))
147 (repo.changelog.rev(secondparent), short(secondparent)))
148 err = hg.merge(repo, secondparent, remind=False)
148 err = hg.merge(repo, secondparent, remind=False)
149
149
150 if not err:
150 if not err:
151 # we don't translate commit messages
151 # we don't translate commit messages
152 message = (cmdutil.logmessage(ui, opts) or
152 message = (cmdutil.logmessage(ui, opts) or
153 ('Automated merge with %s' %
153 ('Automated merge with %s' %
154 util.removeauth(other.url())))
154 util.removeauth(other.url())))
155 editopt = opts.get('edit') or opts.get('force_editor')
155 editopt = opts.get('edit') or opts.get('force_editor')
156 editor = cmdutil.getcommiteditor(edit=editopt, editform='fetch')
156 editor = cmdutil.getcommiteditor(edit=editopt, editform='fetch')
157 n = repo.commit(message, opts['user'], opts['date'], editor=editor)
157 n = repo.commit(message, opts['user'], opts['date'], editor=editor)
158 ui.status(_('new changeset %d:%s merges remote changes '
158 ui.status(_('new changeset %d:%s merges remote changes '
159 'with local\n') % (repo.changelog.rev(n),
159 'with local\n') % (repo.changelog.rev(n),
160 short(n)))
160 short(n)))
161
161
162 return err
162 return err
163
163
164 finally:
164 finally:
165 release(lock, wlock)
165 release(lock, wlock)
@@ -1,695 +1,695 b''
1 # __init__.py - fsmonitor initialization and overrides
1 # __init__.py - fsmonitor initialization and overrides
2 #
2 #
3 # Copyright 2013-2016 Facebook, Inc.
3 # Copyright 2013-2016 Facebook, Inc.
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 '''Faster status operations with the Watchman file monitor (EXPERIMENTAL)
8 '''Faster status operations with the Watchman file monitor (EXPERIMENTAL)
9
9
10 Integrates the file-watching program Watchman with Mercurial to produce faster
10 Integrates the file-watching program Watchman with Mercurial to produce faster
11 status results.
11 status results.
12
12
13 On a particular Linux system, for a real-world repository with over 400,000
13 On a particular Linux system, for a real-world repository with over 400,000
14 files hosted on ext4, vanilla `hg status` takes 1.3 seconds. On the same
14 files hosted on ext4, vanilla `hg status` takes 1.3 seconds. On the same
15 system, with fsmonitor it takes about 0.3 seconds.
15 system, with fsmonitor it takes about 0.3 seconds.
16
16
17 fsmonitor requires no configuration -- it will tell Watchman about your
17 fsmonitor requires no configuration -- it will tell Watchman about your
18 repository as necessary. You'll need to install Watchman from
18 repository as necessary. You'll need to install Watchman from
19 https://facebook.github.io/watchman/ and make sure it is in your PATH.
19 https://facebook.github.io/watchman/ and make sure it is in your PATH.
20
20
21 The following configuration options exist:
21 The following configuration options exist:
22
22
23 ::
23 ::
24
24
25 [fsmonitor]
25 [fsmonitor]
26 mode = {off, on, paranoid}
26 mode = {off, on, paranoid}
27
27
28 When `mode = off`, fsmonitor will disable itself (similar to not loading the
28 When `mode = off`, fsmonitor will disable itself (similar to not loading the
29 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
29 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
30 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
30 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
31 and ensure that the results are consistent.
31 and ensure that the results are consistent.
32
32
33 ::
33 ::
34
34
35 [fsmonitor]
35 [fsmonitor]
36 timeout = (float)
36 timeout = (float)
37
37
38 A value, in seconds, that determines how long fsmonitor will wait for Watchman
38 A value, in seconds, that determines how long fsmonitor will wait for Watchman
39 to return results. Defaults to `2.0`.
39 to return results. Defaults to `2.0`.
40
40
41 ::
41 ::
42
42
43 [fsmonitor]
43 [fsmonitor]
44 blacklistusers = (list of userids)
44 blacklistusers = (list of userids)
45
45
46 A list of usernames for which fsmonitor will disable itself altogether.
46 A list of usernames for which fsmonitor will disable itself altogether.
47
47
48 ::
48 ::
49
49
50 [fsmonitor]
50 [fsmonitor]
51 walk_on_invalidate = (boolean)
51 walk_on_invalidate = (boolean)
52
52
53 Whether or not to walk the whole repo ourselves when our cached state has been
53 Whether or not to walk the whole repo ourselves when our cached state has been
54 invalidated, for example when Watchman has been restarted or .hgignore rules
54 invalidated, for example when Watchman has been restarted or .hgignore rules
55 have been changed. Walking the repo in that case can result in competing for
55 have been changed. Walking the repo in that case can result in competing for
56 I/O with Watchman. For large repos it is recommended to set this value to
56 I/O with Watchman. For large repos it is recommended to set this value to
57 false. You may wish to set this to true if you have a very fast filesystem
57 false. You may wish to set this to true if you have a very fast filesystem
58 that can outpace the IPC overhead of getting the result data for the full repo
58 that can outpace the IPC overhead of getting the result data for the full repo
59 from Watchman. Defaults to false.
59 from Watchman. Defaults to false.
60
60
61 fsmonitor is incompatible with the largefiles and eol extensions, and
61 fsmonitor is incompatible with the largefiles and eol extensions, and
62 will disable itself if any of those are active.
62 will disable itself if any of those are active.
63
63
64 '''
64 '''
65
65
66 # Platforms Supported
66 # Platforms Supported
67 # ===================
67 # ===================
68 #
68 #
69 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
69 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
70 # even under severe loads.
70 # even under severe loads.
71 #
71 #
72 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
72 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
73 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
73 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
74 # user testing under normal loads.
74 # user testing under normal loads.
75 #
75 #
76 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
76 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
77 # very little testing has been done.
77 # very little testing has been done.
78 #
78 #
79 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
79 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
80 #
80 #
81 # Known Issues
81 # Known Issues
82 # ============
82 # ============
83 #
83 #
84 # * fsmonitor will disable itself if any of the following extensions are
84 # * fsmonitor will disable itself if any of the following extensions are
85 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
85 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
86 # * fsmonitor will produce incorrect results if nested repos that are not
86 # * fsmonitor will produce incorrect results if nested repos that are not
87 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
87 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
88 #
88 #
89 # The issues related to nested repos and subrepos are probably not fundamental
89 # The issues related to nested repos and subrepos are probably not fundamental
90 # ones. Patches to fix them are welcome.
90 # ones. Patches to fix them are welcome.
91
91
92 from __future__ import absolute_import
92 from __future__ import absolute_import
93
93
94 import hashlib
94 import hashlib
95 import os
95 import os
96 import stat
96 import stat
97 import sys
97 import sys
98
98
99 from mercurial.i18n import _
99 from mercurial.i18n import _
100 from mercurial import (
100 from mercurial import (
101 context,
101 context,
102 extensions,
102 extensions,
103 localrepo,
103 localrepo,
104 merge,
104 merge,
105 pathutil,
105 pathutil,
106 scmutil,
106 scmutil,
107 util,
107 util,
108 )
108 )
109 from mercurial import match as matchmod
109 from mercurial import match as matchmod
110
110
111 from . import (
111 from . import (
112 state,
112 state,
113 watchmanclient,
113 watchmanclient,
114 )
114 )
115
115
116 # Note for extension authors: ONLY specify testedwith = 'internal' for
116 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
117 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
117 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
118 # be specifying the version(s) of Mercurial they are tested with, or
118 # be specifying the version(s) of Mercurial they are tested with, or
119 # leave the attribute unspecified.
119 # leave the attribute unspecified.
120 testedwith = 'internal'
120 testedwith = 'ships-with-hg-core'
121
121
122 # This extension is incompatible with the following blacklisted extensions
122 # This extension is incompatible with the following blacklisted extensions
123 # and will disable itself when encountering one of these:
123 # and will disable itself when encountering one of these:
124 _blacklist = ['largefiles', 'eol']
124 _blacklist = ['largefiles', 'eol']
125
125
126 def _handleunavailable(ui, state, ex):
126 def _handleunavailable(ui, state, ex):
127 """Exception handler for Watchman interaction exceptions"""
127 """Exception handler for Watchman interaction exceptions"""
128 if isinstance(ex, watchmanclient.Unavailable):
128 if isinstance(ex, watchmanclient.Unavailable):
129 if ex.warn:
129 if ex.warn:
130 ui.warn(str(ex) + '\n')
130 ui.warn(str(ex) + '\n')
131 if ex.invalidate:
131 if ex.invalidate:
132 state.invalidate()
132 state.invalidate()
133 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
133 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
134 else:
134 else:
135 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
135 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
136
136
137 def _hashignore(ignore):
137 def _hashignore(ignore):
138 """Calculate hash for ignore patterns and filenames
138 """Calculate hash for ignore patterns and filenames
139
139
140 If this information changes between Mercurial invocations, we can't
140 If this information changes between Mercurial invocations, we can't
141 rely on Watchman information anymore and have to re-scan the working
141 rely on Watchman information anymore and have to re-scan the working
142 copy.
142 copy.
143
143
144 """
144 """
145 sha1 = hashlib.sha1()
145 sha1 = hashlib.sha1()
146 if util.safehasattr(ignore, 'includepat'):
146 if util.safehasattr(ignore, 'includepat'):
147 sha1.update(ignore.includepat)
147 sha1.update(ignore.includepat)
148 sha1.update('\0\0')
148 sha1.update('\0\0')
149 if util.safehasattr(ignore, 'excludepat'):
149 if util.safehasattr(ignore, 'excludepat'):
150 sha1.update(ignore.excludepat)
150 sha1.update(ignore.excludepat)
151 sha1.update('\0\0')
151 sha1.update('\0\0')
152 if util.safehasattr(ignore, 'patternspat'):
152 if util.safehasattr(ignore, 'patternspat'):
153 sha1.update(ignore.patternspat)
153 sha1.update(ignore.patternspat)
154 sha1.update('\0\0')
154 sha1.update('\0\0')
155 if util.safehasattr(ignore, '_files'):
155 if util.safehasattr(ignore, '_files'):
156 for f in ignore._files:
156 for f in ignore._files:
157 sha1.update(f)
157 sha1.update(f)
158 sha1.update('\0')
158 sha1.update('\0')
159 return sha1.hexdigest()
159 return sha1.hexdigest()
160
160
161 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
161 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
162 '''Replacement for dirstate.walk, hooking into Watchman.
162 '''Replacement for dirstate.walk, hooking into Watchman.
163
163
164 Whenever full is False, ignored is False, and the Watchman client is
164 Whenever full is False, ignored is False, and the Watchman client is
165 available, use Watchman combined with saved state to possibly return only a
165 available, use Watchman combined with saved state to possibly return only a
166 subset of files.'''
166 subset of files.'''
167 def bail():
167 def bail():
168 return orig(match, subrepos, unknown, ignored, full=True)
168 return orig(match, subrepos, unknown, ignored, full=True)
169
169
170 if full or ignored or not self._watchmanclient.available():
170 if full or ignored or not self._watchmanclient.available():
171 return bail()
171 return bail()
172 state = self._fsmonitorstate
172 state = self._fsmonitorstate
173 clock, ignorehash, notefiles = state.get()
173 clock, ignorehash, notefiles = state.get()
174 if not clock:
174 if not clock:
175 if state.walk_on_invalidate:
175 if state.walk_on_invalidate:
176 return bail()
176 return bail()
177 # Initial NULL clock value, see
177 # Initial NULL clock value, see
178 # https://facebook.github.io/watchman/docs/clockspec.html
178 # https://facebook.github.io/watchman/docs/clockspec.html
179 clock = 'c:0:0'
179 clock = 'c:0:0'
180 notefiles = []
180 notefiles = []
181
181
182 def fwarn(f, msg):
182 def fwarn(f, msg):
183 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
183 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
184 return False
184 return False
185
185
186 def badtype(mode):
186 def badtype(mode):
187 kind = _('unknown')
187 kind = _('unknown')
188 if stat.S_ISCHR(mode):
188 if stat.S_ISCHR(mode):
189 kind = _('character device')
189 kind = _('character device')
190 elif stat.S_ISBLK(mode):
190 elif stat.S_ISBLK(mode):
191 kind = _('block device')
191 kind = _('block device')
192 elif stat.S_ISFIFO(mode):
192 elif stat.S_ISFIFO(mode):
193 kind = _('fifo')
193 kind = _('fifo')
194 elif stat.S_ISSOCK(mode):
194 elif stat.S_ISSOCK(mode):
195 kind = _('socket')
195 kind = _('socket')
196 elif stat.S_ISDIR(mode):
196 elif stat.S_ISDIR(mode):
197 kind = _('directory')
197 kind = _('directory')
198 return _('unsupported file type (type is %s)') % kind
198 return _('unsupported file type (type is %s)') % kind
199
199
200 ignore = self._ignore
200 ignore = self._ignore
201 dirignore = self._dirignore
201 dirignore = self._dirignore
202 if unknown:
202 if unknown:
203 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
203 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
204 # ignore list changed -- can't rely on Watchman state any more
204 # ignore list changed -- can't rely on Watchman state any more
205 if state.walk_on_invalidate:
205 if state.walk_on_invalidate:
206 return bail()
206 return bail()
207 notefiles = []
207 notefiles = []
208 clock = 'c:0:0'
208 clock = 'c:0:0'
209 else:
209 else:
210 # always ignore
210 # always ignore
211 ignore = util.always
211 ignore = util.always
212 dirignore = util.always
212 dirignore = util.always
213
213
214 matchfn = match.matchfn
214 matchfn = match.matchfn
215 matchalways = match.always()
215 matchalways = match.always()
216 dmap = self._map
216 dmap = self._map
217 nonnormalset = getattr(self, '_nonnormalset', None)
217 nonnormalset = getattr(self, '_nonnormalset', None)
218
218
219 copymap = self._copymap
219 copymap = self._copymap
220 getkind = stat.S_IFMT
220 getkind = stat.S_IFMT
221 dirkind = stat.S_IFDIR
221 dirkind = stat.S_IFDIR
222 regkind = stat.S_IFREG
222 regkind = stat.S_IFREG
223 lnkkind = stat.S_IFLNK
223 lnkkind = stat.S_IFLNK
224 join = self._join
224 join = self._join
225 normcase = util.normcase
225 normcase = util.normcase
226 fresh_instance = False
226 fresh_instance = False
227
227
228 exact = skipstep3 = False
228 exact = skipstep3 = False
229 if matchfn == match.exact: # match.exact
229 if matchfn == match.exact: # match.exact
230 exact = True
230 exact = True
231 dirignore = util.always # skip step 2
231 dirignore = util.always # skip step 2
232 elif match.files() and not match.anypats(): # match.match, no patterns
232 elif match.files() and not match.anypats(): # match.match, no patterns
233 skipstep3 = True
233 skipstep3 = True
234
234
235 if not exact and self._checkcase:
235 if not exact and self._checkcase:
236 # note that even though we could receive directory entries, we're only
236 # note that even though we could receive directory entries, we're only
237 # interested in checking if a file with the same name exists. So only
237 # interested in checking if a file with the same name exists. So only
238 # normalize files if possible.
238 # normalize files if possible.
239 normalize = self._normalizefile
239 normalize = self._normalizefile
240 skipstep3 = False
240 skipstep3 = False
241 else:
241 else:
242 normalize = None
242 normalize = None
243
243
244 # step 1: find all explicit files
244 # step 1: find all explicit files
245 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
245 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
246
246
247 skipstep3 = skipstep3 and not (work or dirsnotfound)
247 skipstep3 = skipstep3 and not (work or dirsnotfound)
248 work = [d for d in work if not dirignore(d[0])]
248 work = [d for d in work if not dirignore(d[0])]
249
249
250 if not work and (exact or skipstep3):
250 if not work and (exact or skipstep3):
251 for s in subrepos:
251 for s in subrepos:
252 del results[s]
252 del results[s]
253 del results['.hg']
253 del results['.hg']
254 return results
254 return results
255
255
256 # step 2: query Watchman
256 # step 2: query Watchman
257 try:
257 try:
258 # Use the user-configured timeout for the query.
258 # Use the user-configured timeout for the query.
259 # Add a little slack over the top of the user query to allow for
259 # Add a little slack over the top of the user query to allow for
260 # overheads while transferring the data
260 # overheads while transferring the data
261 self._watchmanclient.settimeout(state.timeout + 0.1)
261 self._watchmanclient.settimeout(state.timeout + 0.1)
262 result = self._watchmanclient.command('query', {
262 result = self._watchmanclient.command('query', {
263 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
263 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
264 'since': clock,
264 'since': clock,
265 'expression': [
265 'expression': [
266 'not', [
266 'not', [
267 'anyof', ['dirname', '.hg'],
267 'anyof', ['dirname', '.hg'],
268 ['name', '.hg', 'wholename']
268 ['name', '.hg', 'wholename']
269 ]
269 ]
270 ],
270 ],
271 'sync_timeout': int(state.timeout * 1000),
271 'sync_timeout': int(state.timeout * 1000),
272 'empty_on_fresh_instance': state.walk_on_invalidate,
272 'empty_on_fresh_instance': state.walk_on_invalidate,
273 })
273 })
274 except Exception as ex:
274 except Exception as ex:
275 _handleunavailable(self._ui, state, ex)
275 _handleunavailable(self._ui, state, ex)
276 self._watchmanclient.clearconnection()
276 self._watchmanclient.clearconnection()
277 return bail()
277 return bail()
278 else:
278 else:
279 # We need to propagate the last observed clock up so that we
279 # We need to propagate the last observed clock up so that we
280 # can use it for our next query
280 # can use it for our next query
281 state.setlastclock(result['clock'])
281 state.setlastclock(result['clock'])
282 if result['is_fresh_instance']:
282 if result['is_fresh_instance']:
283 if state.walk_on_invalidate:
283 if state.walk_on_invalidate:
284 state.invalidate()
284 state.invalidate()
285 return bail()
285 return bail()
286 fresh_instance = True
286 fresh_instance = True
287 # Ignore any prior noteable files from the state info
287 # Ignore any prior noteable files from the state info
288 notefiles = []
288 notefiles = []
289
289
290 # for file paths which require normalization and we encounter a case
290 # for file paths which require normalization and we encounter a case
291 # collision, we store our own foldmap
291 # collision, we store our own foldmap
292 if normalize:
292 if normalize:
293 foldmap = dict((normcase(k), k) for k in results)
293 foldmap = dict((normcase(k), k) for k in results)
294
294
295 switch_slashes = os.sep == '\\'
295 switch_slashes = os.sep == '\\'
296 # The order of the results is, strictly speaking, undefined.
296 # The order of the results is, strictly speaking, undefined.
297 # For case changes on a case insensitive filesystem we may receive
297 # For case changes on a case insensitive filesystem we may receive
298 # two entries, one with exists=True and another with exists=False.
298 # two entries, one with exists=True and another with exists=False.
299 # The exists=True entries in the same response should be interpreted
299 # The exists=True entries in the same response should be interpreted
300 # as being happens-after the exists=False entries due to the way that
300 # as being happens-after the exists=False entries due to the way that
301 # Watchman tracks files. We use this property to reconcile deletes
301 # Watchman tracks files. We use this property to reconcile deletes
302 # for name case changes.
302 # for name case changes.
303 for entry in result['files']:
303 for entry in result['files']:
304 fname = entry['name']
304 fname = entry['name']
305 if switch_slashes:
305 if switch_slashes:
306 fname = fname.replace('\\', '/')
306 fname = fname.replace('\\', '/')
307 if normalize:
307 if normalize:
308 normed = normcase(fname)
308 normed = normcase(fname)
309 fname = normalize(fname, True, True)
309 fname = normalize(fname, True, True)
310 foldmap[normed] = fname
310 foldmap[normed] = fname
311 fmode = entry['mode']
311 fmode = entry['mode']
312 fexists = entry['exists']
312 fexists = entry['exists']
313 kind = getkind(fmode)
313 kind = getkind(fmode)
314
314
315 if not fexists:
315 if not fexists:
316 # if marked as deleted and we don't already have a change
316 # if marked as deleted and we don't already have a change
317 # record, mark it as deleted. If we already have an entry
317 # record, mark it as deleted. If we already have an entry
318 # for fname then it was either part of walkexplicit or was
318 # for fname then it was either part of walkexplicit or was
319 # an earlier result that was a case change
319 # an earlier result that was a case change
320 if fname not in results and fname in dmap and (
320 if fname not in results and fname in dmap and (
321 matchalways or matchfn(fname)):
321 matchalways or matchfn(fname)):
322 results[fname] = None
322 results[fname] = None
323 elif kind == dirkind:
323 elif kind == dirkind:
324 if fname in dmap and (matchalways or matchfn(fname)):
324 if fname in dmap and (matchalways or matchfn(fname)):
325 results[fname] = None
325 results[fname] = None
326 elif kind == regkind or kind == lnkkind:
326 elif kind == regkind or kind == lnkkind:
327 if fname in dmap:
327 if fname in dmap:
328 if matchalways or matchfn(fname):
328 if matchalways or matchfn(fname):
329 results[fname] = entry
329 results[fname] = entry
330 elif (matchalways or matchfn(fname)) and not ignore(fname):
330 elif (matchalways or matchfn(fname)) and not ignore(fname):
331 results[fname] = entry
331 results[fname] = entry
332 elif fname in dmap and (matchalways or matchfn(fname)):
332 elif fname in dmap and (matchalways or matchfn(fname)):
333 results[fname] = None
333 results[fname] = None
334
334
335 # step 3: query notable files we don't already know about
335 # step 3: query notable files we don't already know about
336 # XXX try not to iterate over the entire dmap
336 # XXX try not to iterate over the entire dmap
337 if normalize:
337 if normalize:
338 # any notable files that have changed case will already be handled
338 # any notable files that have changed case will already be handled
339 # above, so just check membership in the foldmap
339 # above, so just check membership in the foldmap
340 notefiles = set((normalize(f, True, True) for f in notefiles
340 notefiles = set((normalize(f, True, True) for f in notefiles
341 if normcase(f) not in foldmap))
341 if normcase(f) not in foldmap))
342 visit = set((f for f in notefiles if (f not in results and matchfn(f)
342 visit = set((f for f in notefiles if (f not in results and matchfn(f)
343 and (f in dmap or not ignore(f)))))
343 and (f in dmap or not ignore(f)))))
344
344
345 if nonnormalset is not None and not fresh_instance:
345 if nonnormalset is not None and not fresh_instance:
346 if matchalways:
346 if matchalways:
347 visit.update(f for f in nonnormalset if f not in results)
347 visit.update(f for f in nonnormalset if f not in results)
348 visit.update(f for f in copymap if f not in results)
348 visit.update(f for f in copymap if f not in results)
349 else:
349 else:
350 visit.update(f for f in nonnormalset
350 visit.update(f for f in nonnormalset
351 if f not in results and matchfn(f))
351 if f not in results and matchfn(f))
352 visit.update(f for f in copymap
352 visit.update(f for f in copymap
353 if f not in results and matchfn(f))
353 if f not in results and matchfn(f))
354 else:
354 else:
355 if matchalways:
355 if matchalways:
356 visit.update(f for f, st in dmap.iteritems()
356 visit.update(f for f, st in dmap.iteritems()
357 if (f not in results and
357 if (f not in results and
358 (st[2] < 0 or st[0] != 'n' or fresh_instance)))
358 (st[2] < 0 or st[0] != 'n' or fresh_instance)))
359 visit.update(f for f in copymap if f not in results)
359 visit.update(f for f in copymap if f not in results)
360 else:
360 else:
361 visit.update(f for f, st in dmap.iteritems()
361 visit.update(f for f, st in dmap.iteritems()
362 if (f not in results and
362 if (f not in results and
363 (st[2] < 0 or st[0] != 'n' or fresh_instance)
363 (st[2] < 0 or st[0] != 'n' or fresh_instance)
364 and matchfn(f)))
364 and matchfn(f)))
365 visit.update(f for f in copymap
365 visit.update(f for f in copymap
366 if f not in results and matchfn(f))
366 if f not in results and matchfn(f))
367
367
368 audit = pathutil.pathauditor(self._root).check
368 audit = pathutil.pathauditor(self._root).check
369 auditpass = [f for f in visit if audit(f)]
369 auditpass = [f for f in visit if audit(f)]
370 auditpass.sort()
370 auditpass.sort()
371 auditfail = visit.difference(auditpass)
371 auditfail = visit.difference(auditpass)
372 for f in auditfail:
372 for f in auditfail:
373 results[f] = None
373 results[f] = None
374
374
375 nf = iter(auditpass).next
375 nf = iter(auditpass).next
376 for st in util.statfiles([join(f) for f in auditpass]):
376 for st in util.statfiles([join(f) for f in auditpass]):
377 f = nf()
377 f = nf()
378 if st or f in dmap:
378 if st or f in dmap:
379 results[f] = st
379 results[f] = st
380
380
381 for s in subrepos:
381 for s in subrepos:
382 del results[s]
382 del results[s]
383 del results['.hg']
383 del results['.hg']
384 return results
384 return results
385
385
386 def overridestatus(
386 def overridestatus(
387 orig, self, node1='.', node2=None, match=None, ignored=False,
387 orig, self, node1='.', node2=None, match=None, ignored=False,
388 clean=False, unknown=False, listsubrepos=False):
388 clean=False, unknown=False, listsubrepos=False):
389 listignored = ignored
389 listignored = ignored
390 listclean = clean
390 listclean = clean
391 listunknown = unknown
391 listunknown = unknown
392
392
393 def _cmpsets(l1, l2):
393 def _cmpsets(l1, l2):
394 try:
394 try:
395 if 'FSMONITOR_LOG_FILE' in os.environ:
395 if 'FSMONITOR_LOG_FILE' in os.environ:
396 fn = os.environ['FSMONITOR_LOG_FILE']
396 fn = os.environ['FSMONITOR_LOG_FILE']
397 f = open(fn, 'wb')
397 f = open(fn, 'wb')
398 else:
398 else:
399 fn = 'fsmonitorfail.log'
399 fn = 'fsmonitorfail.log'
400 f = self.opener(fn, 'wb')
400 f = self.opener(fn, 'wb')
401 except (IOError, OSError):
401 except (IOError, OSError):
402 self.ui.warn(_('warning: unable to write to %s\n') % fn)
402 self.ui.warn(_('warning: unable to write to %s\n') % fn)
403 return
403 return
404
404
405 try:
405 try:
406 for i, (s1, s2) in enumerate(zip(l1, l2)):
406 for i, (s1, s2) in enumerate(zip(l1, l2)):
407 if set(s1) != set(s2):
407 if set(s1) != set(s2):
408 f.write('sets at position %d are unequal\n' % i)
408 f.write('sets at position %d are unequal\n' % i)
409 f.write('watchman returned: %s\n' % s1)
409 f.write('watchman returned: %s\n' % s1)
410 f.write('stat returned: %s\n' % s2)
410 f.write('stat returned: %s\n' % s2)
411 finally:
411 finally:
412 f.close()
412 f.close()
413
413
414 if isinstance(node1, context.changectx):
414 if isinstance(node1, context.changectx):
415 ctx1 = node1
415 ctx1 = node1
416 else:
416 else:
417 ctx1 = self[node1]
417 ctx1 = self[node1]
418 if isinstance(node2, context.changectx):
418 if isinstance(node2, context.changectx):
419 ctx2 = node2
419 ctx2 = node2
420 else:
420 else:
421 ctx2 = self[node2]
421 ctx2 = self[node2]
422
422
423 working = ctx2.rev() is None
423 working = ctx2.rev() is None
424 parentworking = working and ctx1 == self['.']
424 parentworking = working and ctx1 == self['.']
425 match = match or matchmod.always(self.root, self.getcwd())
425 match = match or matchmod.always(self.root, self.getcwd())
426
426
427 # Maybe we can use this opportunity to update Watchman's state.
427 # Maybe we can use this opportunity to update Watchman's state.
428 # Mercurial uses workingcommitctx and/or memctx to represent the part of
428 # Mercurial uses workingcommitctx and/or memctx to represent the part of
429 # the workingctx that is to be committed. So don't update the state in
429 # the workingctx that is to be committed. So don't update the state in
430 # that case.
430 # that case.
431 # HG_PENDING is set in the environment when the dirstate is being updated
431 # HG_PENDING is set in the environment when the dirstate is being updated
432 # in the middle of a transaction; we must not update our state in that
432 # in the middle of a transaction; we must not update our state in that
433 # case, or we risk forgetting about changes in the working copy.
433 # case, or we risk forgetting about changes in the working copy.
434 updatestate = (parentworking and match.always() and
434 updatestate = (parentworking and match.always() and
435 not isinstance(ctx2, (context.workingcommitctx,
435 not isinstance(ctx2, (context.workingcommitctx,
436 context.memctx)) and
436 context.memctx)) and
437 'HG_PENDING' not in os.environ)
437 'HG_PENDING' not in os.environ)
438
438
439 try:
439 try:
440 if self._fsmonitorstate.walk_on_invalidate:
440 if self._fsmonitorstate.walk_on_invalidate:
441 # Use a short timeout to query the current clock. If that
441 # Use a short timeout to query the current clock. If that
442 # takes too long then we assume that the service will be slow
442 # takes too long then we assume that the service will be slow
443 # to answer our query.
443 # to answer our query.
444 # walk_on_invalidate indicates that we prefer to walk the
444 # walk_on_invalidate indicates that we prefer to walk the
445 # tree ourselves because we can ignore portions that Watchman
445 # tree ourselves because we can ignore portions that Watchman
446 # cannot and we tend to be faster in the warmer buffer cache
446 # cannot and we tend to be faster in the warmer buffer cache
447 # cases.
447 # cases.
448 self._watchmanclient.settimeout(0.1)
448 self._watchmanclient.settimeout(0.1)
449 else:
449 else:
450 # Give Watchman more time to potentially complete its walk
450 # Give Watchman more time to potentially complete its walk
451 # and return the initial clock. In this mode we assume that
451 # and return the initial clock. In this mode we assume that
452 # the filesystem will be slower than parsing a potentially
452 # the filesystem will be slower than parsing a potentially
453 # very large Watchman result set.
453 # very large Watchman result set.
454 self._watchmanclient.settimeout(
454 self._watchmanclient.settimeout(
455 self._fsmonitorstate.timeout + 0.1)
455 self._fsmonitorstate.timeout + 0.1)
456 startclock = self._watchmanclient.getcurrentclock()
456 startclock = self._watchmanclient.getcurrentclock()
457 except Exception as ex:
457 except Exception as ex:
458 self._watchmanclient.clearconnection()
458 self._watchmanclient.clearconnection()
459 _handleunavailable(self.ui, self._fsmonitorstate, ex)
459 _handleunavailable(self.ui, self._fsmonitorstate, ex)
460 # boo, Watchman failed. bail
460 # boo, Watchman failed. bail
461 return orig(node1, node2, match, listignored, listclean,
461 return orig(node1, node2, match, listignored, listclean,
462 listunknown, listsubrepos)
462 listunknown, listsubrepos)
463
463
464 if updatestate:
464 if updatestate:
465 # We need info about unknown files. This may make things slower the
465 # We need info about unknown files. This may make things slower the
466 # first time, but whatever.
466 # first time, but whatever.
467 stateunknown = True
467 stateunknown = True
468 else:
468 else:
469 stateunknown = listunknown
469 stateunknown = listunknown
470
470
471 r = orig(node1, node2, match, listignored, listclean, stateunknown,
471 r = orig(node1, node2, match, listignored, listclean, stateunknown,
472 listsubrepos)
472 listsubrepos)
473 modified, added, removed, deleted, unknown, ignored, clean = r
473 modified, added, removed, deleted, unknown, ignored, clean = r
474
474
475 if updatestate:
475 if updatestate:
476 notefiles = modified + added + removed + deleted + unknown
476 notefiles = modified + added + removed + deleted + unknown
477 self._fsmonitorstate.set(
477 self._fsmonitorstate.set(
478 self._fsmonitorstate.getlastclock() or startclock,
478 self._fsmonitorstate.getlastclock() or startclock,
479 _hashignore(self.dirstate._ignore),
479 _hashignore(self.dirstate._ignore),
480 notefiles)
480 notefiles)
481
481
482 if not listunknown:
482 if not listunknown:
483 unknown = []
483 unknown = []
484
484
485 # don't do paranoid checks if we're not going to query Watchman anyway
485 # don't do paranoid checks if we're not going to query Watchman anyway
486 full = listclean or match.traversedir is not None
486 full = listclean or match.traversedir is not None
487 if self._fsmonitorstate.mode == 'paranoid' and not full:
487 if self._fsmonitorstate.mode == 'paranoid' and not full:
488 # run status again and fall back to the old walk this time
488 # run status again and fall back to the old walk this time
489 self.dirstate._fsmonitordisable = True
489 self.dirstate._fsmonitordisable = True
490
490
491 # shut the UI up
491 # shut the UI up
492 quiet = self.ui.quiet
492 quiet = self.ui.quiet
493 self.ui.quiet = True
493 self.ui.quiet = True
494 fout, ferr = self.ui.fout, self.ui.ferr
494 fout, ferr = self.ui.fout, self.ui.ferr
495 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
495 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
496
496
497 try:
497 try:
498 rv2 = orig(
498 rv2 = orig(
499 node1, node2, match, listignored, listclean, listunknown,
499 node1, node2, match, listignored, listclean, listunknown,
500 listsubrepos)
500 listsubrepos)
501 finally:
501 finally:
502 self.dirstate._fsmonitordisable = False
502 self.dirstate._fsmonitordisable = False
503 self.ui.quiet = quiet
503 self.ui.quiet = quiet
504 self.ui.fout, self.ui.ferr = fout, ferr
504 self.ui.fout, self.ui.ferr = fout, ferr
505
505
506 # clean isn't tested since it's set to True above
506 # clean isn't tested since it's set to True above
507 _cmpsets([modified, added, removed, deleted, unknown, ignored, clean],
507 _cmpsets([modified, added, removed, deleted, unknown, ignored, clean],
508 rv2)
508 rv2)
509 modified, added, removed, deleted, unknown, ignored, clean = rv2
509 modified, added, removed, deleted, unknown, ignored, clean = rv2
510
510
511 return scmutil.status(
511 return scmutil.status(
512 modified, added, removed, deleted, unknown, ignored, clean)
512 modified, added, removed, deleted, unknown, ignored, clean)
513
513
514 def makedirstate(cls):
514 def makedirstate(cls):
515 class fsmonitordirstate(cls):
515 class fsmonitordirstate(cls):
516 def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
516 def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
517 # _fsmonitordisable is used in paranoid mode
517 # _fsmonitordisable is used in paranoid mode
518 self._fsmonitordisable = False
518 self._fsmonitordisable = False
519 self._fsmonitorstate = fsmonitorstate
519 self._fsmonitorstate = fsmonitorstate
520 self._watchmanclient = watchmanclient
520 self._watchmanclient = watchmanclient
521
521
522 def walk(self, *args, **kwargs):
522 def walk(self, *args, **kwargs):
523 orig = super(fsmonitordirstate, self).walk
523 orig = super(fsmonitordirstate, self).walk
524 if self._fsmonitordisable:
524 if self._fsmonitordisable:
525 return orig(*args, **kwargs)
525 return orig(*args, **kwargs)
526 return overridewalk(orig, self, *args, **kwargs)
526 return overridewalk(orig, self, *args, **kwargs)
527
527
528 def rebuild(self, *args, **kwargs):
528 def rebuild(self, *args, **kwargs):
529 self._fsmonitorstate.invalidate()
529 self._fsmonitorstate.invalidate()
530 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
530 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
531
531
532 def invalidate(self, *args, **kwargs):
532 def invalidate(self, *args, **kwargs):
533 self._fsmonitorstate.invalidate()
533 self._fsmonitorstate.invalidate()
534 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
534 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
535
535
536 return fsmonitordirstate
536 return fsmonitordirstate
537
537
538 def wrapdirstate(orig, self):
538 def wrapdirstate(orig, self):
539 ds = orig(self)
539 ds = orig(self)
540 # only override the dirstate when Watchman is available for the repo
540 # only override the dirstate when Watchman is available for the repo
541 if util.safehasattr(self, '_fsmonitorstate'):
541 if util.safehasattr(self, '_fsmonitorstate'):
542 ds.__class__ = makedirstate(ds.__class__)
542 ds.__class__ = makedirstate(ds.__class__)
543 ds._fsmonitorinit(self._fsmonitorstate, self._watchmanclient)
543 ds._fsmonitorinit(self._fsmonitorstate, self._watchmanclient)
544 return ds
544 return ds
545
545
546 def extsetup(ui):
546 def extsetup(ui):
547 wrapfilecache(localrepo.localrepository, 'dirstate', wrapdirstate)
547 wrapfilecache(localrepo.localrepository, 'dirstate', wrapdirstate)
548 if sys.platform == 'darwin':
548 if sys.platform == 'darwin':
549 # An assist for avoiding the dangling-symlink fsevents bug
549 # An assist for avoiding the dangling-symlink fsevents bug
550 extensions.wrapfunction(os, 'symlink', wrapsymlink)
550 extensions.wrapfunction(os, 'symlink', wrapsymlink)
551
551
552 extensions.wrapfunction(merge, 'update', wrapupdate)
552 extensions.wrapfunction(merge, 'update', wrapupdate)
553
553
554 def wrapsymlink(orig, source, link_name):
554 def wrapsymlink(orig, source, link_name):
555 ''' if we create a dangling symlink, also touch the parent dir
555 ''' if we create a dangling symlink, also touch the parent dir
556 to encourage fsevents notifications to work more correctly '''
556 to encourage fsevents notifications to work more correctly '''
557 try:
557 try:
558 return orig(source, link_name)
558 return orig(source, link_name)
559 finally:
559 finally:
560 try:
560 try:
561 os.utime(os.path.dirname(link_name), None)
561 os.utime(os.path.dirname(link_name), None)
562 except OSError:
562 except OSError:
563 pass
563 pass
564
564
565 class state_update(object):
565 class state_update(object):
566 ''' This context mananger is responsible for dispatching the state-enter
566 ''' This context mananger is responsible for dispatching the state-enter
567 and state-leave signals to the watchman service '''
567 and state-leave signals to the watchman service '''
568
568
569 def __init__(self, repo, node, distance, partial):
569 def __init__(self, repo, node, distance, partial):
570 self.repo = repo
570 self.repo = repo
571 self.node = node
571 self.node = node
572 self.distance = distance
572 self.distance = distance
573 self.partial = partial
573 self.partial = partial
574
574
575 def __enter__(self):
575 def __enter__(self):
576 self._state('state-enter')
576 self._state('state-enter')
577 return self
577 return self
578
578
579 def __exit__(self, type_, value, tb):
579 def __exit__(self, type_, value, tb):
580 status = 'ok' if type_ is None else 'failed'
580 status = 'ok' if type_ is None else 'failed'
581 self._state('state-leave', status=status)
581 self._state('state-leave', status=status)
582
582
583 def _state(self, cmd, status='ok'):
583 def _state(self, cmd, status='ok'):
584 if not util.safehasattr(self.repo, '_watchmanclient'):
584 if not util.safehasattr(self.repo, '_watchmanclient'):
585 return
585 return
586 try:
586 try:
587 commithash = self.repo[self.node].hex()
587 commithash = self.repo[self.node].hex()
588 self.repo._watchmanclient.command(cmd, {
588 self.repo._watchmanclient.command(cmd, {
589 'name': 'hg.update',
589 'name': 'hg.update',
590 'metadata': {
590 'metadata': {
591 # the target revision
591 # the target revision
592 'rev': commithash,
592 'rev': commithash,
593 # approximate number of commits between current and target
593 # approximate number of commits between current and target
594 'distance': self.distance,
594 'distance': self.distance,
595 # success/failure (only really meaningful for state-leave)
595 # success/failure (only really meaningful for state-leave)
596 'status': status,
596 'status': status,
597 # whether the working copy parent is changing
597 # whether the working copy parent is changing
598 'partial': self.partial,
598 'partial': self.partial,
599 }})
599 }})
600 except Exception as e:
600 except Exception as e:
601 # Swallow any errors; fire and forget
601 # Swallow any errors; fire and forget
602 self.repo.ui.log(
602 self.repo.ui.log(
603 'watchman', 'Exception %s while running %s\n', e, cmd)
603 'watchman', 'Exception %s while running %s\n', e, cmd)
604
604
605 # Bracket working copy updates with calls to the watchman state-enter
605 # Bracket working copy updates with calls to the watchman state-enter
606 # and state-leave commands. This allows clients to perform more intelligent
606 # and state-leave commands. This allows clients to perform more intelligent
607 # settling during bulk file change scenarios
607 # settling during bulk file change scenarios
608 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
608 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
609 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
609 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
610 mergeancestor=False, labels=None, matcher=None, **kwargs):
610 mergeancestor=False, labels=None, matcher=None, **kwargs):
611
611
612 distance = 0
612 distance = 0
613 partial = True
613 partial = True
614 if matcher is None or matcher.always():
614 if matcher is None or matcher.always():
615 partial = False
615 partial = False
616 wc = repo[None]
616 wc = repo[None]
617 parents = wc.parents()
617 parents = wc.parents()
618 if len(parents) == 2:
618 if len(parents) == 2:
619 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
619 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
620 ancrev = repo[anc].rev()
620 ancrev = repo[anc].rev()
621 distance = abs(repo[node].rev() - ancrev)
621 distance = abs(repo[node].rev() - ancrev)
622 elif len(parents) == 1:
622 elif len(parents) == 1:
623 distance = abs(repo[node].rev() - parents[0].rev())
623 distance = abs(repo[node].rev() - parents[0].rev())
624
624
625 with state_update(repo, node, distance, partial):
625 with state_update(repo, node, distance, partial):
626 return orig(
626 return orig(
627 repo, node, branchmerge, force, ancestor, mergeancestor,
627 repo, node, branchmerge, force, ancestor, mergeancestor,
628 labels, matcher, *kwargs)
628 labels, matcher, *kwargs)
629
629
630 def reposetup(ui, repo):
630 def reposetup(ui, repo):
631 # We don't work with largefiles or inotify
631 # We don't work with largefiles or inotify
632 exts = extensions.enabled()
632 exts = extensions.enabled()
633 for ext in _blacklist:
633 for ext in _blacklist:
634 if ext in exts:
634 if ext in exts:
635 ui.warn(_('The fsmonitor extension is incompatible with the %s '
635 ui.warn(_('The fsmonitor extension is incompatible with the %s '
636 'extension and has been disabled.\n') % ext)
636 'extension and has been disabled.\n') % ext)
637 return
637 return
638
638
639 if util.safehasattr(repo, 'dirstate'):
639 if util.safehasattr(repo, 'dirstate'):
640 # We don't work with subrepos either. Note that we can get passed in
640 # We don't work with subrepos either. Note that we can get passed in
641 # e.g. a statichttprepo, which throws on trying to access the substate.
641 # e.g. a statichttprepo, which throws on trying to access the substate.
642 # XXX This sucks.
642 # XXX This sucks.
643 try:
643 try:
644 # if repo[None].substate can cause a dirstate parse, which is too
644 # if repo[None].substate can cause a dirstate parse, which is too
645 # slow. Instead, look for a file called hgsubstate,
645 # slow. Instead, look for a file called hgsubstate,
646 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
646 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
647 return
647 return
648 except AttributeError:
648 except AttributeError:
649 return
649 return
650
650
651 fsmonitorstate = state.state(repo)
651 fsmonitorstate = state.state(repo)
652 if fsmonitorstate.mode == 'off':
652 if fsmonitorstate.mode == 'off':
653 return
653 return
654
654
655 try:
655 try:
656 client = watchmanclient.client(repo)
656 client = watchmanclient.client(repo)
657 except Exception as ex:
657 except Exception as ex:
658 _handleunavailable(ui, fsmonitorstate, ex)
658 _handleunavailable(ui, fsmonitorstate, ex)
659 return
659 return
660
660
661 repo._fsmonitorstate = fsmonitorstate
661 repo._fsmonitorstate = fsmonitorstate
662 repo._watchmanclient = client
662 repo._watchmanclient = client
663
663
664 # at this point since fsmonitorstate wasn't present, repo.dirstate is
664 # at this point since fsmonitorstate wasn't present, repo.dirstate is
665 # not a fsmonitordirstate
665 # not a fsmonitordirstate
666 repo.dirstate.__class__ = makedirstate(repo.dirstate.__class__)
666 repo.dirstate.__class__ = makedirstate(repo.dirstate.__class__)
667 # nuke the dirstate so that _fsmonitorinit and subsequent configuration
667 # nuke the dirstate so that _fsmonitorinit and subsequent configuration
668 # changes take effect on it
668 # changes take effect on it
669 del repo._filecache['dirstate']
669 del repo._filecache['dirstate']
670 delattr(repo.unfiltered(), 'dirstate')
670 delattr(repo.unfiltered(), 'dirstate')
671
671
672 class fsmonitorrepo(repo.__class__):
672 class fsmonitorrepo(repo.__class__):
673 def status(self, *args, **kwargs):
673 def status(self, *args, **kwargs):
674 orig = super(fsmonitorrepo, self).status
674 orig = super(fsmonitorrepo, self).status
675 return overridestatus(orig, self, *args, **kwargs)
675 return overridestatus(orig, self, *args, **kwargs)
676
676
677 repo.__class__ = fsmonitorrepo
677 repo.__class__ = fsmonitorrepo
678
678
679 def wrapfilecache(cls, propname, wrapper):
679 def wrapfilecache(cls, propname, wrapper):
680 """Wraps a filecache property. These can't be wrapped using the normal
680 """Wraps a filecache property. These can't be wrapped using the normal
681 wrapfunction. This should eventually go into upstream Mercurial.
681 wrapfunction. This should eventually go into upstream Mercurial.
682 """
682 """
683 assert callable(wrapper)
683 assert callable(wrapper)
684 for currcls in cls.__mro__:
684 for currcls in cls.__mro__:
685 if propname in currcls.__dict__:
685 if propname in currcls.__dict__:
686 origfn = currcls.__dict__[propname].func
686 origfn = currcls.__dict__[propname].func
687 assert callable(origfn)
687 assert callable(origfn)
688 def wrap(*args, **kwargs):
688 def wrap(*args, **kwargs):
689 return wrapper(origfn, *args, **kwargs)
689 return wrapper(origfn, *args, **kwargs)
690 currcls.__dict__[propname].func = wrap
690 currcls.__dict__[propname].func = wrap
691 break
691 break
692
692
693 if currcls is object:
693 if currcls is object:
694 raise AttributeError(
694 raise AttributeError(
695 _("type '%s' has no property '%s'") % (cls, propname))
695 _("type '%s' has no property '%s'") % (cls, propname))
@@ -1,318 +1,318 b''
1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 '''commands to sign and verify changesets'''
6 '''commands to sign and verify changesets'''
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import binascii
10 import binascii
11 import os
11 import os
12 import tempfile
12 import tempfile
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import (
16 cmdutil,
16 cmdutil,
17 commands,
17 commands,
18 error,
18 error,
19 match,
19 match,
20 node as hgnode,
20 node as hgnode,
21 util,
21 util,
22 )
22 )
23
23
24 cmdtable = {}
24 cmdtable = {}
25 command = cmdutil.command(cmdtable)
25 command = cmdutil.command(cmdtable)
26 # Note for extension authors: ONLY specify testedwith = 'internal' for
26 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
28 # be specifying the version(s) of Mercurial they are tested with, or
28 # be specifying the version(s) of Mercurial they are tested with, or
29 # leave the attribute unspecified.
29 # leave the attribute unspecified.
30 testedwith = 'internal'
30 testedwith = 'ships-with-hg-core'
31
31
32 class gpg(object):
32 class gpg(object):
33 def __init__(self, path, key=None):
33 def __init__(self, path, key=None):
34 self.path = path
34 self.path = path
35 self.key = (key and " --local-user \"%s\"" % key) or ""
35 self.key = (key and " --local-user \"%s\"" % key) or ""
36
36
37 def sign(self, data):
37 def sign(self, data):
38 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
38 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
39 return util.filter(data, gpgcmd)
39 return util.filter(data, gpgcmd)
40
40
41 def verify(self, data, sig):
41 def verify(self, data, sig):
42 """ returns of the good and bad signatures"""
42 """ returns of the good and bad signatures"""
43 sigfile = datafile = None
43 sigfile = datafile = None
44 try:
44 try:
45 # create temporary files
45 # create temporary files
46 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
46 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
47 fp = os.fdopen(fd, 'wb')
47 fp = os.fdopen(fd, 'wb')
48 fp.write(sig)
48 fp.write(sig)
49 fp.close()
49 fp.close()
50 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
50 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
51 fp = os.fdopen(fd, 'wb')
51 fp = os.fdopen(fd, 'wb')
52 fp.write(data)
52 fp.write(data)
53 fp.close()
53 fp.close()
54 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
54 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
55 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
55 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
56 ret = util.filter("", gpgcmd)
56 ret = util.filter("", gpgcmd)
57 finally:
57 finally:
58 for f in (sigfile, datafile):
58 for f in (sigfile, datafile):
59 try:
59 try:
60 if f:
60 if f:
61 os.unlink(f)
61 os.unlink(f)
62 except OSError:
62 except OSError:
63 pass
63 pass
64 keys = []
64 keys = []
65 key, fingerprint = None, None
65 key, fingerprint = None, None
66 for l in ret.splitlines():
66 for l in ret.splitlines():
67 # see DETAILS in the gnupg documentation
67 # see DETAILS in the gnupg documentation
68 # filter the logger output
68 # filter the logger output
69 if not l.startswith("[GNUPG:]"):
69 if not l.startswith("[GNUPG:]"):
70 continue
70 continue
71 l = l[9:]
71 l = l[9:]
72 if l.startswith("VALIDSIG"):
72 if l.startswith("VALIDSIG"):
73 # fingerprint of the primary key
73 # fingerprint of the primary key
74 fingerprint = l.split()[10]
74 fingerprint = l.split()[10]
75 elif l.startswith("ERRSIG"):
75 elif l.startswith("ERRSIG"):
76 key = l.split(" ", 3)[:2]
76 key = l.split(" ", 3)[:2]
77 key.append("")
77 key.append("")
78 fingerprint = None
78 fingerprint = None
79 elif (l.startswith("GOODSIG") or
79 elif (l.startswith("GOODSIG") or
80 l.startswith("EXPSIG") or
80 l.startswith("EXPSIG") or
81 l.startswith("EXPKEYSIG") or
81 l.startswith("EXPKEYSIG") or
82 l.startswith("BADSIG")):
82 l.startswith("BADSIG")):
83 if key is not None:
83 if key is not None:
84 keys.append(key + [fingerprint])
84 keys.append(key + [fingerprint])
85 key = l.split(" ", 2)
85 key = l.split(" ", 2)
86 fingerprint = None
86 fingerprint = None
87 if key is not None:
87 if key is not None:
88 keys.append(key + [fingerprint])
88 keys.append(key + [fingerprint])
89 return keys
89 return keys
90
90
91 def newgpg(ui, **opts):
91 def newgpg(ui, **opts):
92 """create a new gpg instance"""
92 """create a new gpg instance"""
93 gpgpath = ui.config("gpg", "cmd", "gpg")
93 gpgpath = ui.config("gpg", "cmd", "gpg")
94 gpgkey = opts.get('key')
94 gpgkey = opts.get('key')
95 if not gpgkey:
95 if not gpgkey:
96 gpgkey = ui.config("gpg", "key", None)
96 gpgkey = ui.config("gpg", "key", None)
97 return gpg(gpgpath, gpgkey)
97 return gpg(gpgpath, gpgkey)
98
98
99 def sigwalk(repo):
99 def sigwalk(repo):
100 """
100 """
101 walk over every sigs, yields a couple
101 walk over every sigs, yields a couple
102 ((node, version, sig), (filename, linenumber))
102 ((node, version, sig), (filename, linenumber))
103 """
103 """
104 def parsefile(fileiter, context):
104 def parsefile(fileiter, context):
105 ln = 1
105 ln = 1
106 for l in fileiter:
106 for l in fileiter:
107 if not l:
107 if not l:
108 continue
108 continue
109 yield (l.split(" ", 2), (context, ln))
109 yield (l.split(" ", 2), (context, ln))
110 ln += 1
110 ln += 1
111
111
112 # read the heads
112 # read the heads
113 fl = repo.file(".hgsigs")
113 fl = repo.file(".hgsigs")
114 for r in reversed(fl.heads()):
114 for r in reversed(fl.heads()):
115 fn = ".hgsigs|%s" % hgnode.short(r)
115 fn = ".hgsigs|%s" % hgnode.short(r)
116 for item in parsefile(fl.read(r).splitlines(), fn):
116 for item in parsefile(fl.read(r).splitlines(), fn):
117 yield item
117 yield item
118 try:
118 try:
119 # read local signatures
119 # read local signatures
120 fn = "localsigs"
120 fn = "localsigs"
121 for item in parsefile(repo.vfs(fn), fn):
121 for item in parsefile(repo.vfs(fn), fn):
122 yield item
122 yield item
123 except IOError:
123 except IOError:
124 pass
124 pass
125
125
126 def getkeys(ui, repo, mygpg, sigdata, context):
126 def getkeys(ui, repo, mygpg, sigdata, context):
127 """get the keys who signed a data"""
127 """get the keys who signed a data"""
128 fn, ln = context
128 fn, ln = context
129 node, version, sig = sigdata
129 node, version, sig = sigdata
130 prefix = "%s:%d" % (fn, ln)
130 prefix = "%s:%d" % (fn, ln)
131 node = hgnode.bin(node)
131 node = hgnode.bin(node)
132
132
133 data = node2txt(repo, node, version)
133 data = node2txt(repo, node, version)
134 sig = binascii.a2b_base64(sig)
134 sig = binascii.a2b_base64(sig)
135 keys = mygpg.verify(data, sig)
135 keys = mygpg.verify(data, sig)
136
136
137 validkeys = []
137 validkeys = []
138 # warn for expired key and/or sigs
138 # warn for expired key and/or sigs
139 for key in keys:
139 for key in keys:
140 if key[0] == "ERRSIG":
140 if key[0] == "ERRSIG":
141 ui.write(_("%s Unknown key ID \"%s\"\n")
141 ui.write(_("%s Unknown key ID \"%s\"\n")
142 % (prefix, shortkey(ui, key[1][:15])))
142 % (prefix, shortkey(ui, key[1][:15])))
143 continue
143 continue
144 if key[0] == "BADSIG":
144 if key[0] == "BADSIG":
145 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
145 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
146 continue
146 continue
147 if key[0] == "EXPSIG":
147 if key[0] == "EXPSIG":
148 ui.write(_("%s Note: Signature has expired"
148 ui.write(_("%s Note: Signature has expired"
149 " (signed by: \"%s\")\n") % (prefix, key[2]))
149 " (signed by: \"%s\")\n") % (prefix, key[2]))
150 elif key[0] == "EXPKEYSIG":
150 elif key[0] == "EXPKEYSIG":
151 ui.write(_("%s Note: This key has expired"
151 ui.write(_("%s Note: This key has expired"
152 " (signed by: \"%s\")\n") % (prefix, key[2]))
152 " (signed by: \"%s\")\n") % (prefix, key[2]))
153 validkeys.append((key[1], key[2], key[3]))
153 validkeys.append((key[1], key[2], key[3]))
154 return validkeys
154 return validkeys
155
155
156 @command("sigs", [], _('hg sigs'))
156 @command("sigs", [], _('hg sigs'))
157 def sigs(ui, repo):
157 def sigs(ui, repo):
158 """list signed changesets"""
158 """list signed changesets"""
159 mygpg = newgpg(ui)
159 mygpg = newgpg(ui)
160 revs = {}
160 revs = {}
161
161
162 for data, context in sigwalk(repo):
162 for data, context in sigwalk(repo):
163 node, version, sig = data
163 node, version, sig = data
164 fn, ln = context
164 fn, ln = context
165 try:
165 try:
166 n = repo.lookup(node)
166 n = repo.lookup(node)
167 except KeyError:
167 except KeyError:
168 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
168 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
169 continue
169 continue
170 r = repo.changelog.rev(n)
170 r = repo.changelog.rev(n)
171 keys = getkeys(ui, repo, mygpg, data, context)
171 keys = getkeys(ui, repo, mygpg, data, context)
172 if not keys:
172 if not keys:
173 continue
173 continue
174 revs.setdefault(r, [])
174 revs.setdefault(r, [])
175 revs[r].extend(keys)
175 revs[r].extend(keys)
176 for rev in sorted(revs, reverse=True):
176 for rev in sorted(revs, reverse=True):
177 for k in revs[rev]:
177 for k in revs[rev]:
178 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
178 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
179 ui.write("%-30s %s\n" % (keystr(ui, k), r))
179 ui.write("%-30s %s\n" % (keystr(ui, k), r))
180
180
181 @command("sigcheck", [], _('hg sigcheck REV'))
181 @command("sigcheck", [], _('hg sigcheck REV'))
182 def sigcheck(ui, repo, rev):
182 def sigcheck(ui, repo, rev):
183 """verify all the signatures there may be for a particular revision"""
183 """verify all the signatures there may be for a particular revision"""
184 mygpg = newgpg(ui)
184 mygpg = newgpg(ui)
185 rev = repo.lookup(rev)
185 rev = repo.lookup(rev)
186 hexrev = hgnode.hex(rev)
186 hexrev = hgnode.hex(rev)
187 keys = []
187 keys = []
188
188
189 for data, context in sigwalk(repo):
189 for data, context in sigwalk(repo):
190 node, version, sig = data
190 node, version, sig = data
191 if node == hexrev:
191 if node == hexrev:
192 k = getkeys(ui, repo, mygpg, data, context)
192 k = getkeys(ui, repo, mygpg, data, context)
193 if k:
193 if k:
194 keys.extend(k)
194 keys.extend(k)
195
195
196 if not keys:
196 if not keys:
197 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
197 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
198 return
198 return
199
199
200 # print summary
200 # print summary
201 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
201 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
202 for key in keys:
202 for key in keys:
203 ui.write(" %s\n" % keystr(ui, key))
203 ui.write(" %s\n" % keystr(ui, key))
204
204
205 def keystr(ui, key):
205 def keystr(ui, key):
206 """associate a string to a key (username, comment)"""
206 """associate a string to a key (username, comment)"""
207 keyid, user, fingerprint = key
207 keyid, user, fingerprint = key
208 comment = ui.config("gpg", fingerprint, None)
208 comment = ui.config("gpg", fingerprint, None)
209 if comment:
209 if comment:
210 return "%s (%s)" % (user, comment)
210 return "%s (%s)" % (user, comment)
211 else:
211 else:
212 return user
212 return user
213
213
214 @command("sign",
214 @command("sign",
215 [('l', 'local', None, _('make the signature local')),
215 [('l', 'local', None, _('make the signature local')),
216 ('f', 'force', None, _('sign even if the sigfile is modified')),
216 ('f', 'force', None, _('sign even if the sigfile is modified')),
217 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
217 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
218 ('k', 'key', '',
218 ('k', 'key', '',
219 _('the key id to sign with'), _('ID')),
219 _('the key id to sign with'), _('ID')),
220 ('m', 'message', '',
220 ('m', 'message', '',
221 _('use text as commit message'), _('TEXT')),
221 _('use text as commit message'), _('TEXT')),
222 ('e', 'edit', False, _('invoke editor on commit messages')),
222 ('e', 'edit', False, _('invoke editor on commit messages')),
223 ] + commands.commitopts2,
223 ] + commands.commitopts2,
224 _('hg sign [OPTION]... [REV]...'))
224 _('hg sign [OPTION]... [REV]...'))
225 def sign(ui, repo, *revs, **opts):
225 def sign(ui, repo, *revs, **opts):
226 """add a signature for the current or given revision
226 """add a signature for the current or given revision
227
227
228 If no revision is given, the parent of the working directory is used,
228 If no revision is given, the parent of the working directory is used,
229 or tip if no revision is checked out.
229 or tip if no revision is checked out.
230
230
231 The ``gpg.cmd`` config setting can be used to specify the command
231 The ``gpg.cmd`` config setting can be used to specify the command
232 to run. A default key can be specified with ``gpg.key``.
232 to run. A default key can be specified with ``gpg.key``.
233
233
234 See :hg:`help dates` for a list of formats valid for -d/--date.
234 See :hg:`help dates` for a list of formats valid for -d/--date.
235 """
235 """
236 with repo.wlock():
236 with repo.wlock():
237 return _dosign(ui, repo, *revs, **opts)
237 return _dosign(ui, repo, *revs, **opts)
238
238
239 def _dosign(ui, repo, *revs, **opts):
239 def _dosign(ui, repo, *revs, **opts):
240 mygpg = newgpg(ui, **opts)
240 mygpg = newgpg(ui, **opts)
241 sigver = "0"
241 sigver = "0"
242 sigmessage = ""
242 sigmessage = ""
243
243
244 date = opts.get('date')
244 date = opts.get('date')
245 if date:
245 if date:
246 opts['date'] = util.parsedate(date)
246 opts['date'] = util.parsedate(date)
247
247
248 if revs:
248 if revs:
249 nodes = [repo.lookup(n) for n in revs]
249 nodes = [repo.lookup(n) for n in revs]
250 else:
250 else:
251 nodes = [node for node in repo.dirstate.parents()
251 nodes = [node for node in repo.dirstate.parents()
252 if node != hgnode.nullid]
252 if node != hgnode.nullid]
253 if len(nodes) > 1:
253 if len(nodes) > 1:
254 raise error.Abort(_('uncommitted merge - please provide a '
254 raise error.Abort(_('uncommitted merge - please provide a '
255 'specific revision'))
255 'specific revision'))
256 if not nodes:
256 if not nodes:
257 nodes = [repo.changelog.tip()]
257 nodes = [repo.changelog.tip()]
258
258
259 for n in nodes:
259 for n in nodes:
260 hexnode = hgnode.hex(n)
260 hexnode = hgnode.hex(n)
261 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
261 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
262 hgnode.short(n)))
262 hgnode.short(n)))
263 # build data
263 # build data
264 data = node2txt(repo, n, sigver)
264 data = node2txt(repo, n, sigver)
265 sig = mygpg.sign(data)
265 sig = mygpg.sign(data)
266 if not sig:
266 if not sig:
267 raise error.Abort(_("error while signing"))
267 raise error.Abort(_("error while signing"))
268 sig = binascii.b2a_base64(sig)
268 sig = binascii.b2a_base64(sig)
269 sig = sig.replace("\n", "")
269 sig = sig.replace("\n", "")
270 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
270 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
271
271
272 # write it
272 # write it
273 if opts['local']:
273 if opts['local']:
274 repo.vfs.append("localsigs", sigmessage)
274 repo.vfs.append("localsigs", sigmessage)
275 return
275 return
276
276
277 if not opts["force"]:
277 if not opts["force"]:
278 msigs = match.exact(repo.root, '', ['.hgsigs'])
278 msigs = match.exact(repo.root, '', ['.hgsigs'])
279 if any(repo.status(match=msigs, unknown=True, ignored=True)):
279 if any(repo.status(match=msigs, unknown=True, ignored=True)):
280 raise error.Abort(_("working copy of .hgsigs is changed "),
280 raise error.Abort(_("working copy of .hgsigs is changed "),
281 hint=_("please commit .hgsigs manually"))
281 hint=_("please commit .hgsigs manually"))
282
282
283 sigsfile = repo.wfile(".hgsigs", "ab")
283 sigsfile = repo.wfile(".hgsigs", "ab")
284 sigsfile.write(sigmessage)
284 sigsfile.write(sigmessage)
285 sigsfile.close()
285 sigsfile.close()
286
286
287 if '.hgsigs' not in repo.dirstate:
287 if '.hgsigs' not in repo.dirstate:
288 repo[None].add([".hgsigs"])
288 repo[None].add([".hgsigs"])
289
289
290 if opts["no_commit"]:
290 if opts["no_commit"]:
291 return
291 return
292
292
293 message = opts['message']
293 message = opts['message']
294 if not message:
294 if not message:
295 # we don't translate commit messages
295 # we don't translate commit messages
296 message = "\n".join(["Added signature for changeset %s"
296 message = "\n".join(["Added signature for changeset %s"
297 % hgnode.short(n)
297 % hgnode.short(n)
298 for n in nodes])
298 for n in nodes])
299 try:
299 try:
300 editor = cmdutil.getcommiteditor(editform='gpg.sign', **opts)
300 editor = cmdutil.getcommiteditor(editform='gpg.sign', **opts)
301 repo.commit(message, opts['user'], opts['date'], match=msigs,
301 repo.commit(message, opts['user'], opts['date'], match=msigs,
302 editor=editor)
302 editor=editor)
303 except ValueError as inst:
303 except ValueError as inst:
304 raise error.Abort(str(inst))
304 raise error.Abort(str(inst))
305
305
306 def shortkey(ui, key):
306 def shortkey(ui, key):
307 if len(key) != 16:
307 if len(key) != 16:
308 ui.debug("key ID \"%s\" format error\n" % key)
308 ui.debug("key ID \"%s\" format error\n" % key)
309 return key
309 return key
310
310
311 return key[-8:]
311 return key[-8:]
312
312
313 def node2txt(repo, node, ver):
313 def node2txt(repo, node, ver):
314 """map a manifest into some text"""
314 """map a manifest into some text"""
315 if ver == "0":
315 if ver == "0":
316 return "%s\n" % hgnode.hex(node)
316 return "%s\n" % hgnode.hex(node)
317 else:
317 else:
318 raise error.Abort(_("unknown signature version"))
318 raise error.Abort(_("unknown signature version"))
@@ -1,69 +1,69 b''
1 # ASCII graph log extension for Mercurial
1 # ASCII graph log extension for Mercurial
2 #
2 #
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to view revision graphs from a shell (DEPRECATED)
8 '''command to view revision graphs from a shell (DEPRECATED)
9
9
10 The functionality of this extension has been include in core Mercurial
10 The functionality of this extension has been include in core Mercurial
11 since version 2.3. Please use :hg:`log -G ...` instead.
11 since version 2.3. Please use :hg:`log -G ...` instead.
12
12
13 This extension adds a --graph option to the incoming, outgoing and log
13 This extension adds a --graph option to the incoming, outgoing and log
14 commands. When this options is given, an ASCII representation of the
14 commands. When this options is given, an ASCII representation of the
15 revision graph is also shown.
15 revision graph is also shown.
16 '''
16 '''
17
17
18 from __future__ import absolute_import
18 from __future__ import absolute_import
19
19
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import (
21 from mercurial import (
22 cmdutil,
22 cmdutil,
23 commands,
23 commands,
24 )
24 )
25
25
26 cmdtable = {}
26 cmdtable = {}
27 command = cmdutil.command(cmdtable)
27 command = cmdutil.command(cmdtable)
28 # Note for extension authors: ONLY specify testedwith = 'internal' for
28 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
29 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
29 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # be specifying the version(s) of Mercurial they are tested with, or
30 # be specifying the version(s) of Mercurial they are tested with, or
31 # leave the attribute unspecified.
31 # leave the attribute unspecified.
32 testedwith = 'internal'
32 testedwith = 'ships-with-hg-core'
33
33
34 @command('glog',
34 @command('glog',
35 [('f', 'follow', None,
35 [('f', 'follow', None,
36 _('follow changeset history, or file history across copies and renames')),
36 _('follow changeset history, or file history across copies and renames')),
37 ('', 'follow-first', None,
37 ('', 'follow-first', None,
38 _('only follow the first parent of merge changesets (DEPRECATED)')),
38 _('only follow the first parent of merge changesets (DEPRECATED)')),
39 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
39 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
40 ('C', 'copies', None, _('show copied files')),
40 ('C', 'copies', None, _('show copied files')),
41 ('k', 'keyword', [],
41 ('k', 'keyword', [],
42 _('do case-insensitive search for a given text'), _('TEXT')),
42 _('do case-insensitive search for a given text'), _('TEXT')),
43 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
43 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
44 ('', 'removed', None, _('include revisions where files were removed')),
44 ('', 'removed', None, _('include revisions where files were removed')),
45 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
45 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
46 ('u', 'user', [], _('revisions committed by user'), _('USER')),
46 ('u', 'user', [], _('revisions committed by user'), _('USER')),
47 ('', 'only-branch', [],
47 ('', 'only-branch', [],
48 _('show only changesets within the given named branch (DEPRECATED)'),
48 _('show only changesets within the given named branch (DEPRECATED)'),
49 _('BRANCH')),
49 _('BRANCH')),
50 ('b', 'branch', [],
50 ('b', 'branch', [],
51 _('show changesets within the given named branch'), _('BRANCH')),
51 _('show changesets within the given named branch'), _('BRANCH')),
52 ('P', 'prune', [],
52 ('P', 'prune', [],
53 _('do not display revision or any of its ancestors'), _('REV')),
53 _('do not display revision or any of its ancestors'), _('REV')),
54 ] + commands.logopts + commands.walkopts,
54 ] + commands.logopts + commands.walkopts,
55 _('[OPTION]... [FILE]'),
55 _('[OPTION]... [FILE]'),
56 inferrepo=True)
56 inferrepo=True)
57 def glog(ui, repo, *pats, **opts):
57 def glog(ui, repo, *pats, **opts):
58 """show revision history alongside an ASCII revision graph
58 """show revision history alongside an ASCII revision graph
59
59
60 Print a revision history alongside a revision graph drawn with
60 Print a revision history alongside a revision graph drawn with
61 ASCII characters.
61 ASCII characters.
62
62
63 Nodes printed as an @ character are parents of the working
63 Nodes printed as an @ character are parents of the working
64 directory.
64 directory.
65
65
66 This is an alias to :hg:`log -G`.
66 This is an alias to :hg:`log -G`.
67 """
67 """
68 opts['graph'] = True
68 opts['graph'] = True
69 return commands.log(ui, repo, *pats, **opts)
69 return commands.log(ui, repo, *pats, **opts)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file copied from mercurial/mpatch.c to mercurial/mpatch_module.c
NO CONTENT: file copied from mercurial/mpatch.c to mercurial/mpatch_module.c
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now