##// END OF EJS Templates
merge default into stable for 3.9 code freeze
Matt Mackall -
r29605:519bb4f9 merge 3.9-rc stable
parent child Browse files
Show More

The requested changes are 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 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
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 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,261 +1,261
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 clean:
63 clean:
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 packages mercurial/locale
71 rm -rf build packages 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 install: install-bin install-doc
75 install: install-bin install-doc
76
76
77 install-bin: build
77 install-bin: build
78 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
78 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
79
79
80 install-doc: doc
80 install-doc: doc
81 cd doc && $(MAKE) $(MFLAGS) install
81 cd doc && $(MAKE) $(MFLAGS) install
82
82
83 install-home: install-home-bin install-home-doc
83 install-home: install-home-bin install-home-doc
84
84
85 install-home-bin: build
85 install-home-bin: build
86 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
86 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
87
87
88 install-home-doc: doc
88 install-home-doc: doc
89 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
89 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
90
90
91 MANIFEST-doc:
91 MANIFEST-doc:
92 $(MAKE) -C doc MANIFEST
92 $(MAKE) -C doc MANIFEST
93
93
94 MANIFEST.in: MANIFEST-doc
94 MANIFEST.in: MANIFEST-doc
95 hg manifest | sed -e 's/^/include /' > MANIFEST.in
95 hg manifest | sed -e 's/^/include /' > MANIFEST.in
96 echo include mercurial/__version__.py >> MANIFEST.in
96 echo include mercurial/__version__.py >> MANIFEST.in
97 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
97 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
98
98
99 dist: tests dist-notests
99 dist: tests dist-notests
100
100
101 dist-notests: doc MANIFEST.in
101 dist-notests: doc MANIFEST.in
102 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
102 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
103
103
104 check: tests
104 check: tests
105
105
106 tests:
106 tests:
107 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
107 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
108
108
109 test-%:
109 test-%:
110 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
110 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
111
111
112 testpy-%:
112 testpy-%:
113 @echo Looking for Python $* in $(HGPYTHONS)
113 @echo Looking for Python $* in $(HGPYTHONS)
114 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
114 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
115 cd $$(mktemp --directory --tmpdir) && \
115 cd $$(mktemp --directory --tmpdir) && \
116 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
116 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
117 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
117 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
118
118
119 check-code:
119 check-code:
120 hg manifest | xargs python contrib/check-code.py
120 hg manifest | xargs python contrib/check-code.py
121
121
122 update-pot: i18n/hg.pot
122 update-pot: i18n/hg.pot
123
123
124 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
124 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
125 $(PYTHON) i18n/hggettext mercurial/commands.py \
125 $(PYTHON) i18n/hggettext mercurial/commands.py \
126 hgext/*.py hgext/*/__init__.py \
126 hgext/*.py hgext/*/__init__.py \
127 mercurial/fileset.py mercurial/revset.py \
127 mercurial/fileset.py mercurial/revset.py \
128 mercurial/templatefilters.py mercurial/templatekw.py \
128 mercurial/templatefilters.py mercurial/templatekw.py \
129 mercurial/templater.py \
129 mercurial/templater.py \
130 mercurial/filemerge.py \
130 mercurial/filemerge.py \
131 mercurial/hgweb/webcommands.py \
131 mercurial/hgweb/webcommands.py \
132 $(DOCFILES) > i18n/hg.pot.tmp
132 $(DOCFILES) > i18n/hg.pot.tmp
133 # All strings marked for translation in Mercurial contain
133 # All strings marked for translation in Mercurial contain
134 # ASCII characters only. But some files contain string
134 # ASCII characters only. But some files contain string
135 # literals like this '\037\213'. xgettext thinks it has to
135 # literals like this '\037\213'. xgettext thinks it has to
136 # parse them even though they are not marked for translation.
136 # parse them even though they are not marked for translation.
137 # Extracting with an explicit encoding of ISO-8859-1 will make
137 # Extracting with an explicit encoding of ISO-8859-1 will make
138 # xgettext "parse" and ignore them.
138 # xgettext "parse" and ignore them.
139 echo $(PYFILES) | xargs \
139 echo $(PYFILES) | xargs \
140 xgettext --package-name "Mercurial" \
140 xgettext --package-name "Mercurial" \
141 --msgid-bugs-address "<mercurial-devel@selenic.com>" \
141 --msgid-bugs-address "<mercurial-devel@selenic.com>" \
142 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
142 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
143 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
143 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
144 -d hg -p i18n -o hg.pot.tmp
144 -d hg -p i18n -o hg.pot.tmp
145 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
145 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
146 # The target file is not created before the last step. So it never is in
146 # The target file is not created before the last step. So it never is in
147 # an intermediate state.
147 # an intermediate state.
148 mv -f i18n/hg.pot.tmp i18n/hg.pot
148 mv -f i18n/hg.pot.tmp i18n/hg.pot
149
149
150 %.po: i18n/hg.pot
150 %.po: i18n/hg.pot
151 # work on a temporary copy for never having a half completed target
151 # work on a temporary copy for never having a half completed target
152 cp $@ $@.tmp
152 cp $@ $@.tmp
153 msgmerge --no-location --update $@.tmp $^
153 msgmerge --no-location --update $@.tmp $^
154 mv -f $@.tmp $@
154 mv -f $@.tmp $@
155
155
156 # Packaging targets
156 # Packaging targets
157
157
158 osx:
158 osx:
159 python setup.py install --optimize=1 \
159 /usr/bin/python2.7 setup.py install --optimize=1 \
160 --root=build/mercurial/ --prefix=/usr/local/ \
160 --root=build/mercurial/ --prefix=/usr/local/ \
161 --install-lib=/Library/Python/2.7/site-packages/
161 --install-lib=/Library/Python/2.7/site-packages/
162 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
162 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
163 mkdir -p $${OUTPUTDIR:-dist}
163 mkdir -p $${OUTPUTDIR:-dist}
164 pkgbuild --root build/mercurial/ --identifier org.mercurial-scm.mercurial \
164 pkgbuild --root build/mercurial/ --identifier org.mercurial-scm.mercurial \
165 build/mercurial.pkg
165 build/mercurial.pkg
166 HGVER=$$((cat build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py; echo 'print(version)') | python) && \
166 HGVER=$$((cat build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py; echo 'print(version)') | python) && \
167 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
167 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
168 productbuild --distribution contrib/macosx/distribution.xml \
168 productbuild --distribution contrib/macosx/distribution.xml \
169 --package-path build/ \
169 --package-path build/ \
170 --version "$${HGVER}" \
170 --version "$${HGVER}" \
171 --resources contrib/macosx/ \
171 --resources contrib/macosx/ \
172 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
172 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
173
173
174 deb:
174 deb:
175 contrib/builddeb
175 contrib/builddeb
176
176
177 ppa:
177 ppa:
178 contrib/builddeb --source-only
178 contrib/builddeb --source-only
179
179
180 docker-debian-jessie:
180 docker-debian-jessie:
181 mkdir -p packages/debian-jessie
181 mkdir -p packages/debian-jessie
182 contrib/dockerdeb debian jessie
182 contrib/dockerdeb debian jessie
183
183
184 contrib/docker/ubuntu-%: contrib/docker/ubuntu.template
184 contrib/docker/ubuntu-%: contrib/docker/ubuntu.template
185 sed "s/__CODENAME__/$*/" $< > $@
185 sed "s/__CODENAME__/$*/" $< > $@
186
186
187 docker-ubuntu-trusty: contrib/docker/ubuntu-trusty
187 docker-ubuntu-trusty: contrib/docker/ubuntu-trusty
188 contrib/dockerdeb ubuntu trusty
188 contrib/dockerdeb ubuntu trusty
189
189
190 docker-ubuntu-trusty-ppa: contrib/docker/ubuntu-trusty
190 docker-ubuntu-trusty-ppa: contrib/docker/ubuntu-trusty
191 contrib/dockerdeb ubuntu trusty --source-only
191 contrib/dockerdeb ubuntu trusty --source-only
192
192
193 docker-ubuntu-wily: contrib/docker/ubuntu-wily
193 docker-ubuntu-wily: contrib/docker/ubuntu-wily
194 contrib/dockerdeb ubuntu wily
194 contrib/dockerdeb ubuntu wily
195
195
196 docker-ubuntu-wily-ppa: contrib/docker/ubuntu-wily
196 docker-ubuntu-wily-ppa: contrib/docker/ubuntu-wily
197 contrib/dockerdeb ubuntu wily --source-only
197 contrib/dockerdeb ubuntu wily --source-only
198
198
199 docker-ubuntu-xenial: contrib/docker/ubuntu-xenial
199 docker-ubuntu-xenial: contrib/docker/ubuntu-xenial
200 contrib/dockerdeb ubuntu xenial
200 contrib/dockerdeb ubuntu xenial
201
201
202 docker-ubuntu-xenial-ppa: contrib/docker/ubuntu-xenial
202 docker-ubuntu-xenial-ppa: contrib/docker/ubuntu-xenial
203 contrib/dockerdeb ubuntu xenial --source-only
203 contrib/dockerdeb ubuntu xenial --source-only
204
204
205 fedora20:
205 fedora20:
206 mkdir -p packages/fedora20
206 mkdir -p packages/fedora20
207 contrib/buildrpm
207 contrib/buildrpm
208 cp rpmbuild/RPMS/*/* packages/fedora20
208 cp rpmbuild/RPMS/*/* packages/fedora20
209 cp rpmbuild/SRPMS/* packages/fedora20
209 cp rpmbuild/SRPMS/* packages/fedora20
210 rm -rf rpmbuild
210 rm -rf rpmbuild
211
211
212 docker-fedora20:
212 docker-fedora20:
213 mkdir -p packages/fedora20
213 mkdir -p packages/fedora20
214 contrib/dockerrpm fedora20
214 contrib/dockerrpm fedora20
215
215
216 fedora21:
216 fedora21:
217 mkdir -p packages/fedora21
217 mkdir -p packages/fedora21
218 contrib/buildrpm
218 contrib/buildrpm
219 cp rpmbuild/RPMS/*/* packages/fedora21
219 cp rpmbuild/RPMS/*/* packages/fedora21
220 cp rpmbuild/SRPMS/* packages/fedora21
220 cp rpmbuild/SRPMS/* packages/fedora21
221 rm -rf rpmbuild
221 rm -rf rpmbuild
222
222
223 docker-fedora21:
223 docker-fedora21:
224 mkdir -p packages/fedora21
224 mkdir -p packages/fedora21
225 contrib/dockerrpm fedora21
225 contrib/dockerrpm fedora21
226
226
227 centos5:
227 centos5:
228 mkdir -p packages/centos5
228 mkdir -p packages/centos5
229 contrib/buildrpm --withpython
229 contrib/buildrpm --withpython
230 cp rpmbuild/RPMS/*/* packages/centos5
230 cp rpmbuild/RPMS/*/* packages/centos5
231 cp rpmbuild/SRPMS/* packages/centos5
231 cp rpmbuild/SRPMS/* packages/centos5
232
232
233 docker-centos5:
233 docker-centos5:
234 mkdir -p packages/centos5
234 mkdir -p packages/centos5
235 contrib/dockerrpm centos5 --withpython
235 contrib/dockerrpm centos5 --withpython
236
236
237 centos6:
237 centos6:
238 mkdir -p packages/centos6
238 mkdir -p packages/centos6
239 contrib/buildrpm
239 contrib/buildrpm
240 cp rpmbuild/RPMS/*/* packages/centos6
240 cp rpmbuild/RPMS/*/* packages/centos6
241 cp rpmbuild/SRPMS/* packages/centos6
241 cp rpmbuild/SRPMS/* packages/centos6
242
242
243 docker-centos6:
243 docker-centos6:
244 mkdir -p packages/centos6
244 mkdir -p packages/centos6
245 contrib/dockerrpm centos6
245 contrib/dockerrpm centos6
246
246
247 centos7:
247 centos7:
248 mkdir -p packages/centos7
248 mkdir -p packages/centos7
249 contrib/buildrpm
249 contrib/buildrpm
250 cp rpmbuild/RPMS/*/* packages/centos7
250 cp rpmbuild/RPMS/*/* packages/centos7
251 cp rpmbuild/SRPMS/* packages/centos7
251 cp rpmbuild/SRPMS/* packages/centos7
252
252
253 docker-centos7:
253 docker-centos7:
254 mkdir -p packages/centos7
254 mkdir -p packages/centos7
255 contrib/dockerrpm centos7
255 contrib/dockerrpm centos7
256
256
257 .PHONY: help all local build doc clean install install-bin install-doc \
257 .PHONY: help all local build doc clean install install-bin install-doc \
258 install-home install-home-bin install-home-doc \
258 install-home install-home-bin install-home-doc \
259 dist dist-notests check tests check-code update-pot \
259 dist dist-notests check tests check-code update-pot \
260 osx fedora20 docker-fedora20 fedora21 docker-fedora21 \
260 osx fedora20 docker-fedora20 fedora21 docker-fedora21 \
261 centos5 docker-centos5 centos6 docker-centos6 centos7 docker-centos7
261 centos5 docker-centos5 centos6 docker-centos6 centos7 docker-centos7
@@ -1,642 +1,642
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 local files="$(_hg_cmd status -n$1 "glob:$cur**")"
93 local IFS=$'\n'
93 local IFS=$'\n'
94 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
94 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
95 }
95 }
96
96
97 _hg_branches()
97 _hg_branches()
98 {
98 {
99 local branches="$(_hg_cmd branches -q)"
99 local branches="$(_hg_cmd branches -q)"
100 local IFS=$'\n'
100 local IFS=$'\n'
101 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$branches' -- "$cur"))
101 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$branches' -- "$cur"))
102 }
102 }
103
103
104 _hg_bookmarks()
104 _hg_bookmarks()
105 {
105 {
106 local bookmarks="$(_hg_cmd bookmarks -q)"
106 local bookmarks="$(_hg_cmd bookmarks -q)"
107 local IFS=$'\n'
107 local IFS=$'\n'
108 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$bookmarks' -- "$cur"))
108 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$bookmarks' -- "$cur"))
109 }
109 }
110
110
111 _hg_labels()
111 _hg_labels()
112 {
112 {
113 local labels="$(_hg_cmd debugnamecomplete "$cur")"
113 local labels="$(_hg_cmd debugnamecomplete "$cur")"
114 local IFS=$'\n'
114 local IFS=$'\n'
115 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$labels' -- "$cur"))
115 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$labels' -- "$cur"))
116 }
116 }
117
117
118 # this is "kind of" ugly...
118 # this is "kind of" ugly...
119 _hg_count_non_option()
119 _hg_count_non_option()
120 {
120 {
121 local i count=0
121 local i count=0
122 local filters="$1"
122 local filters="$1"
123
123
124 for ((i=1; $i<=$COMP_CWORD; i++)); do
124 for ((i=1; $i<=$COMP_CWORD; i++)); do
125 if [[ "${COMP_WORDS[i]}" != -* ]]; then
125 if [[ "${COMP_WORDS[i]}" != -* ]]; then
126 if [[ ${COMP_WORDS[i-1]} == @($filters|$global_args) ]]; then
126 if [[ ${COMP_WORDS[i-1]} == @($filters|$global_args) ]]; then
127 continue
127 continue
128 fi
128 fi
129 count=$(($count + 1))
129 count=$(($count + 1))
130 fi
130 fi
131 done
131 done
132
132
133 echo $(($count - 1))
133 echo $(($count - 1))
134 }
134 }
135
135
136 _hg_fix_wordlist()
136 _hg_fix_wordlist()
137 {
137 {
138 local LASTCHAR=' '
138 local LASTCHAR=' '
139 if [ ${#COMPREPLY[@]} = 1 ]; then
139 if [ ${#COMPREPLY[@]} = 1 ]; then
140 [ -d "$COMPREPLY" ] && LASTCHAR=/
140 [ -d "$COMPREPLY" ] && LASTCHAR=/
141 COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR")
141 COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR")
142 else
142 else
143 for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
143 for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
144 [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/
144 [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/
145 done
145 done
146 fi
146 fi
147 }
147 }
148
148
149 _hg()
149 _hg()
150 {
150 {
151 local cur prev cmd cmd_index opts i aliashg
151 local cur prev cmd cmd_index opts i aliashg
152 # global options that receive an argument
152 # global options that receive an argument
153 local global_args='--cwd|-R|--repository'
153 local global_args='--cwd|-R|--repository'
154 local hg="$1"
154 local hg="$1"
155 local canonical=0
155 local canonical=0
156
156
157 aliashg=$(alias $hg 2>/dev/null)
157 aliashg=$(alias $hg 2>/dev/null)
158 if [[ -n "$aliashg" ]]; then
158 if [[ -n "$aliashg" ]]; then
159 aliashg=${aliashg#"alias $hg='"}
159 aliashg=${aliashg#"alias $hg='"}
160 aliashg=${aliashg%"'"}
160 aliashg=${aliashg%"'"}
161 hg=$aliashg
161 hg=$aliashg
162 fi
162 fi
163
163
164 COMPREPLY=()
164 COMPREPLY=()
165 cur="$2"
165 cur="$2"
166 prev="$3"
166 prev="$3"
167
167
168 # searching for the command
168 # searching for the command
169 # (first non-option argument that doesn't follow a global option that
169 # (first non-option argument that doesn't follow a global option that
170 # receives an argument)
170 # receives an argument)
171 for ((i=1; $i<=$COMP_CWORD; i++)); do
171 for ((i=1; $i<=$COMP_CWORD; i++)); do
172 if [[ ${COMP_WORDS[i]} != -* ]]; then
172 if [[ ${COMP_WORDS[i]} != -* ]]; then
173 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
173 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
174 cmd="${COMP_WORDS[i]}"
174 cmd="${COMP_WORDS[i]}"
175 cmd_index=$i
175 cmd_index=$i
176 break
176 break
177 fi
177 fi
178 fi
178 fi
179 done
179 done
180
180
181 if [[ "$cur" == -* ]]; then
181 if [[ "$cur" == -* ]]; then
182 if [ "$(type -t "_hg_opt_$cmd")" = function ] && "_hg_opt_$cmd"; then
182 if [ "$(type -t "_hg_opt_$cmd")" = function ] && "_hg_opt_$cmd"; then
183 _hg_fix_wordlist
183 _hg_fix_wordlist
184 return
184 return
185 fi
185 fi
186
186
187 opts=$(_hg_cmd debugcomplete --options "$cmd")
187 opts=$(HGPLAINEXCEPT=alias _hg_cmd debugcomplete --options "$cmd")
188
188
189 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur"))
189 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur"))
190 _hg_fix_wordlist
190 _hg_fix_wordlist
191 return
191 return
192 fi
192 fi
193
193
194 # global options
194 # global options
195 case "$prev" in
195 case "$prev" in
196 -R|--repository)
196 -R|--repository)
197 _hg_paths
197 _hg_paths
198 _hg_repos
198 _hg_repos
199 _hg_fix_wordlist
199 _hg_fix_wordlist
200 return
200 return
201 ;;
201 ;;
202 --cwd)
202 --cwd)
203 # Stick with default bash completion
203 # Stick with default bash completion
204 _hg_fix_wordlist
204 _hg_fix_wordlist
205 return
205 return
206 ;;
206 ;;
207 esac
207 esac
208
208
209 if [ -z "$cmd" ] || [ $COMP_CWORD -eq $i ]; then
209 if [ -z "$cmd" ] || [ $COMP_CWORD -eq $i ]; then
210 _hg_commands
210 _hg_commands
211 _hg_fix_wordlist
211 _hg_fix_wordlist
212 return
212 return
213 fi
213 fi
214
214
215 # try to generate completion candidates for whatever command the user typed
215 # try to generate completion candidates for whatever command the user typed
216 local help
216 local help
217 if _hg_command_specific; then
217 if _hg_command_specific; then
218 _hg_fix_wordlist
218 _hg_fix_wordlist
219 return
219 return
220 fi
220 fi
221
221
222 # canonicalize the command name and try again
222 # canonicalize the command name and try again
223 help=$(_hg_cmd help "$cmd")
223 help=$(_hg_cmd help "$cmd")
224 if [ $? -ne 0 ]; then
224 if [ $? -ne 0 ]; then
225 # Probably either the command doesn't exist or it's ambiguous
225 # Probably either the command doesn't exist or it's ambiguous
226 return
226 return
227 fi
227 fi
228 cmd=${help#hg }
228 cmd=${help#hg }
229 cmd=${cmd%%[$' \n']*}
229 cmd=${cmd%%[$' \n']*}
230 canonical=1
230 canonical=1
231 _hg_command_specific
231 _hg_command_specific
232 _hg_fix_wordlist
232 _hg_fix_wordlist
233 }
233 }
234
234
235 _hg_command_specific()
235 _hg_command_specific()
236 {
236 {
237 if [ "$(type -t "_hg_cmd_$cmd")" = function ]; then
237 if [ "$(type -t "_hg_cmd_$cmd")" = function ]; then
238 "_hg_cmd_$cmd"
238 "_hg_cmd_$cmd"
239 return 0
239 return 0
240 fi
240 fi
241
241
242 if [ "$cmd" != status ]; then
242 if [ "$cmd" != status ]; then
243 case "$prev" in
243 case "$prev" in
244 -r|--rev)
244 -r|--rev)
245 if [[ $canonical = 1 || status != "$cmd"* ]]; then
245 if [[ $canonical = 1 || status != "$cmd"* ]]; then
246 _hg_labels
246 _hg_labels
247 return 0
247 return 0
248 fi
248 fi
249 return 1
249 return 1
250 ;;
250 ;;
251 -B|--bookmark)
251 -B|--bookmark)
252 if [[ $canonical = 1 || status != "$cmd"* ]]; then
252 if [[ $canonical = 1 || status != "$cmd"* ]]; then
253 _hg_bookmarks
253 _hg_bookmarks
254 return 0
254 return 0
255 fi
255 fi
256 return 1
256 return 1
257 ;;
257 ;;
258 -b|--branch)
258 -b|--branch)
259 if [[ $canonical = 1 || status != "$cmd"* ]]; then
259 if [[ $canonical = 1 || status != "$cmd"* ]]; then
260 _hg_branches
260 _hg_branches
261 return 0
261 return 0
262 fi
262 fi
263 return 1
263 return 1
264 ;;
264 ;;
265 esac
265 esac
266 fi
266 fi
267
267
268 local aliascmd=$(_hg_cmd showconfig alias.$cmd | awk '{print $1}')
268 local aliascmd=$(_hg_cmd showconfig alias.$cmd | awk '{print $1}')
269 [ -n "$aliascmd" ] && cmd=$aliascmd
269 [ -n "$aliascmd" ] && cmd=$aliascmd
270
270
271 case "$cmd" in
271 case "$cmd" in
272 help)
272 help)
273 _hg_commands
273 _hg_commands
274 ;;
274 ;;
275 export)
275 export)
276 if _hg_ext_mq_patchlist qapplied && [ "${COMPREPLY[*]}" ]; then
276 if _hg_ext_mq_patchlist qapplied && [ "${COMPREPLY[*]}" ]; then
277 return 0
277 return 0
278 fi
278 fi
279 _hg_labels
279 _hg_labels
280 ;;
280 ;;
281 manifest|update|up|checkout|co)
281 manifest|update|up|checkout|co)
282 _hg_labels
282 _hg_labels
283 ;;
283 ;;
284 pull|push|outgoing|incoming)
284 pull|push|outgoing|incoming)
285 _hg_paths
285 _hg_paths
286 _hg_repos
286 _hg_repos
287 ;;
287 ;;
288 paths)
288 paths)
289 _hg_paths
289 _hg_paths
290 ;;
290 ;;
291 add)
291 add)
292 _hg_status "u"
292 _hg_status "u"
293 ;;
293 ;;
294 merge)
294 merge)
295 _hg_labels
295 _hg_labels
296 ;;
296 ;;
297 commit|ci|record)
297 commit|ci|record)
298 _hg_status "mar"
298 _hg_status "mar"
299 ;;
299 ;;
300 remove|rm)
300 remove|rm)
301 _hg_debugpathcomplete -n
301 _hg_debugpathcomplete -n
302 ;;
302 ;;
303 forget)
303 forget)
304 _hg_debugpathcomplete -fa
304 _hg_debugpathcomplete -fa
305 ;;
305 ;;
306 diff)
306 diff)
307 _hg_status "mar"
307 _hg_status "mar"
308 ;;
308 ;;
309 revert)
309 revert)
310 _hg_debugpathcomplete
310 _hg_debugpathcomplete
311 ;;
311 ;;
312 clone)
312 clone)
313 local count=$(_hg_count_non_option)
313 local count=$(_hg_count_non_option)
314 if [ $count = 1 ]; then
314 if [ $count = 1 ]; then
315 _hg_paths
315 _hg_paths
316 fi
316 fi
317 _hg_repos
317 _hg_repos
318 ;;
318 ;;
319 debugindex|debugindexdot)
319 debugindex|debugindexdot)
320 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.i" -- "$cur"))
320 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.i" -- "$cur"))
321 ;;
321 ;;
322 debugdata)
322 debugdata)
323 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.d" -- "$cur"))
323 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.d" -- "$cur"))
324 ;;
324 ;;
325 *)
325 *)
326 return 1
326 return 1
327 ;;
327 ;;
328 esac
328 esac
329
329
330 return 0
330 return 0
331 }
331 }
332
332
333 complete -o bashdefault -o default -o nospace -F _hg hg \
333 complete -o bashdefault -o default -o nospace -F _hg hg \
334 || complete -o default -o nospace -F _hg hg
334 || complete -o default -o nospace -F _hg hg
335
335
336
336
337 # Completion for commands provided by extensions
337 # Completion for commands provided by extensions
338
338
339 # bookmarks
339 # bookmarks
340 _hg_cmd_bookmarks()
340 _hg_cmd_bookmarks()
341 {
341 {
342 _hg_bookmarks
342 _hg_bookmarks
343 return
343 return
344 }
344 }
345
345
346 # mq
346 # mq
347 _hg_ext_mq_patchlist()
347 _hg_ext_mq_patchlist()
348 {
348 {
349 local patches
349 local patches
350 patches=$(_hg_cmd $1)
350 patches=$(_hg_cmd $1)
351 if [ $? -eq 0 ] && [ "$patches" ]; then
351 if [ $? -eq 0 ] && [ "$patches" ]; then
352 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$patches' -- "$cur"))
352 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$patches' -- "$cur"))
353 return 0
353 return 0
354 fi
354 fi
355 return 1
355 return 1
356 }
356 }
357
357
358 _hg_ext_mq_queues()
358 _hg_ext_mq_queues()
359 {
359 {
360 local root=$(_hg_cmd root)
360 local root=$(_hg_cmd root)
361 local n
361 local n
362 for n in $(cd "$root"/.hg && compgen -d -- "$cur"); do
362 for n in $(cd "$root"/.hg && compgen -d -- "$cur"); do
363 # I think we're usually not interested in the regular "patches" queue
363 # I think we're usually not interested in the regular "patches" queue
364 # so just filter it.
364 # so just filter it.
365 if [ "$n" != patches ] && [ -e "$root/.hg/$n/series" ]; then
365 if [ "$n" != patches ] && [ -e "$root/.hg/$n/series" ]; then
366 COMPREPLY=(${COMPREPLY[@]:-} "$n")
366 COMPREPLY=(${COMPREPLY[@]:-} "$n")
367 fi
367 fi
368 done
368 done
369 }
369 }
370
370
371 _hg_cmd_qpop()
371 _hg_cmd_qpop()
372 {
372 {
373 if [[ "$prev" = @(-n|--name) ]]; then
373 if [[ "$prev" = @(-n|--name) ]]; then
374 _hg_ext_mq_queues
374 _hg_ext_mq_queues
375 return
375 return
376 fi
376 fi
377 _hg_ext_mq_patchlist qapplied
377 _hg_ext_mq_patchlist qapplied
378 }
378 }
379
379
380 _hg_cmd_qpush()
380 _hg_cmd_qpush()
381 {
381 {
382 if [[ "$prev" = @(-n|--name) ]]; then
382 if [[ "$prev" = @(-n|--name) ]]; then
383 _hg_ext_mq_queues
383 _hg_ext_mq_queues
384 return
384 return
385 fi
385 fi
386 _hg_ext_mq_patchlist qunapplied
386 _hg_ext_mq_patchlist qunapplied
387 }
387 }
388
388
389 _hg_cmd_qgoto()
389 _hg_cmd_qgoto()
390 {
390 {
391 if [[ "$prev" = @(-n|--name) ]]; then
391 if [[ "$prev" = @(-n|--name) ]]; then
392 _hg_ext_mq_queues
392 _hg_ext_mq_queues
393 return
393 return
394 fi
394 fi
395 _hg_ext_mq_patchlist qseries
395 _hg_ext_mq_patchlist qseries
396 }
396 }
397
397
398 _hg_cmd_qdelete()
398 _hg_cmd_qdelete()
399 {
399 {
400 local qcmd=qunapplied
400 local qcmd=qunapplied
401 if [[ "$prev" = @(-r|--rev) ]]; then
401 if [[ "$prev" = @(-r|--rev) ]]; then
402 qcmd=qapplied
402 qcmd=qapplied
403 fi
403 fi
404 _hg_ext_mq_patchlist $qcmd
404 _hg_ext_mq_patchlist $qcmd
405 }
405 }
406
406
407 _hg_cmd_qfinish()
407 _hg_cmd_qfinish()
408 {
408 {
409 if [[ "$prev" = @(-a|--applied) ]]; then
409 if [[ "$prev" = @(-a|--applied) ]]; then
410 return
410 return
411 fi
411 fi
412 _hg_ext_mq_patchlist qapplied
412 _hg_ext_mq_patchlist qapplied
413 }
413 }
414
414
415 _hg_cmd_qsave()
415 _hg_cmd_qsave()
416 {
416 {
417 if [[ "$prev" = @(-n|--name) ]]; then
417 if [[ "$prev" = @(-n|--name) ]]; then
418 _hg_ext_mq_queues
418 _hg_ext_mq_queues
419 return
419 return
420 fi
420 fi
421 }
421 }
422
422
423 _hg_cmd_rebase() {
423 _hg_cmd_rebase() {
424 if [[ "$prev" = @(-s|--source|-d|--dest|-b|--base|-r|--rev) ]]; then
424 if [[ "$prev" = @(-s|--source|-d|--dest|-b|--base|-r|--rev) ]]; then
425 _hg_labels
425 _hg_labels
426 return
426 return
427 fi
427 fi
428 }
428 }
429
429
430 _hg_cmd_strip()
430 _hg_cmd_strip()
431 {
431 {
432 if [[ "$prev" = @(-B|--bookmark) ]]; then
432 if [[ "$prev" = @(-B|--bookmark) ]]; then
433 _hg_bookmarks
433 _hg_bookmarks
434 return
434 return
435 fi
435 fi
436 _hg_labels
436 _hg_labels
437 }
437 }
438
438
439 _hg_cmd_qcommit()
439 _hg_cmd_qcommit()
440 {
440 {
441 local root=$(_hg_cmd root)
441 local root=$(_hg_cmd root)
442 # this is run in a sub-shell, so we can't use _hg_status
442 # 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)
443 local files=$(cd "$root/.hg/patches" && _hg_cmd status -nmar)
444 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
444 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
445 }
445 }
446
446
447 _hg_cmd_qfold()
447 _hg_cmd_qfold()
448 {
448 {
449 _hg_ext_mq_patchlist qunapplied
449 _hg_ext_mq_patchlist qunapplied
450 }
450 }
451
451
452 _hg_cmd_qrename()
452 _hg_cmd_qrename()
453 {
453 {
454 _hg_ext_mq_patchlist qseries
454 _hg_ext_mq_patchlist qseries
455 }
455 }
456
456
457 _hg_cmd_qheader()
457 _hg_cmd_qheader()
458 {
458 {
459 _hg_ext_mq_patchlist qseries
459 _hg_ext_mq_patchlist qseries
460 }
460 }
461
461
462 _hg_cmd_qclone()
462 _hg_cmd_qclone()
463 {
463 {
464 local count=$(_hg_count_non_option)
464 local count=$(_hg_count_non_option)
465 if [ $count = 1 ]; then
465 if [ $count = 1 ]; then
466 _hg_paths
466 _hg_paths
467 fi
467 fi
468 _hg_repos
468 _hg_repos
469 }
469 }
470
470
471 _hg_ext_mq_guards()
471 _hg_ext_mq_guards()
472 {
472 {
473 _hg_cmd qselect --series | sed -e 's/^.//'
473 _hg_cmd qselect --series | sed -e 's/^.//'
474 }
474 }
475
475
476 _hg_cmd_qselect()
476 _hg_cmd_qselect()
477 {
477 {
478 local guards=$(_hg_ext_mq_guards)
478 local guards=$(_hg_ext_mq_guards)
479 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$guards' -- "$cur"))
479 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$guards' -- "$cur"))
480 }
480 }
481
481
482 _hg_cmd_qguard()
482 _hg_cmd_qguard()
483 {
483 {
484 local prefix=''
484 local prefix=''
485
485
486 if [[ "$cur" == +* ]]; then
486 if [[ "$cur" == +* ]]; then
487 prefix=+
487 prefix=+
488 elif [[ "$cur" == -* ]]; then
488 elif [[ "$cur" == -* ]]; then
489 prefix=-
489 prefix=-
490 fi
490 fi
491 local ncur=${cur#[-+]}
491 local ncur=${cur#[-+]}
492
492
493 if ! [ "$prefix" ]; then
493 if ! [ "$prefix" ]; then
494 _hg_ext_mq_patchlist qseries
494 _hg_ext_mq_patchlist qseries
495 return
495 return
496 fi
496 fi
497
497
498 local guards=$(_hg_ext_mq_guards)
498 local guards=$(_hg_ext_mq_guards)
499 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -P $prefix -W '$guards' -- "$ncur"))
499 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -P $prefix -W '$guards' -- "$ncur"))
500 }
500 }
501
501
502 _hg_opt_qguard()
502 _hg_opt_qguard()
503 {
503 {
504 local i
504 local i
505 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
505 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
506 if [[ ${COMP_WORDS[i]} != -* ]]; then
506 if [[ ${COMP_WORDS[i]} != -* ]]; then
507 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
507 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
508 _hg_cmd_qguard
508 _hg_cmd_qguard
509 return 0
509 return 0
510 fi
510 fi
511 elif [ "${COMP_WORDS[i]}" = -- ]; then
511 elif [ "${COMP_WORDS[i]}" = -- ]; then
512 _hg_cmd_qguard
512 _hg_cmd_qguard
513 return 0
513 return 0
514 fi
514 fi
515 done
515 done
516 return 1
516 return 1
517 }
517 }
518
518
519 _hg_cmd_qqueue()
519 _hg_cmd_qqueue()
520 {
520 {
521 local q
521 local q
522 local queues
522 local queues
523 local opts="--list --create --rename --delete --purge"
523 local opts="--list --create --rename --delete --purge"
524
524
525 queues=$( _hg_cmd qqueue --quiet )
525 queues=$( _hg_cmd qqueue --quiet )
526
526
527 COMPREPLY=( $( compgen -W "${opts} ${queues}" "${cur}" ) )
527 COMPREPLY=( $( compgen -W "${opts} ${queues}" "${cur}" ) )
528 }
528 }
529
529
530
530
531 # hbisect
531 # hbisect
532 _hg_cmd_bisect()
532 _hg_cmd_bisect()
533 {
533 {
534 local i subcmd
534 local i subcmd
535
535
536 # find the sub-command
536 # find the sub-command
537 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
537 for ((i=cmd_index+1; i<=COMP_CWORD; i++)); do
538 if [[ ${COMP_WORDS[i]} != -* ]]; then
538 if [[ ${COMP_WORDS[i]} != -* ]]; then
539 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
539 if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then
540 subcmd="${COMP_WORDS[i]}"
540 subcmd="${COMP_WORDS[i]}"
541 break
541 break
542 fi
542 fi
543 fi
543 fi
544 done
544 done
545
545
546 if [ -z "$subcmd" ] || [ $COMP_CWORD -eq $i ] || [ "$subcmd" = help ]; then
546 if [ -z "$subcmd" ] || [ $COMP_CWORD -eq $i ] || [ "$subcmd" = help ]; then
547 COMPREPLY=(${COMPREPLY[@]:-}
547 COMPREPLY=(${COMPREPLY[@]:-}
548 $(compgen -W 'bad good help init next reset' -- "$cur"))
548 $(compgen -W 'bad good help init next reset' -- "$cur"))
549 return
549 return
550 fi
550 fi
551
551
552 case "$subcmd" in
552 case "$subcmd" in
553 good|bad)
553 good|bad)
554 _hg_labels
554 _hg_labels
555 ;;
555 ;;
556 esac
556 esac
557
557
558 return
558 return
559 }
559 }
560
560
561
561
562 # patchbomb
562 # patchbomb
563 _hg_cmd_email()
563 _hg_cmd_email()
564 {
564 {
565 case "$prev" in
565 case "$prev" in
566 -c|--cc|-t|--to|-f|--from|--bcc)
566 -c|--cc|-t|--to|-f|--from|--bcc)
567 # we need an e-mail address. let the user provide a function
567 # we need an e-mail address. let the user provide a function
568 # to get them
568 # to get them
569 if [ "$(type -t _hg_emails)" = function ]; then
569 if [ "$(type -t _hg_emails)" = function ]; then
570 local arg=to
570 local arg=to
571 if [[ "$prev" == @(-f|--from) ]]; then
571 if [[ "$prev" == @(-f|--from) ]]; then
572 arg=from
572 arg=from
573 fi
573 fi
574 local addresses=$(_hg_emails $arg)
574 local addresses=$(_hg_emails $arg)
575 COMPREPLY=(${COMPREPLY[@]:-}
575 COMPREPLY=(${COMPREPLY[@]:-}
576 $(compgen -W '$addresses' -- "$cur"))
576 $(compgen -W '$addresses' -- "$cur"))
577 fi
577 fi
578 return
578 return
579 ;;
579 ;;
580 -m|--mbox)
580 -m|--mbox)
581 # fallback to standard filename completion
581 # fallback to standard filename completion
582 return
582 return
583 ;;
583 ;;
584 -s|--subject)
584 -s|--subject)
585 # free form string
585 # free form string
586 return
586 return
587 ;;
587 ;;
588 esac
588 esac
589
589
590 _hg_labels
590 _hg_labels
591 return
591 return
592 }
592 }
593
593
594
594
595 # gpg
595 # gpg
596 _hg_cmd_sign()
596 _hg_cmd_sign()
597 {
597 {
598 _hg_labels
598 _hg_labels
599 }
599 }
600
600
601
601
602 # transplant
602 # transplant
603 _hg_cmd_transplant()
603 _hg_cmd_transplant()
604 {
604 {
605 case "$prev" in
605 case "$prev" in
606 -s|--source)
606 -s|--source)
607 _hg_paths
607 _hg_paths
608 _hg_repos
608 _hg_repos
609 return
609 return
610 ;;
610 ;;
611 --filter)
611 --filter)
612 # standard filename completion
612 # standard filename completion
613 return
613 return
614 ;;
614 ;;
615 esac
615 esac
616
616
617 # all other transplant options values and command parameters are revisions
617 # all other transplant options values and command parameters are revisions
618 _hg_labels
618 _hg_labels
619 return
619 return
620 }
620 }
621
621
622 # shelve
622 # shelve
623 _hg_shelves()
623 _hg_shelves()
624 {
624 {
625 local shelves="$(_hg_cmd shelve -ql)"
625 local shelves="$(_hg_cmd shelve -ql)"
626 local IFS=$'\n'
626 local IFS=$'\n'
627 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$shelves' -- "$cur"))
627 COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$shelves' -- "$cur"))
628 }
628 }
629
629
630 _hg_cmd_shelve()
630 _hg_cmd_shelve()
631 {
631 {
632 if [[ "$prev" = @(-d|--delete|-l|--list|-p|--patch|--stat) ]]; then
632 if [[ "$prev" = @(-d|--delete|-l|--list|-p|--patch|--stat) ]]; then
633 _hg_shelves
633 _hg_shelves
634 else
634 else
635 _hg_status "mard"
635 _hg_status "mard"
636 fi
636 fi
637 }
637 }
638
638
639 _hg_cmd_unshelve()
639 _hg_cmd_unshelve()
640 {
640 {
641 _hg_shelves
641 _hg_shelves
642 }
642 }
@@ -1,98 +1,100
1 # Randomized torture test generation for bdiff
1 # Randomized torture test generation for bdiff
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4 import random, sys
4 import random
5 import sys
6
5 from mercurial import (
7 from mercurial import (
6 bdiff,
8 bdiff,
7 mpatch,
9 mpatch,
8 )
10 )
9
11
10 def reducetest(a, b):
12 def reducetest(a, b):
11 tries = 0
13 tries = 0
12 reductions = 0
14 reductions = 0
13 print("reducing...")
15 print("reducing...")
14 while tries < 1000:
16 while tries < 1000:
15 a2 = "\n".join(l for l in a.splitlines()
17 a2 = "\n".join(l for l in a.splitlines()
16 if random.randint(0, 100) > 0) + "\n"
18 if random.randint(0, 100) > 0) + "\n"
17 b2 = "\n".join(l for l in b.splitlines()
19 b2 = "\n".join(l for l in b.splitlines()
18 if random.randint(0, 100) > 0) + "\n"
20 if random.randint(0, 100) > 0) + "\n"
19 if a2 == a and b2 == b:
21 if a2 == a and b2 == b:
20 continue
22 continue
21 if a2 == b2:
23 if a2 == b2:
22 continue
24 continue
23 tries += 1
25 tries += 1
24
26
25 try:
27 try:
26 test1(a, b)
28 test1(a, b)
27 except Exception as inst:
29 except Exception as inst:
28 reductions += 1
30 reductions += 1
29 tries = 0
31 tries = 0
30 a = a2
32 a = a2
31 b = b2
33 b = b2
32
34
33 print("reduced:", reductions, len(a) + len(b),
35 print("reduced:", reductions, len(a) + len(b),
34 repr(a), repr(b))
36 repr(a), repr(b))
35 try:
37 try:
36 test1(a, b)
38 test1(a, b)
37 except Exception as inst:
39 except Exception as inst:
38 print("failed:", inst)
40 print("failed:", inst)
39
41
40 sys.exit(0)
42 sys.exit(0)
41
43
42 def test1(a, b):
44 def test1(a, b):
43 d = bdiff.bdiff(a, b)
45 d = bdiff.bdiff(a, b)
44 if not d:
46 if not d:
45 raise ValueError("empty")
47 raise ValueError("empty")
46 c = mpatch.patches(a, [d])
48 c = mpatch.patches(a, [d])
47 if c != b:
49 if c != b:
48 raise ValueError("bad")
50 raise ValueError("bad")
49
51
50 def testwrap(a, b):
52 def testwrap(a, b):
51 try:
53 try:
52 test1(a, b)
54 test1(a, b)
53 return
55 return
54 except Exception as inst:
56 except Exception as inst:
55 pass
57 pass
56 print("exception:", inst)
58 print("exception:", inst)
57 reducetest(a, b)
59 reducetest(a, b)
58
60
59 def test(a, b):
61 def test(a, b):
60 testwrap(a, b)
62 testwrap(a, b)
61 testwrap(b, a)
63 testwrap(b, a)
62
64
63 def rndtest(size, noise):
65 def rndtest(size, noise):
64 a = []
66 a = []
65 src = " aaaaaaaabbbbccd"
67 src = " aaaaaaaabbbbccd"
66 for x in xrange(size):
68 for x in xrange(size):
67 a.append(src[random.randint(0, len(src) - 1)])
69 a.append(src[random.randint(0, len(src) - 1)])
68
70
69 while True:
71 while True:
70 b = [c for c in a if random.randint(0, 99) > noise]
72 b = [c for c in a if random.randint(0, 99) > noise]
71 b2 = []
73 b2 = []
72 for c in b:
74 for c in b:
73 b2.append(c)
75 b2.append(c)
74 while random.randint(0, 99) < noise:
76 while random.randint(0, 99) < noise:
75 b2.append(src[random.randint(0, len(src) - 1)])
77 b2.append(src[random.randint(0, len(src) - 1)])
76 if b2 != a:
78 if b2 != a:
77 break
79 break
78
80
79 a = "\n".join(a) + "\n"
81 a = "\n".join(a) + "\n"
80 b = "\n".join(b2) + "\n"
82 b = "\n".join(b2) + "\n"
81
83
82 test(a, b)
84 test(a, b)
83
85
84 maxvol = 10000
86 maxvol = 10000
85 startsize = 2
87 startsize = 2
86 while True:
88 while True:
87 size = startsize
89 size = startsize
88 count = 0
90 count = 0
89 while size < maxvol:
91 while size < maxvol:
90 print(size)
92 print(size)
91 volume = 0
93 volume = 0
92 while volume < maxvol:
94 while volume < maxvol:
93 rndtest(size, 2)
95 rndtest(size, 2)
94 volume += size
96 volume += size
95 count += 2
97 count += 2
96 size *= 2
98 size *= 2
97 maxvol *= 4
99 maxvol *= 4
98 startsize *= 4
100 startsize *= 4
@@ -1,609 +1,657
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:
30 opentext = open
31 else:
32 def opentext(f):
33 return open(f, encoding='ascii')
34 try:
35 xrange
36 except NameError:
37 xrange = range
29 try:
38 try:
30 import re2
39 import re2
31 except ImportError:
40 except ImportError:
32 re2 = None
41 re2 = None
33
42
34 def compilere(pat, multiline=False):
43 def compilere(pat, multiline=False):
35 if multiline:
44 if multiline:
36 pat = '(?m)' + pat
45 pat = '(?m)' + pat
37 if re2:
46 if re2:
38 try:
47 try:
39 return re2.compile(pat)
48 return re2.compile(pat)
40 except re2.error:
49 except re2.error:
41 pass
50 pass
42 return re.compile(pat)
51 return re.compile(pat)
43
52
53 # check "rules depending on implementation of repquote()" in each
54 # patterns (especially pypats), before changing around repquote()
55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
57 def _repquoteencodechr(i):
58 if i > 255:
59 return 'u'
60 c = chr(i)
61 if c in _repquotefixedmap:
62 return _repquotefixedmap[c]
63 if c.isalpha():
64 return 'x'
65 if c.isdigit():
66 return 'n'
67 return 'o'
68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
69
44 def repquote(m):
70 def repquote(m):
45 fromc = '.:'
46 tochr = 'pq'
47 def encodechr(i):
48 if i > 255:
49 return 'u'
50 c = chr(i)
51 if c in ' \n':
52 return c
53 if c.isalpha():
54 return 'x'
55 if c.isdigit():
56 return 'n'
57 try:
58 return tochr[fromc.find(c)]
59 except (ValueError, IndexError):
60 return 'o'
61 t = m.group('text')
71 t = m.group('text')
62 tt = ''.join(encodechr(i) for i in xrange(256))
72 t = t.translate(_repquotett)
63 t = t.translate(tt)
64 return m.group('quote') + t + m.group('quote')
73 return m.group('quote') + t + m.group('quote')
65
74
66 def reppython(m):
75 def reppython(m):
67 comment = m.group('comment')
76 comment = m.group('comment')
68 if comment:
77 if comment:
69 l = len(comment.rstrip())
78 l = len(comment.rstrip())
70 return "#" * l + comment[l:]
79 return "#" * l + comment[l:]
71 return repquote(m)
80 return repquote(m)
72
81
73 def repcomment(m):
82 def repcomment(m):
74 return m.group(1) + "#" * len(m.group(2))
83 return m.group(1) + "#" * len(m.group(2))
75
84
76 def repccomment(m):
85 def repccomment(m):
77 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
78 return m.group(1) + t + "*/"
87 return m.group(1) + t + "*/"
79
88
80 def repcallspaces(m):
89 def repcallspaces(m):
81 t = re.sub(r"\n\s+", "\n", m.group(2))
90 t = re.sub(r"\n\s+", "\n", m.group(2))
82 return m.group(1) + t
91 return m.group(1) + t
83
92
84 def repinclude(m):
93 def repinclude(m):
85 return m.group(1) + "<foo>"
94 return m.group(1) + "<foo>"
86
95
87 def rephere(m):
96 def rephere(m):
88 t = re.sub(r"\S", "x", m.group(2))
97 t = re.sub(r"\S", "x", m.group(2))
89 return m.group(1) + t
98 return m.group(1) + t
90
99
91
100
92 testpats = [
101 testpats = [
93 [
102 [
94 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
103 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
95 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
96 (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"),
97 (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"),
98 (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"),
99 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
100 (r'echo -n', "don't use 'echo -n', use printf"),
109 (r'echo -n', "don't use 'echo -n', use printf"),
101 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
102 (r'head -c', "don't use 'head -c', use 'dd'"),
111 (r'head -c', "don't use 'head -c', use 'dd'"),
103 (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>'"),
104 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
105 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
114 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
106 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
107 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
108 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
109 (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"),
110 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
119 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
111 "use egrep for extended grep syntax"),
120 "use egrep for extended grep syntax"),
112 (r'/bin/', "don't use explicit paths for tools"),
121 (r'/bin/', "don't use explicit paths for tools"),
113 (r'[^\n]\Z', "no trailing newline"),
122 (r'[^\n]\Z', "no trailing newline"),
114 (r'export .*=', "don't export and assign at once"),
123 (r'export .*=', "don't export and assign at once"),
115 (r'^source\b', "don't use 'source', use '.'"),
124 (r'^source\b', "don't use 'source', use '.'"),
116 (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"),
117 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
126 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
118 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
127 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
119 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
128 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
120 (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'"),
121 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
130 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
122 (r'^alias\b.*=', "don't use alias, use a function"),
131 (r'^alias\b.*=', "don't use alias, use a function"),
123 (r'if\s*!', "don't use '!' to negate exit status"),
132 (r'if\s*!', "don't use '!' to negate exit status"),
124 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
133 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
125 (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"),
126 (r'^( *)\t', "don't use tabs to indent"),
135 (r'^( *)\t', "don't use tabs to indent"),
127 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
136 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
128 "put a backslash-escaped newline after sed 'i' command"),
137 "put a backslash-escaped newline after sed 'i' command"),
129 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
138 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
130 (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"),
131 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
140 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
132 (r'\butil\.Abort\b', "directly use error.Abort"),
141 (r'\butil\.Abort\b', "directly use error.Abort"),
133 (r'\|&', "don't use |&, use 2>&1"),
142 (r'\|&', "don't use |&, use 2>&1"),
134 (r'\w = +\w', "only one space after = allowed"),
143 (r'\w = +\w', "only one space after = allowed"),
135 (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'")
136 ],
146 ],
137 # warnings
147 # warnings
138 [
148 [
139 (r'^function', "don't use 'function', use old style"),
149 (r'^function', "don't use 'function', use old style"),
140 (r'^diff.*-\w*N', "don't use 'diff -N'"),
150 (r'^diff.*-\w*N', "don't use 'diff -N'"),
141 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
151 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
142 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
152 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
143 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
153 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
144 ]
154 ]
145 ]
155 ]
146
156
147 testfilters = [
157 testfilters = [
148 (r"( *)(#([^\n]*\S)?)", repcomment),
158 (r"( *)(#([^\n]*\S)?)", repcomment),
149 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
159 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
150 ]
160 ]
151
161
152 winglobmsg = "use (glob) to match Windows paths too"
162 winglobmsg = "use (glob) to match Windows paths too"
153 uprefix = r"^ \$ "
163 uprefix = r"^ \$ "
154 utestpats = [
164 utestpats = [
155 [
165 [
156 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
166 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
157 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
167 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
158 "use regex test output patterns instead of sed"),
168 "use regex test output patterns instead of sed"),
159 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
169 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
160 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
170 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
161 (uprefix + r'.*\|\| echo.*(fail|error)',
171 (uprefix + r'.*\|\| echo.*(fail|error)',
162 "explicit exit code checks unnecessary"),
172 "explicit exit code checks unnecessary"),
163 (uprefix + r'set -e', "don't use set -e"),
173 (uprefix + r'set -e', "don't use set -e"),
164 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
174 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
165 (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 "
166 "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
167 '# no-msys'), # in test-pull.t which is skipped on windows
177 '# no-msys'), # in test-pull.t which is skipped on windows
168 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
178 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
169 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
179 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
170 winglobmsg),
180 winglobmsg),
171 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
181 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
172 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
182 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
173 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
183 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
174 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
184 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
175 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
185 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
176 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
186 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
177 (r'^ moving \S+/.*[^)]$', winglobmsg),
187 (r'^ moving \S+/.*[^)]$', winglobmsg),
178 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
188 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
179 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
189 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
180 (r'^ .*file://\$TESTTMP',
190 (r'^ .*file://\$TESTTMP',
181 '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.*[^)]$',
193 'use (glob) to match localhost IP on hosts without 127.0.0.1 too'),
182 (r'^ (cat|find): .*: No such file or directory',
194 (r'^ (cat|find): .*: No such file or directory',
183 'use test -f to test for file existence'),
195 'use test -f to test for file existence'),
184 (r'^ diff -[^ -]*p',
196 (r'^ diff -[^ -]*p',
185 "don't use (external) diff with -p for portability"),
197 "don't use (external) diff with -p for portability"),
186 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
198 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
187 "glob timezone field in diff output for portability"),
199 "glob timezone field in diff output for portability"),
188 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
200 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
189 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
201 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
190 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
202 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
191 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
203 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
192 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
204 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
193 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
205 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
194 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
206 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
195 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
207 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
196 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
208 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
197 ],
209 ],
198 # warnings
210 # warnings
199 [
211 [
200 (r'^ [^*?/\n]* \(glob\)$',
212 (r'^ (?!.*127\.0\.0\.1)[^*?/\n]* \(glob\)$',
201 "glob match with no glob character (?*/)"),
213 "glob match with no glob string (?, *, /, and 127.0.0.1)"),
202 ]
214 ]
203 ]
215 ]
204
216
205 for i in [0, 1]:
217 for i in [0, 1]:
206 for tp in testpats[i]:
218 for tp in testpats[i]:
207 p = tp[0]
219 p = tp[0]
208 m = tp[1]
220 m = tp[1]
209 if p.startswith(r'^'):
221 if p.startswith(r'^'):
210 p = r"^ [$>] (%s)" % p[1:]
222 p = r"^ [$>] (%s)" % p[1:]
211 else:
223 else:
212 p = r"^ [$>] .*(%s)" % p
224 p = r"^ [$>] .*(%s)" % p
213 utestpats[i].append((p, m) + tp[2:])
225 utestpats[i].append((p, m) + tp[2:])
214
226
215 utestfilters = [
227 utestfilters = [
216 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
228 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
217 (r"( *)(#([^\n]*\S)?)", repcomment),
229 (r"( +)(#([^\n]*\S)?)", repcomment),
218 ]
230 ]
219
231
220 pypats = [
232 pypats = [
221 [
233 [
222 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
234 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
223 "tuple parameter unpacking not available in Python 3+"),
235 "tuple parameter unpacking not available in Python 3+"),
224 (r'lambda\s*\(.*,.*\)',
236 (r'lambda\s*\(.*,.*\)',
225 "tuple parameter unpacking not available in Python 3+"),
237 "tuple parameter unpacking not available in Python 3+"),
226 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
238 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
227 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
239 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
228 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
240 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
229 'dict-from-generator'),
241 'dict-from-generator'),
230 (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+"),
231 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
243 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
232 (r'^\s*\t', "don't use tabs"),
244 (r'^\s*\t', "don't use tabs"),
233 (r'\S;\s*\n', "semicolon"),
245 (r'\S;\s*\n', "semicolon"),
234 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
246 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
235 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
247 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
236 (r'(\w|\)),\w', "missing whitespace after ,"),
248 (r'(\w|\)),\w', "missing whitespace after ,"),
237 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
249 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
238 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
250 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
239 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
251 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
240 (r'.{81}', "line too long"),
252 (r'.{81}', "line too long"),
241 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
242 (r'[^\n]\Z', "no trailing newline"),
253 (r'[^\n]\Z', "no trailing newline"),
243 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
254 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
244 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
255 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
245 # "don't use underbars in identifiers"),
256 # "don't use underbars in identifiers"),
246 (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* = ',
247 "don't use camelcase in identifiers"),
258 "don't use camelcase in identifiers"),
248 (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]+',
249 "linebreak after :"),
260 "linebreak after :"),
250 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
261 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
251 r'#.*old-style'),
262 r'#.*old-style'),
252 (r'class\s[^( \n]+\(\):',
263 (r'class\s[^( \n]+\(\):',
253 "class foo() creates old style object, use class foo(object)",
264 "class foo() creates old style object, use class foo(object)",
254 r'#.*old-style'),
265 r'#.*old-style'),
255 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
266 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
256 if k not in ('print', 'exec')),
267 if k not in ('print', 'exec')),
257 "Python keyword is not a function"),
268 "Python keyword is not a function"),
258 (r',]', "unneeded trailing ',' in list"),
269 (r',]', "unneeded trailing ',' in list"),
259 # (r'class\s[A-Z][^\(]*\((?!Exception)',
270 # (r'class\s[A-Z][^\(]*\((?!Exception)',
260 # "don't capitalize non-exception classes"),
271 # "don't capitalize non-exception classes"),
261 # (r'in range\(', "use xrange"),
272 # (r'in range\(', "use xrange"),
262 # (r'^\s*print\s+', "avoid using print in core and extensions"),
273 # (r'^\s*print\s+', "avoid using print in core and extensions"),
263 (r'[\x80-\xff]', "non-ASCII character literal"),
274 (r'[\x80-\xff]', "non-ASCII character literal"),
264 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
275 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
265 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
276 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
266 "gratuitous whitespace after Python keyword"),
277 "gratuitous whitespace after Python keyword"),
267 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
278 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
268 # (r'\s\s=', "gratuitous whitespace before ="),
279 # (r'\s\s=', "gratuitous whitespace before ="),
269 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
280 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
270 "missing whitespace around operator"),
281 "missing whitespace around operator"),
271 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
282 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
272 "missing whitespace around operator"),
283 "missing whitespace around operator"),
273 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
284 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
274 "missing whitespace around operator"),
285 "missing whitespace around operator"),
275 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
286 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
276 "wrong whitespace around ="),
287 "wrong whitespace around ="),
277 (r'\([^()]*( =[^=]|[^<>!=]= )',
288 (r'\([^()]*( =[^=]|[^<>!=]= )',
278 "no whitespace around = for named parameters"),
289 "no whitespace around = for named parameters"),
279 (r'raise Exception', "don't raise generic exceptions"),
290 (r'raise Exception', "don't raise generic exceptions"),
280 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
291 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
281 "don't use old-style two-argument raise, use Exception(message)"),
292 "don't use old-style two-argument raise, use Exception(message)"),
282 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
293 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
283 (r' [=!]=\s+(True|False|None)',
294 (r' [=!]=\s+(True|False|None)',
284 "comparison with singleton, use 'is' or 'is not' instead"),
295 "comparison with singleton, use 'is' or 'is not' instead"),
285 (r'^\s*(while|if) [01]:',
296 (r'^\s*(while|if) [01]:',
286 "use True/False for constant Boolean expression"),
297 "use True/False for constant Boolean expression"),
287 (r'(?:(?<!def)\s+|\()hasattr',
298 (r'(?:(?<!def)\s+|\()hasattr',
288 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
299 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
289 (r'opener\([^)]*\).read\(',
300 (r'opener\([^)]*\).read\(',
290 "use opener.read() instead"),
301 "use opener.read() instead"),
291 (r'opener\([^)]*\).write\(',
302 (r'opener\([^)]*\).write\(',
292 "use opener.write() instead"),
303 "use opener.write() instead"),
293 (r'[\s\(](open|file)\([^)]*\)\.read\(',
304 (r'[\s\(](open|file)\([^)]*\)\.read\(',
294 "use util.readfile() instead"),
305 "use util.readfile() instead"),
295 (r'[\s\(](open|file)\([^)]*\)\.write\(',
306 (r'[\s\(](open|file)\([^)]*\)\.write\(',
296 "use util.writefile() instead"),
307 "use util.writefile() instead"),
297 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
308 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
298 "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"),
299 (r'[\s\(](open|file)\([^)]*\)\.',
310 (r'[\s\(](open|file)\([^)]*\)\.',
300 "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"),
301 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
312 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
302 (r'\.debug\(\_', "don't mark debug messages for translation"),
313 (r'\.debug\(\_', "don't mark debug messages for translation"),
303 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
314 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
304 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
315 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
305 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
316 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
306 'legacy exception syntax; use "as" instead of ","'),
317 'legacy exception syntax; use "as" instead of ","'),
307 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
318 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
308 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
309 "missing _() in ui message (use () to hide false-positives)"),
310 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
319 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
311 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
320 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
312 (r'os\.path\.join\(.*, *(""|\'\')\)',
321 (r'os\.path\.join\(.*, *(""|\'\')\)',
313 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
322 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
314 (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"'),
315 # 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
316 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
325 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
317 (r'\butil\.Abort\b', "directly use error.Abort"),
326 (r'\butil\.Abort\b', "directly use error.Abort"),
318 (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"),
319 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
328 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
320 (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"),
331 (r'^import urlparse', "don't use urlparse, use util.urlparse"),
332 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
333 (r'^import cPickle', "don't use cPickle, 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"),
336 (r'^import BaseHTTPServer', "use util.httpserver instead"),
337 (r'\.next\(\)', "don't use .next(), use next(...)"),
338
339 # rules depending on implementation of repquote()
340 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
341 'string join across lines with no space'),
342 (r'''(?x)ui\.(status|progress|write|note|warn)\(
343 [ \t\n#]*
344 (?# any strings/comments might precede a string, which
345 # contains translatable message)
346 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
347 (?# sequence consisting of below might precede translatable message
348 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
349 # - escaped character: "\\", "\n", "\0" ...
350 # - character other than '%', 'b' as '\', and 'x' as alphabet)
351 (['"]|\'\'\'|""")
352 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
353 (?# this regexp can't use [^...] style,
354 # because _preparepats forcibly adds "\n" into [^...],
355 # even though this regexp wants match it against "\n")''',
356 "missing _() in ui message (use () to hide false-positives)"),
321 ],
357 ],
322 # warnings
358 # warnings
323 [
359 [
360 # rules depending on implementation of repquote()
324 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
361 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
325 ]
362 ]
326 ]
363 ]
327
364
328 pyfilters = [
365 pyfilters = [
329 (r"""(?msx)(?P<comment>\#.*?$)|
366 (r"""(?msx)(?P<comment>\#.*?$)|
330 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
367 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
331 (?P<text>(([^\\]|\\.)*?))
368 (?P<text>(([^\\]|\\.)*?))
332 (?P=quote))""", reppython),
369 (?P=quote))""", reppython),
333 ]
370 ]
334
371
335 txtfilters = []
372 txtfilters = []
336
373
337 txtpats = [
374 txtpats = [
338 [
375 [
339 ('\s$', 'trailing whitespace'),
376 ('\s$', 'trailing whitespace'),
340 ('.. note::[ \n][^\n]', 'add two newlines after note::')
377 ('.. note::[ \n][^\n]', 'add two newlines after note::')
341 ],
378 ],
342 []
379 []
343 ]
380 ]
344
381
345 cpats = [
382 cpats = [
346 [
383 [
347 (r'//', "don't use //-style comments"),
384 (r'//', "don't use //-style comments"),
348 (r'^ ', "don't use spaces to indent"),
385 (r'^ ', "don't use spaces to indent"),
349 (r'\S\t', "don't use tabs except for indent"),
386 (r'\S\t', "don't use tabs except for indent"),
350 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
387 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
351 (r'.{81}', "line too long"),
388 (r'.{81}', "line too long"),
352 (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"),
353 (r'return\(', "return is not a function"),
390 (r'return\(', "return is not a function"),
354 (r' ;', "no space before ;"),
391 (r' ;', "no space before ;"),
355 (r'[^;] \)', "no space before )"),
392 (r'[^;] \)', "no space before )"),
356 (r'[)][{]', "space between ) and {"),
393 (r'[)][{]', "space between ) and {"),
357 (r'\w+\* \w+', "use int *foo, not int* foo"),
394 (r'\w+\* \w+', "use int *foo, not int* foo"),
358 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
395 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
359 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
396 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
360 (r'\w,\w', "missing whitespace after ,"),
397 (r'\w,\w', "missing whitespace after ,"),
361 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
398 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
362 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
399 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
363 (r'^#\s+\w', "use #foo, not # foo"),
400 (r'^#\s+\w', "use #foo, not # foo"),
364 (r'[^\n]\Z', "no trailing newline"),
401 (r'[^\n]\Z', "no trailing newline"),
365 (r'^\s*#import\b', "use only #include in standard C code"),
402 (r'^\s*#import\b', "use only #include in standard C code"),
366 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
403 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
367 (r'strcat\(', "don't use strcat"),
404 (r'strcat\(', "don't use strcat"),
405
406 # rules depending on implementation of repquote()
368 ],
407 ],
369 # warnings
408 # warnings
370 []
409 [
410 # rules depending on implementation of repquote()
411 ]
371 ]
412 ]
372
413
373 cfilters = [
414 cfilters = [
374 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
415 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
375 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
416 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
376 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
417 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
377 (r'(\()([^)]+\))', repcallspaces),
418 (r'(\()([^)]+\))', repcallspaces),
378 ]
419 ]
379
420
380 inutilpats = [
421 inutilpats = [
381 [
422 [
382 (r'\bui\.', "don't use ui in util"),
423 (r'\bui\.', "don't use ui in util"),
383 ],
424 ],
384 # warnings
425 # warnings
385 []
426 []
386 ]
427 ]
387
428
388 inrevlogpats = [
429 inrevlogpats = [
389 [
430 [
390 (r'\brepo\.', "don't use repo in revlog"),
431 (r'\brepo\.', "don't use repo in revlog"),
391 ],
432 ],
392 # warnings
433 # warnings
393 []
434 []
394 ]
435 ]
395
436
396 webtemplatefilters = []
437 webtemplatefilters = []
397
438
398 webtemplatepats = [
439 webtemplatepats = [
399 [],
440 [],
400 [
441 [
401 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
442 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
402 'follow desc keyword with either firstline or websub'),
443 'follow desc keyword with either firstline or websub'),
403 ]
444 ]
404 ]
445 ]
405
446
406 checks = [
447 checks = [
407 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
448 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
408 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
449 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
409 ('c', r'.*\.[ch]$', '', cfilters, cpats),
450 ('c', r'.*\.[ch]$', '', cfilters, cpats),
410 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
451 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
411 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
452 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
412 pyfilters, inrevlogpats),
453 pyfilters, inrevlogpats),
413 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
454 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
414 inutilpats),
455 inutilpats),
415 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
456 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
416 ('web template', r'mercurial/templates/.*\.tmpl', '',
457 ('web template', r'mercurial/templates/.*\.tmpl', '',
417 webtemplatefilters, webtemplatepats),
458 webtemplatefilters, webtemplatepats),
418 ]
459 ]
419
460
420 def _preparepats():
461 def _preparepats():
421 for c in checks:
462 for c in checks:
422 failandwarn = c[-1]
463 failandwarn = c[-1]
423 for pats in failandwarn:
464 for pats in failandwarn:
424 for i, pseq in enumerate(pats):
465 for i, pseq in enumerate(pats):
425 # fix-up regexes for multi-line searches
466 # fix-up regexes for multi-line searches
426 p = pseq[0]
467 p = pseq[0]
427 # \s doesn't match \n
468 # \s doesn't match \n
428 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
469 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
429 # [^...] doesn't match newline
470 # [^...] doesn't match newline
430 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
471 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
431
472
432 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
473 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
433 filters = c[3]
474 filters = c[3]
434 for i, flt in enumerate(filters):
475 for i, flt in enumerate(filters):
435 filters[i] = re.compile(flt[0]), flt[1]
476 filters[i] = re.compile(flt[0]), flt[1]
436 _preparepats()
437
477
438 class norepeatlogger(object):
478 class norepeatlogger(object):
439 def __init__(self):
479 def __init__(self):
440 self._lastseen = None
480 self._lastseen = None
441
481
442 def log(self, fname, lineno, line, msg, blame):
482 def log(self, fname, lineno, line, msg, blame):
443 """print error related a to given line of a given file.
483 """print error related a to given line of a given file.
444
484
445 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
446 of multiple errors.
486 of multiple errors.
447
487
448 :fname: filename
488 :fname: filename
449 :lineno: line number
489 :lineno: line number
450 :line: actual content of the line
490 :line: actual content of the line
451 :msg: error message
491 :msg: error message
452 """
492 """
453 msgid = fname, lineno, line
493 msgid = fname, lineno, line
454 if msgid != self._lastseen:
494 if msgid != self._lastseen:
455 if blame:
495 if blame:
456 print("%s:%d (%s):" % (fname, lineno, blame))
496 print("%s:%d (%s):" % (fname, lineno, blame))
457 else:
497 else:
458 print("%s:%d:" % (fname, lineno))
498 print("%s:%d:" % (fname, lineno))
459 print(" > %s" % line)
499 print(" > %s" % line)
460 self._lastseen = msgid
500 self._lastseen = msgid
461 print(" " + msg)
501 print(" " + msg)
462
502
463 _defaultlogger = norepeatlogger()
503 _defaultlogger = norepeatlogger()
464
504
465 def getblame(f):
505 def getblame(f):
466 lines = []
506 lines = []
467 for l in os.popen('hg annotate -un %s' % f):
507 for l in os.popen('hg annotate -un %s' % f):
468 start, line = l.split(':', 1)
508 start, line = l.split(':', 1)
469 user, rev = start.split()
509 user, rev = start.split()
470 lines.append((line[1:-1], user, rev))
510 lines.append((line[1:-1], user, rev))
471 return lines
511 return lines
472
512
473 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
513 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
474 blame=False, debug=False, lineno=True):
514 blame=False, debug=False, lineno=True):
475 """checks style and portability of a given file
515 """checks style and portability of a given file
476
516
477 :f: filepath
517 :f: filepath
478 :logfunc: function used to report error
518 :logfunc: function used to report error
479 logfunc(filename, linenumber, linecontent, errormessage)
519 logfunc(filename, linenumber, linecontent, errormessage)
480 :maxerr: number of error to display before aborting.
520 :maxerr: number of error to display before aborting.
481 Set to false (default) to report all errors
521 Set to false (default) to report all errors
482
522
483 return True if no error is found, False otherwise.
523 return True if no error is found, False otherwise.
484 """
524 """
485 blamecache = None
525 blamecache = None
486 result = True
526 result = True
487
527
488 try:
528 try:
489 fp = open(f)
529 with opentext(f) as fp:
530 try:
531 pre = post = fp.read()
532 except UnicodeDecodeError as e:
533 print("%s while reading %s" % (e, f))
534 return result
490 except IOError as e:
535 except IOError as e:
491 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
536 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
492 return result
537 return result
493 pre = post = fp.read()
494 fp.close()
495
538
496 for name, match, magic, filters, pats in checks:
539 for name, match, magic, filters, pats in checks:
497 if debug:
540 if debug:
498 print(name, f)
541 print(name, f)
499 fc = 0
542 fc = 0
500 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))):
501 if debug:
544 if debug:
502 print("Skipping %s for %s it doesn't match %s" % (
545 print("Skipping %s for %s it doesn't match %s" % (
503 name, match, f))
546 name, match, f))
504 continue
547 continue
505 if "no-" "check-code" in pre:
548 if "no-" "check-code" in pre:
506 # 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:
507 # no- check- code
550 # no- check- code
508 # but the reason to output skipping is to make life for
551 # but the reason to output skipping is to make life for
509 # tests easier. So, instead of writing it with a normal
552 # tests easier. So, instead of writing it with a normal
510 # spelling, we write it with the expected spelling from
553 # spelling, we write it with the expected spelling from
511 # tests/test-check-code.t
554 # tests/test-check-code.t
512 print("Skipping %s it has no-che?k-code (glob)" % f)
555 print("Skipping %s it has no-che?k-code (glob)" % f)
513 return "Skip" # skip checking this file
556 return "Skip" # skip checking this file
514 for p, r in filters:
557 for p, r in filters:
515 post = re.sub(p, r, post)
558 post = re.sub(p, r, post)
516 nerrs = len(pats[0]) # nerr elements are errors
559 nerrs = len(pats[0]) # nerr elements are errors
517 if warnings:
560 if warnings:
518 pats = pats[0] + pats[1]
561 pats = pats[0] + pats[1]
519 else:
562 else:
520 pats = pats[0]
563 pats = pats[0]
521 # print post # uncomment to show filtered version
564 # print post # uncomment to show filtered version
522
565
523 if debug:
566 if debug:
524 print("Checking %s for %s" % (name, f))
567 print("Checking %s for %s" % (name, f))
525
568
526 prelines = None
569 prelines = None
527 errors = []
570 errors = []
528 for i, pat in enumerate(pats):
571 for i, pat in enumerate(pats):
529 if len(pat) == 3:
572 if len(pat) == 3:
530 p, msg, ignore = pat
573 p, msg, ignore = pat
531 else:
574 else:
532 p, msg = pat
575 p, msg = pat
533 ignore = None
576 ignore = None
534 if i >= nerrs:
577 if i >= nerrs:
535 msg = "warning: " + msg
578 msg = "warning: " + msg
536
579
537 pos = 0
580 pos = 0
538 n = 0
581 n = 0
539 for m in p.finditer(post):
582 for m in p.finditer(post):
540 if prelines is None:
583 if prelines is None:
541 prelines = pre.splitlines()
584 prelines = pre.splitlines()
542 postlines = post.splitlines(True)
585 postlines = post.splitlines(True)
543
586
544 start = m.start()
587 start = m.start()
545 while n < len(postlines):
588 while n < len(postlines):
546 step = len(postlines[n])
589 step = len(postlines[n])
547 if pos + step > start:
590 if pos + step > start:
548 break
591 break
549 pos += step
592 pos += step
550 n += 1
593 n += 1
551 l = prelines[n]
594 l = prelines[n]
552
595
553 if ignore and re.search(ignore, l, re.MULTILINE):
596 if ignore and re.search(ignore, l, re.MULTILINE):
554 if debug:
597 if debug:
555 print("Skipping %s for %s:%s (ignore pattern)" % (
598 print("Skipping %s for %s:%s (ignore pattern)" % (
556 name, f, n))
599 name, f, n))
557 continue
600 continue
558 bd = ""
601 bd = ""
559 if blame:
602 if blame:
560 bd = 'working directory'
603 bd = 'working directory'
561 if not blamecache:
604 if not blamecache:
562 blamecache = getblame(f)
605 blamecache = getblame(f)
563 if n < len(blamecache):
606 if n < len(blamecache):
564 bl, bu, br = blamecache[n]
607 bl, bu, br = blamecache[n]
565 if bl == l:
608 if bl == l:
566 bd = '%s@%s' % (bu, br)
609 bd = '%s@%s' % (bu, br)
567
610
568 errors.append((f, lineno and n + 1, l, msg, bd))
611 errors.append((f, lineno and n + 1, l, msg, bd))
569 result = False
612 result = False
570
613
571 errors.sort()
614 errors.sort()
572 for e in errors:
615 for e in errors:
573 logfunc(*e)
616 logfunc(*e)
574 fc += 1
617 fc += 1
575 if maxerr and fc >= maxerr:
618 if maxerr and fc >= maxerr:
576 print(" (too many errors, giving up)")
619 print(" (too many errors, giving up)")
577 break
620 break
578
621
579 return result
622 return result
580
623
581 if __name__ == "__main__":
624 def main():
582 parser = optparse.OptionParser("%prog [options] [files]")
625 parser = optparse.OptionParser("%prog [options] [files]")
583 parser.add_option("-w", "--warnings", action="store_true",
626 parser.add_option("-w", "--warnings", action="store_true",
584 help="include warning-level checks")
627 help="include warning-level checks")
585 parser.add_option("-p", "--per-file", type="int",
628 parser.add_option("-p", "--per-file", type="int",
586 help="max warnings per file")
629 help="max warnings per file")
587 parser.add_option("-b", "--blame", action="store_true",
630 parser.add_option("-b", "--blame", action="store_true",
588 help="use annotate to generate blame info")
631 help="use annotate to generate blame info")
589 parser.add_option("", "--debug", action="store_true",
632 parser.add_option("", "--debug", action="store_true",
590 help="show debug information")
633 help="show debug information")
591 parser.add_option("", "--nolineno", action="store_false",
634 parser.add_option("", "--nolineno", action="store_false",
592 dest='lineno', help="don't show line numbers")
635 dest='lineno', help="don't show line numbers")
593
636
594 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,
595 lineno=True)
638 lineno=True)
596 (options, args) = parser.parse_args()
639 (options, args) = parser.parse_args()
597
640
598 if len(args) == 0:
641 if len(args) == 0:
599 check = glob.glob("*")
642 check = glob.glob("*")
600 else:
643 else:
601 check = args
644 check = args
602
645
646 _preparepats()
647
603 ret = 0
648 ret = 0
604 for f in check:
649 for f in check:
605 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
650 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
606 blame=options.blame, debug=options.debug,
651 blame=options.blame, debug=options.debug,
607 lineno=options.lineno):
652 lineno=options.lineno):
608 ret = 1
653 ret = 1
609 sys.exit(ret)
654 return ret
655
656 if __name__ == "__main__":
657 sys.exit(main())
@@ -1,98 +1,102
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 import re, sys, os
18 from __future__ import absolute_import, print_function
19
20 import os
21 import re
22 import sys
19
23
20 commitheader = r"^(?:# [^\n]*\n)*"
24 commitheader = r"^(?:# [^\n]*\n)*"
21 afterheader = commitheader + r"(?!#)"
25 afterheader = commitheader + r"(?!#)"
22 beforepatch = afterheader + r"(?!\n(?!@@))"
26 beforepatch = afterheader + r"(?!\n(?!@@))"
23
27
24 errors = [
28 errors = [
25 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
29 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
26 (beforepatch + r".*[(]issue \d\d\d",
30 (beforepatch + r".*[(]issue \d\d\d",
27 "no space allowed between issue and number"),
31 "no space allowed between issue and number"),
28 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
32 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
29 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
33 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
30 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
34 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
31 "summary line doesn't start with 'topic: '"),
35 "summary line doesn't start with 'topic: '"),
32 (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"),
33 (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"),
34 (afterheader + r"\S*[^A-Za-z0-9-]\S*: ",
38 (afterheader + r"\S*[^A-Za-z0-9-]\S*: ",
35 "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"),
36 (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"),
37 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
41 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
38 (r"\n\+\n( |\+)\n", "adds double empty line"),
42 (r"\n\+\n( |\+)\n", "adds double empty line"),
39 (r"\n \n\+\n", "adds double empty line"),
43 (r"\n \n\+\n", "adds double empty line"),
40 (r"\n\+[ \t]+def [a-z]+_[a-z]", "adds a function with foo_bar naming"),
44 (r"\n\+[ \t]+def [a-z]+_[a-z]", "adds a function with foo_bar naming"),
41 ]
45 ]
42
46
43 word = re.compile('\S')
47 word = re.compile('\S')
44 def nonempty(first, second):
48 def nonempty(first, second):
45 if word.search(first):
49 if word.search(first):
46 return first
50 return first
47 return second
51 return second
48
52
49 def checkcommit(commit, node=None):
53 def checkcommit(commit, node=None):
50 exitcode = 0
54 exitcode = 0
51 printed = node is None
55 printed = node is None
52 hits = []
56 hits = []
53 for exp, msg in errors:
57 for exp, msg in errors:
54 for m in re.finditer(exp, commit):
58 for m in re.finditer(exp, commit):
55 end = m.end()
59 end = m.end()
56 trailing = re.search(r'(\\n)+$', exp)
60 trailing = re.search(r'(\\n)+$', exp)
57 if trailing:
61 if trailing:
58 end -= len(trailing.group()) / 2
62 end -= len(trailing.group()) / 2
59 hits.append((end, exp, msg))
63 hits.append((end, exp, msg))
60 if hits:
64 if hits:
61 hits.sort()
65 hits.sort()
62 pos = 0
66 pos = 0
63 last = ''
67 last = ''
64 for n, l in enumerate(commit.splitlines(True)):
68 for n, l in enumerate(commit.splitlines(True)):
65 pos += len(l)
69 pos += len(l)
66 while len(hits):
70 while len(hits):
67 end, exp, msg = hits[0]
71 end, exp, msg = hits[0]
68 if pos < end:
72 if pos < end:
69 break
73 break
70 if not printed:
74 if not printed:
71 printed = True
75 printed = True
72 print "node: %s" % node
76 print("node: %s" % node)
73 print "%d: %s" % (n, msg)
77 print("%d: %s" % (n, msg))
74 print " %s" % nonempty(l, last)[:-1]
78 print(" %s" % nonempty(l, last)[:-1])
75 if "BYPASS" not in os.environ:
79 if "BYPASS" not in os.environ:
76 exitcode = 1
80 exitcode = 1
77 del hits[0]
81 del hits[0]
78 last = nonempty(l, last)
82 last = nonempty(l, last)
79
83
80 return exitcode
84 return exitcode
81
85
82 def readcommit(node):
86 def readcommit(node):
83 return os.popen("hg export %s" % node).read()
87 return os.popen("hg export %s" % node).read()
84
88
85 if __name__ == "__main__":
89 if __name__ == "__main__":
86 exitcode = 0
90 exitcode = 0
87 node = os.environ.get("HG_NODE")
91 node = os.environ.get("HG_NODE")
88
92
89 if node:
93 if node:
90 commit = readcommit(node)
94 commit = readcommit(node)
91 exitcode = checkcommit(commit)
95 exitcode = checkcommit(commit)
92 elif sys.argv[1:]:
96 elif sys.argv[1:]:
93 for node in sys.argv[1:]:
97 for node in sys.argv[1:]:
94 exitcode |= checkcommit(readcommit(node), node)
98 exitcode |= checkcommit(readcommit(node), node)
95 else:
99 else:
96 commit = sys.stdin.read()
100 commit = sys.stdin.read()
97 exitcode = checkcommit(commit)
101 exitcode = checkcommit(commit)
98 sys.exit(exitcode)
102 sys.exit(exitcode)
@@ -1,83 +1,96
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
13 import imp
14 import os
14 import os
15 import sys
15 import sys
16 import traceback
16 import traceback
17
17
18 def check_compat_py2(f):
18 def check_compat_py2(f):
19 """Check Python 3 compatibility for a file with Python 2"""
19 """Check Python 3 compatibility for a file with Python 2"""
20 with open(f, 'rb') as fh:
20 with open(f, 'rb') as fh:
21 content = fh.read()
21 content = fh.read()
22 root = ast.parse(content)
22 root = ast.parse(content)
23
23
24 # Ignore empty files.
24 # Ignore empty files.
25 if not root.body:
25 if not root.body:
26 return
26 return
27
27
28 futures = set()
28 futures = set()
29 haveprint = False
29 haveprint = False
30 for node in ast.walk(root):
30 for node in ast.walk(root):
31 if isinstance(node, ast.ImportFrom):
31 if isinstance(node, ast.ImportFrom):
32 if node.module == '__future__':
32 if node.module == '__future__':
33 futures |= set(n.name for n in node.names)
33 futures |= set(n.name for n in node.names)
34 elif isinstance(node, ast.Print):
34 elif isinstance(node, ast.Print):
35 haveprint = True
35 haveprint = True
36
36
37 if 'absolute_import' not in futures:
37 if 'absolute_import' not in futures:
38 print('%s not using absolute_import' % f)
38 print('%s not using absolute_import' % f)
39 if haveprint and 'print_function' not in futures:
39 if haveprint and 'print_function' not in futures:
40 print('%s requires print_function' % f)
40 print('%s requires print_function' % f)
41
41
42 def check_compat_py3(f):
42 def check_compat_py3(f):
43 """Check Python 3 compatibility of a file with Python 3."""
43 """Check Python 3 compatibility of a file with Python 3."""
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]
59 with open(f, 'r') as fh:
59 with open(f, 'r') as fh:
60 try:
60 try:
61 imp.load_module(name, fh, '', ('py', 'r', imp.PY_SOURCE))
61 imp.load_module(name, fh, '', ('py', 'r', imp.PY_SOURCE))
62 except Exception as e:
62 except Exception as e:
63 exc_type, exc_value, tb = sys.exc_info()
63 exc_type, exc_value, tb = sys.exc_info()
64 frame = traceback.extract_tb(tb)[-1]
64 # We walk the stack and ignore frames from our custom importer,
65 # import mechanisms, and stdlib modules. This kinda/sorta
66 # emulates CPython behavior in import.c while also attempting
67 # to pin blame on a Mercurial file.
68 for frame in reversed(traceback.extract_tb(tb)):
69 if frame.name == '_call_with_frames_removed':
70 continue
71 if 'importlib' in frame.filename:
72 continue
73 if 'mercurial/__init__.py' in frame.filename:
74 continue
75 if frame.filename.startswith(sys.prefix):
76 continue
77 break
65
78
66 if frame.filename:
79 if frame.filename:
67 filename = os.path.basename(frame.filename)
80 filename = os.path.basename(frame.filename)
68 print('%s: error importing: <%s> %s (error at %s:%d)' % (
81 print('%s: error importing: <%s> %s (error at %s:%d)' % (
69 f, type(e).__name__, e, filename, frame.lineno))
82 f, type(e).__name__, e, filename, frame.lineno))
70 else:
83 else:
71 print('%s: error importing module: <%s> %s (line %d)' % (
84 print('%s: error importing module: <%s> %s (line %d)' % (
72 f, type(e).__name__, e, frame.lineno))
85 f, type(e).__name__, e, frame.lineno))
73
86
74 if __name__ == '__main__':
87 if __name__ == '__main__':
75 if sys.version_info[0] == 2:
88 if sys.version_info[0] == 2:
76 fn = check_compat_py2
89 fn = check_compat_py2
77 else:
90 else:
78 fn = check_compat_py3
91 fn = check_compat_py3
79
92
80 for f in sys.argv[1:]:
93 for f in sys.argv[1:]:
81 fn(f)
94 fn(f)
82
95
83 sys.exit(0)
96 sys.exit(0)
@@ -1,30 +1,32
1 cHg
1 cHg
2 ===
2 ===
3
3
4 A fast client for Mercurial command server running on Unix.
4 A fast client for Mercurial command server running on Unix.
5
5
6 Install:
6 Install:
7
7
8 $ make
8 $ make
9 $ make install
9 $ make install
10
10
11 Usage:
11 Usage:
12
12
13 $ chg help # show help of Mercurial
13 $ chg help # show help of Mercurial
14 $ alias hg=chg # replace hg command
14 $ alias hg=chg # replace hg command
15 $ chg --kill-chg-daemon # terminate background server
15 $ chg --kill-chg-daemon # terminate background server
16
16
17 Environment variables:
17 Environment variables:
18
18
19 Although cHg tries to update environment variables, some of them cannot be
19 Although cHg tries to update environment variables, some of them cannot be
20 changed after spawning the server. The following variables are specially
20 changed after spawning the server. The following variables are specially
21 handled:
21 handled:
22
22
23 * configuration files are reloaded automatically by default.
23 * configuration files are reloaded automatically by default.
24 * CHGHG or HG specifies the path to the hg executable spawned as the
24 * CHGHG or HG specifies the path to the hg executable spawned as the
25 background command server.
25 background command server.
26
26
27 The following variables are available for testing:
27 The following variables are available for testing:
28
28
29 * CHGDEBUG enables debug messages.
29 * CHGDEBUG enables debug messages.
30 * CHGSOCKNAME specifies the socket path of the background cmdserver.
30 * CHGSOCKNAME specifies the socket path of the background cmdserver.
31 * CHGTIMEOUT specifies how many seconds chg will wait before giving up
32 connecting to a cmdserver. If it is 0, chg will wait forever. Default: 60
@@ -1,593 +1,669
1 /*
1 /*
2 * A fast client for Mercurial command server
2 * A fast client for Mercurial command server
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 <assert.h>
10 #include <assert.h>
11 #include <errno.h>
11 #include <errno.h>
12 #include <fcntl.h>
12 #include <fcntl.h>
13 #include <signal.h>
13 #include <signal.h>
14 #include <stdio.h>
14 #include <stdio.h>
15 #include <stdlib.h>
15 #include <stdlib.h>
16 #include <string.h>
16 #include <string.h>
17 #include <sys/file.h>
17 #include <sys/file.h>
18 #include <sys/stat.h>
18 #include <sys/stat.h>
19 #include <sys/types.h>
19 #include <sys/types.h>
20 #include <sys/un.h>
20 #include <sys/un.h>
21 #include <sys/wait.h>
21 #include <sys/wait.h>
22 #include <time.h>
22 #include <time.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 #ifndef UNIX_PATH_MAX
28 #ifndef UNIX_PATH_MAX
29 #define UNIX_PATH_MAX (sizeof(((struct sockaddr_un *)NULL)->sun_path))
29 #define UNIX_PATH_MAX (sizeof(((struct sockaddr_un *)NULL)->sun_path))
30 #endif
30 #endif
31
31
32 struct cmdserveropts {
32 struct cmdserveropts {
33 char sockname[UNIX_PATH_MAX];
33 char sockname[UNIX_PATH_MAX];
34 char redirectsockname[UNIX_PATH_MAX];
34 char redirectsockname[UNIX_PATH_MAX];
35 char lockfile[UNIX_PATH_MAX];
35 char lockfile[UNIX_PATH_MAX];
36 size_t argsize;
36 size_t argsize;
37 const char **args;
37 const char **args;
38 int lockfd;
38 int lockfd;
39 int sockdirfd;
39 int sockdirfd;
40 };
40 };
41
41
42 static void initcmdserveropts(struct cmdserveropts *opts) {
42 static void initcmdserveropts(struct cmdserveropts *opts) {
43 memset(opts, 0, sizeof(struct cmdserveropts));
43 memset(opts, 0, sizeof(struct cmdserveropts));
44 opts->lockfd = -1;
44 opts->lockfd = -1;
45 opts->sockdirfd = -1;
45 opts->sockdirfd = -1;
46 }
46 }
47
47
48 static void freecmdserveropts(struct cmdserveropts *opts) {
48 static void freecmdserveropts(struct cmdserveropts *opts) {
49 free(opts->args);
49 free(opts->args);
50 opts->args = NULL;
50 opts->args = NULL;
51 opts->argsize = 0;
51 opts->argsize = 0;
52 assert(opts->lockfd == -1 && "should be closed by unlockcmdserver()");
52 assert(opts->lockfd == -1 && "should be closed by unlockcmdserver()");
53 if (opts->sockdirfd >= 0) {
53 if (opts->sockdirfd >= 0) {
54 close(opts->sockdirfd);
54 close(opts->sockdirfd);
55 opts->sockdirfd = -1;
55 opts->sockdirfd = -1;
56 }
56 }
57 }
57 }
58
58
59 /*
59 /*
60 * Test if an argument is a sensitive flag that should be passed to the server.
60 * Test if an argument is a sensitive flag that should be passed to the server.
61 * Return 0 if not, otherwise the number of arguments starting from the current
61 * Return 0 if not, otherwise the number of arguments starting from the current
62 * one that should be passed to the server.
62 * one that should be passed to the server.
63 */
63 */
64 static size_t testsensitiveflag(const char *arg)
64 static size_t testsensitiveflag(const char *arg)
65 {
65 {
66 static const struct {
66 static const struct {
67 const char *name;
67 const char *name;
68 size_t narg;
68 size_t narg;
69 } flags[] = {
69 } flags[] = {
70 {"--config", 1},
70 {"--config", 1},
71 {"--cwd", 1},
71 {"--cwd", 1},
72 {"--repo", 1},
72 {"--repo", 1},
73 {"--repository", 1},
73 {"--repository", 1},
74 {"--traceback", 0},
74 {"--traceback", 0},
75 {"-R", 1},
75 {"-R", 1},
76 };
76 };
77 size_t i;
77 size_t i;
78 for (i = 0; i < sizeof(flags) / sizeof(flags[0]); ++i) {
78 for (i = 0; i < sizeof(flags) / sizeof(flags[0]); ++i) {
79 size_t len = strlen(flags[i].name);
79 size_t len = strlen(flags[i].name);
80 size_t narg = flags[i].narg;
80 size_t narg = flags[i].narg;
81 if (memcmp(arg, flags[i].name, len) == 0) {
81 if (memcmp(arg, flags[i].name, len) == 0) {
82 if (arg[len] == '\0') {
82 if (arg[len] == '\0') {
83 /* --flag (value) */
83 /* --flag (value) */
84 return narg + 1;
84 return narg + 1;
85 } else if (arg[len] == '=' && narg > 0) {
85 } else if (arg[len] == '=' && narg > 0) {
86 /* --flag=value */
86 /* --flag=value */
87 return 1;
87 return 1;
88 } else if (flags[i].name[1] != '-') {
88 } else if (flags[i].name[1] != '-') {
89 /* short flag */
89 /* short flag */
90 return 1;
90 return 1;
91 }
91 }
92 }
92 }
93 }
93 }
94 return 0;
94 return 0;
95 }
95 }
96
96
97 /*
97 /*
98 * Parse argv[] and put sensitive flags to opts->args
98 * Parse argv[] and put sensitive flags to opts->args
99 */
99 */
100 static void setcmdserverargs(struct cmdserveropts *opts,
100 static void setcmdserverargs(struct cmdserveropts *opts,
101 int argc, const char *argv[])
101 int argc, const char *argv[])
102 {
102 {
103 size_t i, step;
103 size_t i, step;
104 opts->argsize = 0;
104 opts->argsize = 0;
105 for (i = 0, step = 1; i < (size_t)argc; i += step, step = 1) {
105 for (i = 0, step = 1; i < (size_t)argc; i += step, step = 1) {
106 if (!argv[i])
106 if (!argv[i])
107 continue; /* pass clang-analyse */
107 continue; /* pass clang-analyse */
108 if (strcmp(argv[i], "--") == 0)
108 if (strcmp(argv[i], "--") == 0)
109 break;
109 break;
110 size_t n = testsensitiveflag(argv[i]);
110 size_t n = testsensitiveflag(argv[i]);
111 if (n == 0 || i + n > (size_t)argc)
111 if (n == 0 || i + n > (size_t)argc)
112 continue;
112 continue;
113 opts->args = reallocx(opts->args,
113 opts->args = reallocx(opts->args,
114 (n + opts->argsize) * sizeof(char *));
114 (n + opts->argsize) * sizeof(char *));
115 memcpy(opts->args + opts->argsize, argv + i,
115 memcpy(opts->args + opts->argsize, argv + i,
116 sizeof(char *) * n);
116 sizeof(char *) * n);
117 opts->argsize += n;
117 opts->argsize += n;
118 step = n;
118 step = n;
119 }
119 }
120 }
120 }
121
121
122 static void preparesockdir(const char *sockdir)
122 static void preparesockdir(const char *sockdir)
123 {
123 {
124 int r;
124 int r;
125 r = mkdir(sockdir, 0700);
125 r = mkdir(sockdir, 0700);
126 if (r < 0 && errno != EEXIST)
126 if (r < 0 && errno != EEXIST)
127 abortmsgerrno("cannot create sockdir %s", sockdir);
127 abortmsgerrno("cannot create sockdir %s", sockdir);
128
128
129 struct stat st;
129 struct stat st;
130 r = lstat(sockdir, &st);
130 r = lstat(sockdir, &st);
131 if (r < 0)
131 if (r < 0)
132 abortmsgerrno("cannot stat %s", sockdir);
132 abortmsgerrno("cannot stat %s", sockdir);
133 if (!S_ISDIR(st.st_mode))
133 if (!S_ISDIR(st.st_mode))
134 abortmsg("cannot create sockdir %s (file exists)", sockdir);
134 abortmsg("cannot create sockdir %s (file exists)", sockdir);
135 if (st.st_uid != geteuid() || st.st_mode & 0077)
135 if (st.st_uid != geteuid() || st.st_mode & 0077)
136 abortmsg("insecure sockdir %s", sockdir);
136 abortmsg("insecure sockdir %s", sockdir);
137 }
137 }
138
138
139 static void setcmdserveropts(struct cmdserveropts *opts)
139 static void setcmdserveropts(struct cmdserveropts *opts)
140 {
140 {
141 int r;
141 int r;
142 char sockdir[UNIX_PATH_MAX];
142 char sockdir[UNIX_PATH_MAX];
143 const char *envsockname = getenv("CHGSOCKNAME");
143 const char *envsockname = getenv("CHGSOCKNAME");
144 if (!envsockname) {
144 if (!envsockname) {
145 /* by default, put socket file in secure directory
145 /* by default, put socket file in secure directory
146 * (permission of socket file may be ignored on some Unices) */
146 * (permission of socket file may be ignored on some Unices) */
147 const char *tmpdir = getenv("TMPDIR");
147 const char *tmpdir = getenv("TMPDIR");
148 if (!tmpdir)
148 if (!tmpdir)
149 tmpdir = "/tmp";
149 tmpdir = "/tmp";
150 r = snprintf(sockdir, sizeof(sockdir), "%s/chg%d",
150 r = snprintf(sockdir, sizeof(sockdir), "%s/chg%d",
151 tmpdir, geteuid());
151 tmpdir, geteuid());
152 if (r < 0 || (size_t)r >= sizeof(sockdir))
152 if (r < 0 || (size_t)r >= sizeof(sockdir))
153 abortmsg("too long TMPDIR (r = %d)", r);
153 abortmsg("too long TMPDIR (r = %d)", r);
154 preparesockdir(sockdir);
154 preparesockdir(sockdir);
155 }
155 }
156
156
157 const char *basename = (envsockname) ? envsockname : sockdir;
157 const char *basename = (envsockname) ? envsockname : sockdir;
158 const char *sockfmt = (envsockname) ? "%s" : "%s/server";
158 const char *sockfmt = (envsockname) ? "%s" : "%s/server";
159 const char *lockfmt = (envsockname) ? "%s.lock" : "%s/lock";
159 const char *lockfmt = (envsockname) ? "%s.lock" : "%s/lock";
160 r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename);
160 r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename);
161 if (r < 0 || (size_t)r >= sizeof(opts->sockname))
161 if (r < 0 || (size_t)r >= sizeof(opts->sockname))
162 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
162 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
163 r = snprintf(opts->lockfile, sizeof(opts->lockfile), lockfmt, basename);
163 r = snprintf(opts->lockfile, sizeof(opts->lockfile), lockfmt, basename);
164 if (r < 0 || (size_t)r >= sizeof(opts->lockfile))
164 if (r < 0 || (size_t)r >= sizeof(opts->lockfile))
165 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
165 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
166 }
166 }
167
167
168 /*
168 /*
169 * Acquire a file lock that indicates a client is trying to start and connect
169 * Acquire a file lock that indicates a client is trying to start and connect
170 * to a server, before executing a command. The lock is released upon exit or
170 * to a server, before executing a command. The lock is released upon exit or
171 * explicit unlock. Will block if the lock is held by another process.
171 * explicit unlock. Will block if the lock is held by another process.
172 */
172 */
173 static void lockcmdserver(struct cmdserveropts *opts)
173 static void lockcmdserver(struct cmdserveropts *opts)
174 {
174 {
175 if (opts->lockfd == -1) {
175 if (opts->lockfd == -1) {
176 opts->lockfd = open(opts->lockfile,
176 opts->lockfd = open(opts->lockfile,
177 O_RDWR | O_CREAT | O_NOFOLLOW, 0600);
177 O_RDWR | O_CREAT | O_NOFOLLOW, 0600);
178 if (opts->lockfd == -1)
178 if (opts->lockfd == -1)
179 abortmsgerrno("cannot create lock file %s",
179 abortmsgerrno("cannot create lock file %s",
180 opts->lockfile);
180 opts->lockfile);
181 fsetcloexec(opts->lockfd);
181 fsetcloexec(opts->lockfd);
182 }
182 }
183 int r = flock(opts->lockfd, LOCK_EX);
183 int r = flock(opts->lockfd, LOCK_EX);
184 if (r == -1)
184 if (r == -1)
185 abortmsgerrno("cannot acquire lock");
185 abortmsgerrno("cannot acquire lock");
186 }
186 }
187
187
188 /*
188 /*
189 * Release the file lock held by calling lockcmdserver. Will do nothing if
189 * Release the file lock held by calling lockcmdserver. Will do nothing if
190 * lockcmdserver is not called.
190 * lockcmdserver is not called.
191 */
191 */
192 static void unlockcmdserver(struct cmdserveropts *opts)
192 static void unlockcmdserver(struct cmdserveropts *opts)
193 {
193 {
194 if (opts->lockfd == -1)
194 if (opts->lockfd == -1)
195 return;
195 return;
196 flock(opts->lockfd, LOCK_UN);
196 flock(opts->lockfd, LOCK_UN);
197 close(opts->lockfd);
197 close(opts->lockfd);
198 opts->lockfd = -1;
198 opts->lockfd = -1;
199 }
199 }
200
200
201 static const char *gethgcmd(void)
201 static const char *gethgcmd(void)
202 {
202 {
203 static const char *hgcmd = NULL;
203 static const char *hgcmd = NULL;
204 if (!hgcmd) {
204 if (!hgcmd) {
205 hgcmd = getenv("CHGHG");
205 hgcmd = getenv("CHGHG");
206 if (!hgcmd || hgcmd[0] == '\0')
206 if (!hgcmd || hgcmd[0] == '\0')
207 hgcmd = getenv("HG");
207 hgcmd = getenv("HG");
208 if (!hgcmd || hgcmd[0] == '\0')
208 if (!hgcmd || hgcmd[0] == '\0')
209 #ifdef HGPATH
209 #ifdef HGPATH
210 hgcmd = (HGPATH);
210 hgcmd = (HGPATH);
211 #else
211 #else
212 hgcmd = "hg";
212 hgcmd = "hg";
213 #endif
213 #endif
214 }
214 }
215 return hgcmd;
215 return hgcmd;
216 }
216 }
217
217
218 static void execcmdserver(const struct cmdserveropts *opts)
218 static void execcmdserver(const struct cmdserveropts *opts)
219 {
219 {
220 const char *hgcmd = gethgcmd();
220 const char *hgcmd = gethgcmd();
221
221
222 const char *baseargv[] = {
222 const char *baseargv[] = {
223 hgcmd,
223 hgcmd,
224 "serve",
224 "serve",
225 "--cmdserver", "chgunix",
225 "--cmdserver", "chgunix",
226 "--address", opts->sockname,
226 "--address", opts->sockname,
227 "--daemon-postexec", "chdir:/",
227 "--daemon-postexec", "chdir:/",
228 "--config", "extensions.chgserver=",
228 "--config", "extensions.chgserver=",
229 };
229 };
230 size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
230 size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
231 size_t argsize = baseargvsize + opts->argsize + 1;
231 size_t argsize = baseargvsize + opts->argsize + 1;
232
232
233 const char **argv = mallocx(sizeof(char *) * argsize);
233 const char **argv = mallocx(sizeof(char *) * argsize);
234 memcpy(argv, baseargv, sizeof(baseargv));
234 memcpy(argv, baseargv, sizeof(baseargv));
235 memcpy(argv + baseargvsize, opts->args, sizeof(char *) * opts->argsize);
235 memcpy(argv + baseargvsize, opts->args, sizeof(char *) * opts->argsize);
236 argv[argsize - 1] = NULL;
236 argv[argsize - 1] = NULL;
237
237
238 if (putenv("CHGINTERNALMARK=") != 0)
238 if (putenv("CHGINTERNALMARK=") != 0)
239 abortmsgerrno("failed to putenv");
239 abortmsgerrno("failed to putenv");
240 if (execvp(hgcmd, (char **)argv) < 0)
240 if (execvp(hgcmd, (char **)argv) < 0)
241 abortmsgerrno("failed to exec cmdserver");
241 abortmsgerrno("failed to exec cmdserver");
242 free(argv);
242 free(argv);
243 }
243 }
244
244
245 /* Retry until we can connect to the server. Give up after some time. */
245 /* Retry until we can connect to the server. Give up after some time. */
246 static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid)
246 static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid)
247 {
247 {
248 static const struct timespec sleepreq = {0, 10 * 1000000};
248 static const struct timespec sleepreq = {0, 10 * 1000000};
249 int pst = 0;
249 int pst = 0;
250
250
251 debugmsg("try connect to %s repeatedly", opts->sockname);
251 debugmsg("try connect to %s repeatedly", opts->sockname);
252 for (unsigned int i = 0; i < 10 * 100; i++) {
252
253 unsigned int timeoutsec = 60; /* default: 60 seconds */
254 const char *timeoutenv = getenv("CHGTIMEOUT");
255 if (timeoutenv)
256 sscanf(timeoutenv, "%u", &timeoutsec);
257
258 for (unsigned int i = 0; !timeoutsec || i < timeoutsec * 100; i++) {
253 hgclient_t *hgc = hgc_open(opts->sockname);
259 hgclient_t *hgc = hgc_open(opts->sockname);
254 if (hgc)
260 if (hgc)
255 return hgc;
261 return hgc;
256
262
257 if (pid > 0) {
263 if (pid > 0) {
258 /* collect zombie if child process fails to start */
264 /* collect zombie if child process fails to start */
259 int r = waitpid(pid, &pst, WNOHANG);
265 int r = waitpid(pid, &pst, WNOHANG);
260 if (r != 0)
266 if (r != 0)
261 goto cleanup;
267 goto cleanup;
262 }
268 }
263
269
264 nanosleep(&sleepreq, NULL);
270 nanosleep(&sleepreq, NULL);
265 }
271 }
266
272
267 abortmsg("timed out waiting for cmdserver %s", opts->sockname);
273 abortmsg("timed out waiting for cmdserver %s", opts->sockname);
268 return NULL;
274 return NULL;
269
275
270 cleanup:
276 cleanup:
271 if (WIFEXITED(pst)) {
277 if (WIFEXITED(pst)) {
272 if (WEXITSTATUS(pst) == 0)
278 if (WEXITSTATUS(pst) == 0)
273 abortmsg("could not connect to cmdserver "
279 abortmsg("could not connect to cmdserver "
274 "(exited with status 0)");
280 "(exited with status 0)");
275 debugmsg("cmdserver exited with status %d", WEXITSTATUS(pst));
281 debugmsg("cmdserver exited with status %d", WEXITSTATUS(pst));
276 exit(WEXITSTATUS(pst));
282 exit(WEXITSTATUS(pst));
277 } else if (WIFSIGNALED(pst)) {
283 } else if (WIFSIGNALED(pst)) {
278 abortmsg("cmdserver killed by signal %d", WTERMSIG(pst));
284 abortmsg("cmdserver killed by signal %d", WTERMSIG(pst));
279 } else {
285 } else {
280 abortmsg("error while waiting for cmdserver");
286 abortmsg("error while waiting for cmdserver");
281 }
287 }
282 return NULL;
288 return NULL;
283 }
289 }
284
290
285 /* Connect to a cmdserver. Will start a new server on demand. */
291 /* Connect to a cmdserver. Will start a new server on demand. */
286 static hgclient_t *connectcmdserver(struct cmdserveropts *opts)
292 static hgclient_t *connectcmdserver(struct cmdserveropts *opts)
287 {
293 {
288 const char *sockname = opts->redirectsockname[0] ?
294 const char *sockname = opts->redirectsockname[0] ?
289 opts->redirectsockname : opts->sockname;
295 opts->redirectsockname : opts->sockname;
290 debugmsg("try connect to %s", sockname);
296 debugmsg("try connect to %s", sockname);
291 hgclient_t *hgc = hgc_open(sockname);
297 hgclient_t *hgc = hgc_open(sockname);
292 if (hgc)
298 if (hgc)
293 return hgc;
299 return hgc;
294
300
295 lockcmdserver(opts);
301 lockcmdserver(opts);
296 hgc = hgc_open(sockname);
302 hgc = hgc_open(sockname);
297 if (hgc) {
303 if (hgc) {
298 unlockcmdserver(opts);
304 unlockcmdserver(opts);
299 debugmsg("cmdserver is started by another process");
305 debugmsg("cmdserver is started by another process");
300 return hgc;
306 return hgc;
301 }
307 }
302
308
303 /* prevent us from being connected to an outdated server: we were
309 /* prevent us from being connected to an outdated server: we were
304 * told by a server to redirect to opts->redirectsockname and that
310 * told by a server to redirect to opts->redirectsockname and that
305 * address does not work. we do not want to connect to the server
311 * address does not work. we do not want to connect to the server
306 * again because it will probably tell us the same thing. */
312 * again because it will probably tell us the same thing. */
307 if (sockname == opts->redirectsockname)
313 if (sockname == opts->redirectsockname)
308 unlink(opts->sockname);
314 unlink(opts->sockname);
309
315
310 debugmsg("start cmdserver at %s", opts->sockname);
316 debugmsg("start cmdserver at %s", opts->sockname);
311
317
312 pid_t pid = fork();
318 pid_t pid = fork();
313 if (pid < 0)
319 if (pid < 0)
314 abortmsg("failed to fork cmdserver process");
320 abortmsg("failed to fork cmdserver process");
315 if (pid == 0) {
321 if (pid == 0) {
316 execcmdserver(opts);
322 execcmdserver(opts);
317 } else {
323 } else {
318 hgc = retryconnectcmdserver(opts, pid);
324 hgc = retryconnectcmdserver(opts, pid);
319 }
325 }
320
326
321 unlockcmdserver(opts);
327 unlockcmdserver(opts);
322 return hgc;
328 return hgc;
323 }
329 }
324
330
325 static void killcmdserver(const struct cmdserveropts *opts)
331 static void killcmdserver(const struct cmdserveropts *opts)
326 {
332 {
327 /* resolve config hash */
333 /* resolve config hash */
328 char *resolvedpath = realpath(opts->sockname, NULL);
334 char *resolvedpath = realpath(opts->sockname, NULL);
329 if (resolvedpath) {
335 if (resolvedpath) {
330 unlink(resolvedpath);
336 unlink(resolvedpath);
331 free(resolvedpath);
337 free(resolvedpath);
332 }
338 }
333 }
339 }
334
340
341 static pid_t pagerpid = 0;
335 static pid_t peerpid = 0;
342 static pid_t peerpid = 0;
336
343
337 static void forwardsignal(int sig)
344 static void forwardsignal(int sig)
338 {
345 {
339 assert(peerpid > 0);
346 assert(peerpid > 0);
340 if (kill(peerpid, sig) < 0)
347 if (kill(peerpid, sig) < 0)
341 abortmsgerrno("cannot kill %d", peerpid);
348 abortmsgerrno("cannot kill %d", peerpid);
342 debugmsg("forward signal %d", sig);
349 debugmsg("forward signal %d", sig);
343 }
350 }
344
351
345 static void handlestopsignal(int sig)
352 static void handlestopsignal(int sig)
346 {
353 {
347 sigset_t unblockset, oldset;
354 sigset_t unblockset, oldset;
348 struct sigaction sa, oldsa;
355 struct sigaction sa, oldsa;
349 if (sigemptyset(&unblockset) < 0)
356 if (sigemptyset(&unblockset) < 0)
350 goto error;
357 goto error;
351 if (sigaddset(&unblockset, sig) < 0)
358 if (sigaddset(&unblockset, sig) < 0)
352 goto error;
359 goto error;
353 memset(&sa, 0, sizeof(sa));
360 memset(&sa, 0, sizeof(sa));
354 sa.sa_handler = SIG_DFL;
361 sa.sa_handler = SIG_DFL;
355 sa.sa_flags = SA_RESTART;
362 sa.sa_flags = SA_RESTART;
356 if (sigemptyset(&sa.sa_mask) < 0)
363 if (sigemptyset(&sa.sa_mask) < 0)
357 goto error;
364 goto error;
358
365
359 forwardsignal(sig);
366 forwardsignal(sig);
360 if (raise(sig) < 0) /* resend to self */
367 if (raise(sig) < 0) /* resend to self */
361 goto error;
368 goto error;
362 if (sigaction(sig, &sa, &oldsa) < 0)
369 if (sigaction(sig, &sa, &oldsa) < 0)
363 goto error;
370 goto error;
364 if (sigprocmask(SIG_UNBLOCK, &unblockset, &oldset) < 0)
371 if (sigprocmask(SIG_UNBLOCK, &unblockset, &oldset) < 0)
365 goto error;
372 goto error;
366 /* resent signal will be handled before sigprocmask() returns */
373 /* resent signal will be handled before sigprocmask() returns */
367 if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0)
374 if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0)
368 goto error;
375 goto error;
369 if (sigaction(sig, &oldsa, NULL) < 0)
376 if (sigaction(sig, &oldsa, NULL) < 0)
370 goto error;
377 goto error;
371 return;
378 return;
372
379
373 error:
380 error:
374 abortmsgerrno("failed to handle stop signal");
381 abortmsgerrno("failed to handle stop signal");
375 }
382 }
376
383
384 static void handlechildsignal(int sig UNUSED_)
385 {
386 if (peerpid == 0 || pagerpid == 0)
387 return;
388 /* if pager exits, notify the server with SIGPIPE immediately.
389 * otherwise the server won't get SIGPIPE if it does not write
390 * anything. (issue5278) */
391 if (waitpid(pagerpid, NULL, WNOHANG) == pagerpid)
392 kill(peerpid, SIGPIPE);
393 }
394
377 static void setupsignalhandler(pid_t pid)
395 static void setupsignalhandler(pid_t pid)
378 {
396 {
379 if (pid <= 0)
397 if (pid <= 0)
380 return;
398 return;
381 peerpid = pid;
399 peerpid = pid;
382
400
383 struct sigaction sa;
401 struct sigaction sa;
384 memset(&sa, 0, sizeof(sa));
402 memset(&sa, 0, sizeof(sa));
385 sa.sa_handler = forwardsignal;
403 sa.sa_handler = forwardsignal;
386 sa.sa_flags = SA_RESTART;
404 sa.sa_flags = SA_RESTART;
387 if (sigemptyset(&sa.sa_mask) < 0)
405 if (sigemptyset(&sa.sa_mask) < 0)
388 goto error;
406 goto error;
389
407
390 if (sigaction(SIGHUP, &sa, NULL) < 0)
408 if (sigaction(SIGHUP, &sa, NULL) < 0)
391 goto error;
409 goto error;
392 if (sigaction(SIGINT, &sa, NULL) < 0)
410 if (sigaction(SIGINT, &sa, NULL) < 0)
393 goto error;
411 goto error;
394
412
395 /* terminate frontend by double SIGTERM in case of server freeze */
413 /* terminate frontend by double SIGTERM in case of server freeze */
396 sa.sa_flags |= SA_RESETHAND;
414 sa.sa_flags |= SA_RESETHAND;
397 if (sigaction(SIGTERM, &sa, NULL) < 0)
415 if (sigaction(SIGTERM, &sa, NULL) < 0)
398 goto error;
416 goto error;
399
417
400 /* notify the worker about window resize events */
418 /* notify the worker about window resize events */
401 sa.sa_flags = SA_RESTART;
419 sa.sa_flags = SA_RESTART;
402 if (sigaction(SIGWINCH, &sa, NULL) < 0)
420 if (sigaction(SIGWINCH, &sa, NULL) < 0)
403 goto error;
421 goto error;
404 /* propagate job control requests to worker */
422 /* propagate job control requests to worker */
405 sa.sa_handler = forwardsignal;
423 sa.sa_handler = forwardsignal;
406 sa.sa_flags = SA_RESTART;
424 sa.sa_flags = SA_RESTART;
407 if (sigaction(SIGCONT, &sa, NULL) < 0)
425 if (sigaction(SIGCONT, &sa, NULL) < 0)
408 goto error;
426 goto error;
409 sa.sa_handler = handlestopsignal;
427 sa.sa_handler = handlestopsignal;
410 sa.sa_flags = SA_RESTART;
428 sa.sa_flags = SA_RESTART;
411 if (sigaction(SIGTSTP, &sa, NULL) < 0)
429 if (sigaction(SIGTSTP, &sa, NULL) < 0)
412 goto error;
430 goto error;
431 /* get notified when pager exits */
432 sa.sa_handler = handlechildsignal;
433 sa.sa_flags = SA_RESTART;
434 if (sigaction(SIGCHLD, &sa, NULL) < 0)
435 goto error;
413
436
414 return;
437 return;
415
438
416 error:
439 error:
417 abortmsgerrno("failed to set up signal handlers");
440 abortmsgerrno("failed to set up signal handlers");
418 }
441 }
419
442
420 /* This implementation is based on hgext/pager.py (pre 369741ef7253) */
443 static void restoresignalhandler()
421 static void setuppager(hgclient_t *hgc, const char *const args[],
444 {
445 struct sigaction sa;
446 memset(&sa, 0, sizeof(sa));
447 sa.sa_handler = SIG_DFL;
448 sa.sa_flags = SA_RESTART;
449 if (sigemptyset(&sa.sa_mask) < 0)
450 goto error;
451
452 if (sigaction(SIGHUP, &sa, NULL) < 0)
453 goto error;
454 if (sigaction(SIGTERM, &sa, NULL) < 0)
455 goto error;
456 if (sigaction(SIGWINCH, &sa, NULL) < 0)
457 goto error;
458 if (sigaction(SIGCONT, &sa, NULL) < 0)
459 goto error;
460 if (sigaction(SIGTSTP, &sa, NULL) < 0)
461 goto error;
462 if (sigaction(SIGCHLD, &sa, NULL) < 0)
463 goto error;
464
465 /* ignore Ctrl+C while shutting down to make pager exits cleanly */
466 sa.sa_handler = SIG_IGN;
467 if (sigaction(SIGINT, &sa, NULL) < 0)
468 goto error;
469
470 peerpid = 0;
471 return;
472
473 error:
474 abortmsgerrno("failed to restore signal handlers");
475 }
476
477 /* This implementation is based on hgext/pager.py (post 369741ef7253)
478 * Return 0 if pager is not started, or pid of the pager */
479 static pid_t setuppager(hgclient_t *hgc, const char *const args[],
422 size_t argsize)
480 size_t argsize)
423 {
481 {
424 const char *pagercmd = hgc_getpager(hgc, args, argsize);
482 const char *pagercmd = hgc_getpager(hgc, args, argsize);
425 if (!pagercmd)
483 if (!pagercmd)
426 return;
484 return 0;
427
485
428 int pipefds[2];
486 int pipefds[2];
429 if (pipe(pipefds) < 0)
487 if (pipe(pipefds) < 0)
430 return;
488 return 0;
431 pid_t pid = fork();
489 pid_t pid = fork();
432 if (pid < 0)
490 if (pid < 0)
433 goto error;
491 goto error;
434 if (pid == 0) {
492 if (pid > 0) {
435 close(pipefds[0]);
493 close(pipefds[0]);
436 if (dup2(pipefds[1], fileno(stdout)) < 0)
494 if (dup2(pipefds[1], fileno(stdout)) < 0)
437 goto error;
495 goto error;
438 if (isatty(fileno(stderr))) {
496 if (isatty(fileno(stderr))) {
439 if (dup2(pipefds[1], fileno(stderr)) < 0)
497 if (dup2(pipefds[1], fileno(stderr)) < 0)
440 goto error;
498 goto error;
441 }
499 }
442 close(pipefds[1]);
500 close(pipefds[1]);
443 hgc_attachio(hgc); /* reattach to pager */
501 hgc_attachio(hgc); /* reattach to pager */
444 return;
502 return pid;
445 } else {
503 } else {
446 dup2(pipefds[0], fileno(stdin));
504 dup2(pipefds[0], fileno(stdin));
447 close(pipefds[0]);
505 close(pipefds[0]);
448 close(pipefds[1]);
506 close(pipefds[1]);
449
507
450 int r = execlp("/bin/sh", "/bin/sh", "-c", pagercmd, NULL);
508 int r = execlp("/bin/sh", "/bin/sh", "-c", pagercmd, NULL);
451 if (r < 0) {
509 if (r < 0) {
452 abortmsgerrno("cannot start pager '%s'", pagercmd);
510 abortmsgerrno("cannot start pager '%s'", pagercmd);
453 }
511 }
454 return;
512 return 0;
455 }
513 }
456
514
457 error:
515 error:
458 close(pipefds[0]);
516 close(pipefds[0]);
459 close(pipefds[1]);
517 close(pipefds[1]);
460 abortmsgerrno("failed to prepare pager");
518 abortmsgerrno("failed to prepare pager");
519 return 0;
520 }
521
522 static void waitpager(pid_t pid)
523 {
524 /* close output streams to notify the pager its input ends */
525 fclose(stdout);
526 fclose(stderr);
527 while (1) {
528 pid_t ret = waitpid(pid, NULL, 0);
529 if (ret == -1 && errno == EINTR)
530 continue;
531 break;
532 }
461 }
533 }
462
534
463 /* Run instructions sent from the server like unlink and set redirect path
535 /* Run instructions sent from the server like unlink and set redirect path
464 * Return 1 if reconnect is needed, otherwise 0 */
536 * Return 1 if reconnect is needed, otherwise 0 */
465 static int runinstructions(struct cmdserveropts *opts, const char **insts)
537 static int runinstructions(struct cmdserveropts *opts, const char **insts)
466 {
538 {
467 int needreconnect = 0;
539 int needreconnect = 0;
468 if (!insts)
540 if (!insts)
469 return needreconnect;
541 return needreconnect;
470
542
471 assert(insts);
543 assert(insts);
472 opts->redirectsockname[0] = '\0';
544 opts->redirectsockname[0] = '\0';
473 const char **pinst;
545 const char **pinst;
474 for (pinst = insts; *pinst; pinst++) {
546 for (pinst = insts; *pinst; pinst++) {
475 debugmsg("instruction: %s", *pinst);
547 debugmsg("instruction: %s", *pinst);
476 if (strncmp(*pinst, "unlink ", 7) == 0) {
548 if (strncmp(*pinst, "unlink ", 7) == 0) {
477 unlink(*pinst + 7);
549 unlink(*pinst + 7);
478 } else if (strncmp(*pinst, "redirect ", 9) == 0) {
550 } else if (strncmp(*pinst, "redirect ", 9) == 0) {
479 int r = snprintf(opts->redirectsockname,
551 int r = snprintf(opts->redirectsockname,
480 sizeof(opts->redirectsockname),
552 sizeof(opts->redirectsockname),
481 "%s", *pinst + 9);
553 "%s", *pinst + 9);
482 if (r < 0 || r >= (int)sizeof(opts->redirectsockname))
554 if (r < 0 || r >= (int)sizeof(opts->redirectsockname))
483 abortmsg("redirect path is too long (%d)", r);
555 abortmsg("redirect path is too long (%d)", r);
484 needreconnect = 1;
556 needreconnect = 1;
485 } else if (strncmp(*pinst, "exit ", 5) == 0) {
557 } else if (strncmp(*pinst, "exit ", 5) == 0) {
486 int n = 0;
558 int n = 0;
487 if (sscanf(*pinst + 5, "%d", &n) != 1)
559 if (sscanf(*pinst + 5, "%d", &n) != 1)
488 abortmsg("cannot read the exit code");
560 abortmsg("cannot read the exit code");
489 exit(n);
561 exit(n);
490 } else if (strcmp(*pinst, "reconnect") == 0) {
562 } else if (strcmp(*pinst, "reconnect") == 0) {
491 needreconnect = 1;
563 needreconnect = 1;
492 } else {
564 } else {
493 abortmsg("unknown instruction: %s", *pinst);
565 abortmsg("unknown instruction: %s", *pinst);
494 }
566 }
495 }
567 }
496 return needreconnect;
568 return needreconnect;
497 }
569 }
498
570
499 /*
571 /*
500 * Test whether the command is unsupported or not. This is not designed to
572 * Test whether the command is unsupported or not. This is not designed to
501 * cover all cases. But it's fast, does not depend on the server and does
573 * cover all cases. But it's fast, does not depend on the server and does
502 * not return false positives.
574 * not return false positives.
503 */
575 */
504 static int isunsupported(int argc, const char *argv[])
576 static int isunsupported(int argc, const char *argv[])
505 {
577 {
506 enum {
578 enum {
507 SERVE = 1,
579 SERVE = 1,
508 DAEMON = 2,
580 DAEMON = 2,
509 SERVEDAEMON = SERVE | DAEMON,
581 SERVEDAEMON = SERVE | DAEMON,
510 TIME = 4,
582 TIME = 4,
511 };
583 };
512 unsigned int state = 0;
584 unsigned int state = 0;
513 int i;
585 int i;
514 for (i = 0; i < argc; ++i) {
586 for (i = 0; i < argc; ++i) {
515 if (strcmp(argv[i], "--") == 0)
587 if (strcmp(argv[i], "--") == 0)
516 break;
588 break;
517 if (i == 0 && strcmp("serve", argv[i]) == 0)
589 if (i == 0 && strcmp("serve", argv[i]) == 0)
518 state |= SERVE;
590 state |= SERVE;
519 else if (strcmp("-d", argv[i]) == 0 ||
591 else if (strcmp("-d", argv[i]) == 0 ||
520 strcmp("--daemon", argv[i]) == 0)
592 strcmp("--daemon", argv[i]) == 0)
521 state |= DAEMON;
593 state |= DAEMON;
522 else if (strcmp("--time", argv[i]) == 0)
594 else if (strcmp("--time", argv[i]) == 0)
523 state |= TIME;
595 state |= TIME;
524 }
596 }
525 return (state & TIME) == TIME ||
597 return (state & TIME) == TIME ||
526 (state & SERVEDAEMON) == SERVEDAEMON;
598 (state & SERVEDAEMON) == SERVEDAEMON;
527 }
599 }
528
600
529 static void execoriginalhg(const char *argv[])
601 static void execoriginalhg(const char *argv[])
530 {
602 {
531 debugmsg("execute original hg");
603 debugmsg("execute original hg");
532 if (execvp(gethgcmd(), (char **)argv) < 0)
604 if (execvp(gethgcmd(), (char **)argv) < 0)
533 abortmsgerrno("failed to exec original hg");
605 abortmsgerrno("failed to exec original hg");
534 }
606 }
535
607
536 int main(int argc, const char *argv[], const char *envp[])
608 int main(int argc, const char *argv[], const char *envp[])
537 {
609 {
538 if (getenv("CHGDEBUG"))
610 if (getenv("CHGDEBUG"))
539 enabledebugmsg();
611 enabledebugmsg();
540
612
541 if (!getenv("HGPLAIN") && isatty(fileno(stderr)))
613 if (!getenv("HGPLAIN") && isatty(fileno(stderr)))
542 enablecolor();
614 enablecolor();
543
615
544 if (getenv("CHGINTERNALMARK"))
616 if (getenv("CHGINTERNALMARK"))
545 abortmsg("chg started by chg detected.\n"
617 abortmsg("chg started by chg detected.\n"
546 "Please make sure ${HG:-hg} is not a symlink or "
618 "Please make sure ${HG:-hg} is not a symlink or "
547 "wrapper to chg. Alternatively, set $CHGHG to the "
619 "wrapper to chg. Alternatively, set $CHGHG to the "
548 "path of real hg.");
620 "path of real hg.");
549
621
550 if (isunsupported(argc - 1, argv + 1))
622 if (isunsupported(argc - 1, argv + 1))
551 execoriginalhg(argv);
623 execoriginalhg(argv);
552
624
553 struct cmdserveropts opts;
625 struct cmdserveropts opts;
554 initcmdserveropts(&opts);
626 initcmdserveropts(&opts);
555 setcmdserveropts(&opts);
627 setcmdserveropts(&opts);
556 setcmdserverargs(&opts, argc, argv);
628 setcmdserverargs(&opts, argc, argv);
557
629
558 if (argc == 2) {
630 if (argc == 2) {
559 if (strcmp(argv[1], "--kill-chg-daemon") == 0) {
631 if (strcmp(argv[1], "--kill-chg-daemon") == 0) {
560 killcmdserver(&opts);
632 killcmdserver(&opts);
561 return 0;
633 return 0;
562 }
634 }
563 }
635 }
564
636
565 hgclient_t *hgc;
637 hgclient_t *hgc;
566 size_t retry = 0;
638 size_t retry = 0;
567 while (1) {
639 while (1) {
568 hgc = connectcmdserver(&opts);
640 hgc = connectcmdserver(&opts);
569 if (!hgc)
641 if (!hgc)
570 abortmsg("cannot open hg client");
642 abortmsg("cannot open hg client");
571 hgc_setenv(hgc, envp);
643 hgc_setenv(hgc, envp);
572 const char **insts = hgc_validate(hgc, argv + 1, argc - 1);
644 const char **insts = hgc_validate(hgc, argv + 1, argc - 1);
573 int needreconnect = runinstructions(&opts, insts);
645 int needreconnect = runinstructions(&opts, insts);
574 free(insts);
646 free(insts);
575 if (!needreconnect)
647 if (!needreconnect)
576 break;
648 break;
577 hgc_close(hgc);
649 hgc_close(hgc);
578 if (++retry > 10)
650 if (++retry > 10)
579 abortmsg("too many redirections.\n"
651 abortmsg("too many redirections.\n"
580 "Please make sure %s is not a wrapper which "
652 "Please make sure %s is not a wrapper which "
581 "changes sensitive environment variables "
653 "changes sensitive environment variables "
582 "before executing hg. If you have to use a "
654 "before executing hg. If you have to use a "
583 "wrapper, wrap chg instead of hg.",
655 "wrapper, wrap chg instead of hg.",
584 gethgcmd());
656 gethgcmd());
585 }
657 }
586
658
587 setupsignalhandler(hgc_peerpid(hgc));
659 setupsignalhandler(hgc_peerpid(hgc));
588 setuppager(hgc, argv + 1, argc - 1);
660 pagerpid = setuppager(hgc, argv + 1, argc - 1);
589 int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1);
661 int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1);
662 restoresignalhandler();
590 hgc_close(hgc);
663 hgc_close(hgc);
591 freecmdserveropts(&opts);
664 freecmdserveropts(&opts);
665 if (pagerpid)
666 waitpager(pagerpid);
667
592 return exitcode;
668 return exitcode;
593 }
669 }
@@ -1,576 +1,590
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 pid;
67 pid_t pid;
67 context_t ctx;
68 context_t ctx;
68 unsigned int capflags;
69 unsigned int capflags;
69 };
70 };
70
71
71 static const size_t defaultdatasize = 4096;
72 static const size_t defaultdatasize = 4096;
72
73
73 static void initcontext(context_t *ctx)
74 static void initcontext(context_t *ctx)
74 {
75 {
75 ctx->ch = '\0';
76 ctx->ch = '\0';
76 ctx->data = malloc(defaultdatasize);
77 ctx->data = malloc(defaultdatasize);
77 ctx->maxdatasize = (ctx->data) ? defaultdatasize : 0;
78 ctx->maxdatasize = (ctx->data) ? defaultdatasize : 0;
78 ctx->datasize = 0;
79 ctx->datasize = 0;
79 debugmsg("initialize context buffer with size %zu", ctx->maxdatasize);
80 debugmsg("initialize context buffer with size %zu", ctx->maxdatasize);
80 }
81 }
81
82
82 static void enlargecontext(context_t *ctx, size_t newsize)
83 static void enlargecontext(context_t *ctx, size_t newsize)
83 {
84 {
84 if (newsize <= ctx->maxdatasize)
85 if (newsize <= ctx->maxdatasize)
85 return;
86 return;
86
87
87 newsize = defaultdatasize
88 newsize = defaultdatasize
88 * ((newsize + defaultdatasize - 1) / defaultdatasize);
89 * ((newsize + defaultdatasize - 1) / defaultdatasize);
89 ctx->data = reallocx(ctx->data, newsize);
90 ctx->data = reallocx(ctx->data, newsize);
90 ctx->maxdatasize = newsize;
91 ctx->maxdatasize = newsize;
91 debugmsg("enlarge context buffer to %zu", ctx->maxdatasize);
92 debugmsg("enlarge context buffer to %zu", ctx->maxdatasize);
92 }
93 }
93
94
94 static void freecontext(context_t *ctx)
95 static void freecontext(context_t *ctx)
95 {
96 {
96 debugmsg("free context buffer");
97 debugmsg("free context buffer");
97 free(ctx->data);
98 free(ctx->data);
98 ctx->data = NULL;
99 ctx->data = NULL;
99 ctx->maxdatasize = 0;
100 ctx->maxdatasize = 0;
100 ctx->datasize = 0;
101 ctx->datasize = 0;
101 }
102 }
102
103
103 /* Read channeled response from cmdserver */
104 /* Read channeled response from cmdserver */
104 static void readchannel(hgclient_t *hgc)
105 static void readchannel(hgclient_t *hgc)
105 {
106 {
106 assert(hgc);
107 assert(hgc);
107
108
108 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);
109 if (rsize != sizeof(hgc->ctx.ch)) {
110 if (rsize != sizeof(hgc->ctx.ch)) {
110 /* server would have exception and traceback would be printed */
111 /* server would have exception and traceback would be printed */
111 debugmsg("failed to read channel");
112 debugmsg("failed to read channel");
112 exit(255);
113 exit(255);
113 }
114 }
114
115
115 uint32_t datasize_n;
116 uint32_t datasize_n;
116 rsize = recv(hgc->sockfd, &datasize_n, sizeof(datasize_n), 0);
117 rsize = recv(hgc->sockfd, &datasize_n, sizeof(datasize_n), 0);
117 if (rsize != sizeof(datasize_n))
118 if (rsize != sizeof(datasize_n))
118 abortmsg("failed to read data size");
119 abortmsg("failed to read data size");
119
120
120 /* datasize denotes the maximum size to write if input request */
121 /* datasize denotes the maximum size to write if input request */
121 hgc->ctx.datasize = ntohl(datasize_n);
122 hgc->ctx.datasize = ntohl(datasize_n);
122 enlargecontext(&hgc->ctx, hgc->ctx.datasize);
123 enlargecontext(&hgc->ctx, hgc->ctx.datasize);
123
124
124 if (isupper(hgc->ctx.ch) && hgc->ctx.ch != 'S')
125 if (isupper(hgc->ctx.ch) && hgc->ctx.ch != 'S')
125 return; /* assumes input request */
126 return; /* assumes input request */
126
127
127 size_t cursize = 0;
128 size_t cursize = 0;
129 int emptycount = 0;
128 while (cursize < hgc->ctx.datasize) {
130 while (cursize < hgc->ctx.datasize) {
129 rsize = recv(hgc->sockfd, hgc->ctx.data + cursize,
131 rsize = recv(hgc->sockfd, hgc->ctx.data + cursize,
130 hgc->ctx.datasize - cursize, 0);
132 hgc->ctx.datasize - cursize, 0);
131 if (rsize < 0)
133 /* rsize == 0 normally indicates EOF, while it's also a valid
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)
132 abortmsg("failed to read data block");
138 abortmsg("failed to read data block");
133 cursize += rsize;
139 cursize += rsize;
134 }
140 }
135 }
141 }
136
142
137 static void sendall(int sockfd, const void *data, size_t datasize)
143 static void sendall(int sockfd, const void *data, size_t datasize)
138 {
144 {
139 const char *p = data;
145 const char *p = data;
140 const char *const endp = p + datasize;
146 const char *const endp = p + datasize;
141 while (p < endp) {
147 while (p < endp) {
142 ssize_t r = send(sockfd, p, endp - p, 0);
148 ssize_t r = send(sockfd, p, endp - p, 0);
143 if (r < 0)
149 if (r < 0)
144 abortmsgerrno("cannot communicate");
150 abortmsgerrno("cannot communicate");
145 p += r;
151 p += r;
146 }
152 }
147 }
153 }
148
154
149 /* Write lengh-data block to cmdserver */
155 /* Write lengh-data block to cmdserver */
150 static void writeblock(const hgclient_t *hgc)
156 static void writeblock(const hgclient_t *hgc)
151 {
157 {
152 assert(hgc);
158 assert(hgc);
153
159
154 const uint32_t datasize_n = htonl(hgc->ctx.datasize);
160 const uint32_t datasize_n = htonl(hgc->ctx.datasize);
155 sendall(hgc->sockfd, &datasize_n, sizeof(datasize_n));
161 sendall(hgc->sockfd, &datasize_n, sizeof(datasize_n));
156
162
157 sendall(hgc->sockfd, hgc->ctx.data, hgc->ctx.datasize);
163 sendall(hgc->sockfd, hgc->ctx.data, hgc->ctx.datasize);
158 }
164 }
159
165
160 static void writeblockrequest(const hgclient_t *hgc, const char *chcmd)
166 static void writeblockrequest(const hgclient_t *hgc, const char *chcmd)
161 {
167 {
162 debugmsg("request %s, block size %zu", chcmd, hgc->ctx.datasize);
168 debugmsg("request %s, block size %zu", chcmd, hgc->ctx.datasize);
163
169
164 char buf[strlen(chcmd) + 1];
170 char buf[strlen(chcmd) + 1];
165 memcpy(buf, chcmd, sizeof(buf) - 1);
171 memcpy(buf, chcmd, sizeof(buf) - 1);
166 buf[sizeof(buf) - 1] = '\n';
172 buf[sizeof(buf) - 1] = '\n';
167 sendall(hgc->sockfd, buf, sizeof(buf));
173 sendall(hgc->sockfd, buf, sizeof(buf));
168
174
169 writeblock(hgc);
175 writeblock(hgc);
170 }
176 }
171
177
172 /* Build '\0'-separated list of args. argsize < 0 denotes that args are
178 /* Build '\0'-separated list of args. argsize < 0 denotes that args are
173 * terminated by NULL. */
179 * terminated by NULL. */
174 static void packcmdargs(context_t *ctx, const char *const args[],
180 static void packcmdargs(context_t *ctx, const char *const args[],
175 ssize_t argsize)
181 ssize_t argsize)
176 {
182 {
177 ctx->datasize = 0;
183 ctx->datasize = 0;
178 const char *const *const end = (argsize >= 0) ? args + argsize : NULL;
184 const char *const *const end = (argsize >= 0) ? args + argsize : NULL;
179 for (const char *const *it = args; it != end && *it; ++it) {
185 for (const char *const *it = args; it != end && *it; ++it) {
180 const size_t n = strlen(*it) + 1; /* include '\0' */
186 const size_t n = strlen(*it) + 1; /* include '\0' */
181 enlargecontext(ctx, ctx->datasize + n);
187 enlargecontext(ctx, ctx->datasize + n);
182 memcpy(ctx->data + ctx->datasize, *it, n);
188 memcpy(ctx->data + ctx->datasize, *it, n);
183 ctx->datasize += n;
189 ctx->datasize += n;
184 }
190 }
185
191
186 if (ctx->datasize > 0)
192 if (ctx->datasize > 0)
187 --ctx->datasize; /* strip last '\0' */
193 --ctx->datasize; /* strip last '\0' */
188 }
194 }
189
195
190 /* Extract '\0'-separated list of args to new buffer, terminated by NULL */
196 /* Extract '\0'-separated list of args to new buffer, terminated by NULL */
191 static const char **unpackcmdargsnul(const context_t *ctx)
197 static const char **unpackcmdargsnul(const context_t *ctx)
192 {
198 {
193 const char **args = NULL;
199 const char **args = NULL;
194 size_t nargs = 0, maxnargs = 0;
200 size_t nargs = 0, maxnargs = 0;
195 const char *s = ctx->data;
201 const char *s = ctx->data;
196 const char *e = ctx->data + ctx->datasize;
202 const char *e = ctx->data + ctx->datasize;
197 for (;;) {
203 for (;;) {
198 if (nargs + 1 >= maxnargs) { /* including last NULL */
204 if (nargs + 1 >= maxnargs) { /* including last NULL */
199 maxnargs += 256;
205 maxnargs += 256;
200 args = reallocx(args, maxnargs * sizeof(args[0]));
206 args = reallocx(args, maxnargs * sizeof(args[0]));
201 }
207 }
202 args[nargs] = s;
208 args[nargs] = s;
203 nargs++;
209 nargs++;
204 s = memchr(s, '\0', e - s);
210 s = memchr(s, '\0', e - s);
205 if (!s)
211 if (!s)
206 break;
212 break;
207 s++;
213 s++;
208 }
214 }
209 args[nargs] = NULL;
215 args[nargs] = NULL;
210 return args;
216 return args;
211 }
217 }
212
218
213 static void handlereadrequest(hgclient_t *hgc)
219 static void handlereadrequest(hgclient_t *hgc)
214 {
220 {
215 context_t *ctx = &hgc->ctx;
221 context_t *ctx = &hgc->ctx;
216 size_t r = fread(ctx->data, sizeof(ctx->data[0]), ctx->datasize, stdin);
222 size_t r = fread(ctx->data, sizeof(ctx->data[0]), ctx->datasize, stdin);
217 ctx->datasize = r;
223 ctx->datasize = r;
218 writeblock(hgc);
224 writeblock(hgc);
219 }
225 }
220
226
221 /* Read single-line */
227 /* Read single-line */
222 static void handlereadlinerequest(hgclient_t *hgc)
228 static void handlereadlinerequest(hgclient_t *hgc)
223 {
229 {
224 context_t *ctx = &hgc->ctx;
230 context_t *ctx = &hgc->ctx;
225 if (!fgets(ctx->data, ctx->datasize, stdin))
231 if (!fgets(ctx->data, ctx->datasize, stdin))
226 ctx->data[0] = '\0';
232 ctx->data[0] = '\0';
227 ctx->datasize = strlen(ctx->data);
233 ctx->datasize = strlen(ctx->data);
228 writeblock(hgc);
234 writeblock(hgc);
229 }
235 }
230
236
231 /* Execute the requested command and write exit code */
237 /* Execute the requested command and write exit code */
232 static void handlesystemrequest(hgclient_t *hgc)
238 static void handlesystemrequest(hgclient_t *hgc)
233 {
239 {
234 context_t *ctx = &hgc->ctx;
240 context_t *ctx = &hgc->ctx;
235 enlargecontext(ctx, ctx->datasize + 1);
241 enlargecontext(ctx, ctx->datasize + 1);
236 ctx->data[ctx->datasize] = '\0'; /* terminate last string */
242 ctx->data[ctx->datasize] = '\0'; /* terminate last string */
237
243
238 const char **args = unpackcmdargsnul(ctx);
244 const char **args = unpackcmdargsnul(ctx);
239 if (!args[0] || !args[1])
245 if (!args[0] || !args[1])
240 abortmsg("missing command or cwd in system request");
246 abortmsg("missing command or cwd in system request");
241 debugmsg("run '%s' at '%s'", args[0], args[1]);
247 debugmsg("run '%s' at '%s'", args[0], args[1]);
242 int32_t r = runshellcmd(args[0], args + 2, args[1]);
248 int32_t r = runshellcmd(args[0], args + 2, args[1]);
243 free(args);
249 free(args);
244
250
245 uint32_t r_n = htonl(r);
251 uint32_t r_n = htonl(r);
246 memcpy(ctx->data, &r_n, sizeof(r_n));
252 memcpy(ctx->data, &r_n, sizeof(r_n));
247 ctx->datasize = sizeof(r_n);
253 ctx->datasize = sizeof(r_n);
248 writeblock(hgc);
254 writeblock(hgc);
249 }
255 }
250
256
251 /* Read response of command execution until receiving 'r'-esult */
257 /* Read response of command execution until receiving 'r'-esult */
252 static void handleresponse(hgclient_t *hgc)
258 static void handleresponse(hgclient_t *hgc)
253 {
259 {
254 for (;;) {
260 for (;;) {
255 readchannel(hgc);
261 readchannel(hgc);
256 context_t *ctx = &hgc->ctx;
262 context_t *ctx = &hgc->ctx;
257 debugmsg("response read from channel %c, size %zu",
263 debugmsg("response read from channel %c, size %zu",
258 ctx->ch, ctx->datasize);
264 ctx->ch, ctx->datasize);
259 switch (ctx->ch) {
265 switch (ctx->ch) {
260 case 'o':
266 case 'o':
261 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
267 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
262 stdout);
268 stdout);
263 break;
269 break;
264 case 'e':
270 case 'e':
265 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
271 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
266 stderr);
272 stderr);
267 break;
273 break;
268 case 'd':
274 case 'd':
269 /* assumes last char is '\n' */
275 /* assumes last char is '\n' */
270 ctx->data[ctx->datasize - 1] = '\0';
276 ctx->data[ctx->datasize - 1] = '\0';
271 debugmsg("server: %s", ctx->data);
277 debugmsg("server: %s", ctx->data);
272 break;
278 break;
273 case 'r':
279 case 'r':
274 return;
280 return;
275 case 'I':
281 case 'I':
276 handlereadrequest(hgc);
282 handlereadrequest(hgc);
277 break;
283 break;
278 case 'L':
284 case 'L':
279 handlereadlinerequest(hgc);
285 handlereadlinerequest(hgc);
280 break;
286 break;
281 case 'S':
287 case 'S':
282 handlesystemrequest(hgc);
288 handlesystemrequest(hgc);
283 break;
289 break;
284 default:
290 default:
285 if (isupper(ctx->ch))
291 if (isupper(ctx->ch))
286 abortmsg("cannot handle response (ch = %c)",
292 abortmsg("cannot handle response (ch = %c)",
287 ctx->ch);
293 ctx->ch);
288 }
294 }
289 }
295 }
290 }
296 }
291
297
292 static unsigned int parsecapabilities(const char *s, const char *e)
298 static unsigned int parsecapabilities(const char *s, const char *e)
293 {
299 {
294 unsigned int flags = 0;
300 unsigned int flags = 0;
295 while (s < e) {
301 while (s < e) {
296 const char *t = strchr(s, ' ');
302 const char *t = strchr(s, ' ');
297 if (!t || t > e)
303 if (!t || t > e)
298 t = e;
304 t = e;
299 const cappair_t *cap;
305 const cappair_t *cap;
300 for (cap = captable; cap->flag; ++cap) {
306 for (cap = captable; cap->flag; ++cap) {
301 size_t n = t - s;
307 size_t n = t - s;
302 if (strncmp(s, cap->name, n) == 0 &&
308 if (strncmp(s, cap->name, n) == 0 &&
303 strlen(cap->name) == n) {
309 strlen(cap->name) == n) {
304 flags |= cap->flag;
310 flags |= cap->flag;
305 break;
311 break;
306 }
312 }
307 }
313 }
308 s = t + 1;
314 s = t + 1;
309 }
315 }
310 return flags;
316 return flags;
311 }
317 }
312
318
313 static void readhello(hgclient_t *hgc)
319 static void readhello(hgclient_t *hgc)
314 {
320 {
315 readchannel(hgc);
321 readchannel(hgc);
316 context_t *ctx = &hgc->ctx;
322 context_t *ctx = &hgc->ctx;
317 if (ctx->ch != 'o') {
323 if (ctx->ch != 'o') {
318 char ch = ctx->ch;
324 char ch = ctx->ch;
319 if (ch == 'e') {
325 if (ch == 'e') {
320 /* write early error and will exit */
326 /* write early error and will exit */
321 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
327 fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize,
322 stderr);
328 stderr);
323 handleresponse(hgc);
329 handleresponse(hgc);
324 }
330 }
325 abortmsg("unexpected channel of hello message (ch = %c)", ch);
331 abortmsg("unexpected channel of hello message (ch = %c)", ch);
326 }
332 }
327 enlargecontext(ctx, ctx->datasize + 1);
333 enlargecontext(ctx, ctx->datasize + 1);
328 ctx->data[ctx->datasize] = '\0';
334 ctx->data[ctx->datasize] = '\0';
329 debugmsg("hello received: %s (size = %zu)", ctx->data, ctx->datasize);
335 debugmsg("hello received: %s (size = %zu)", ctx->data, ctx->datasize);
330
336
331 const char *s = ctx->data;
337 const char *s = ctx->data;
332 const char *const dataend = ctx->data + ctx->datasize;
338 const char *const dataend = ctx->data + ctx->datasize;
333 while (s < dataend) {
339 while (s < dataend) {
334 const char *t = strchr(s, ':');
340 const char *t = strchr(s, ':');
335 if (!t || t[1] != ' ')
341 if (!t || t[1] != ' ')
336 break;
342 break;
337 const char *u = strchr(t + 2, '\n');
343 const char *u = strchr(t + 2, '\n');
338 if (!u)
344 if (!u)
339 u = dataend;
345 u = dataend;
340 if (strncmp(s, "capabilities:", t - s + 1) == 0) {
346 if (strncmp(s, "capabilities:", t - s + 1) == 0) {
341 hgc->capflags = parsecapabilities(t + 2, u);
347 hgc->capflags = parsecapabilities(t + 2, u);
348 } else if (strncmp(s, "pgid:", t - s + 1) == 0) {
349 hgc->pgid = strtol(t + 2, NULL, 10);
342 } else if (strncmp(s, "pid:", t - s + 1) == 0) {
350 } else if (strncmp(s, "pid:", t - s + 1) == 0) {
343 hgc->pid = strtol(t + 2, NULL, 10);
351 hgc->pid = strtol(t + 2, NULL, 10);
344 }
352 }
345 s = u + 1;
353 s = u + 1;
346 }
354 }
347 debugmsg("capflags=0x%04x, pid=%d", hgc->capflags, hgc->pid);
355 debugmsg("capflags=0x%04x, pid=%d", hgc->capflags, hgc->pid);
348 }
356 }
349
357
350 static void attachio(hgclient_t *hgc)
358 static void attachio(hgclient_t *hgc)
351 {
359 {
352 debugmsg("request attachio");
360 debugmsg("request attachio");
353 static const char chcmd[] = "attachio\n";
361 static const char chcmd[] = "attachio\n";
354 sendall(hgc->sockfd, chcmd, sizeof(chcmd) - 1);
362 sendall(hgc->sockfd, chcmd, sizeof(chcmd) - 1);
355 readchannel(hgc);
363 readchannel(hgc);
356 context_t *ctx = &hgc->ctx;
364 context_t *ctx = &hgc->ctx;
357 if (ctx->ch != 'I')
365 if (ctx->ch != 'I')
358 abortmsg("unexpected response for attachio (ch = %c)", ctx->ch);
366 abortmsg("unexpected response for attachio (ch = %c)", ctx->ch);
359
367
360 static const int fds[3] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
368 static const int fds[3] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
361 struct msghdr msgh;
369 struct msghdr msgh;
362 memset(&msgh, 0, sizeof(msgh));
370 memset(&msgh, 0, sizeof(msgh));
363 struct iovec iov = {ctx->data, ctx->datasize}; /* dummy payload */
371 struct iovec iov = {ctx->data, ctx->datasize}; /* dummy payload */
364 msgh.msg_iov = &iov;
372 msgh.msg_iov = &iov;
365 msgh.msg_iovlen = 1;
373 msgh.msg_iovlen = 1;
366 char fdbuf[CMSG_SPACE(sizeof(fds))];
374 char fdbuf[CMSG_SPACE(sizeof(fds))];
367 msgh.msg_control = fdbuf;
375 msgh.msg_control = fdbuf;
368 msgh.msg_controllen = sizeof(fdbuf);
376 msgh.msg_controllen = sizeof(fdbuf);
369 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh);
377 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh);
370 cmsg->cmsg_level = SOL_SOCKET;
378 cmsg->cmsg_level = SOL_SOCKET;
371 cmsg->cmsg_type = SCM_RIGHTS;
379 cmsg->cmsg_type = SCM_RIGHTS;
372 cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
380 cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
373 memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
381 memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
374 msgh.msg_controllen = cmsg->cmsg_len;
382 msgh.msg_controllen = cmsg->cmsg_len;
375 ssize_t r = sendmsg(hgc->sockfd, &msgh, 0);
383 ssize_t r = sendmsg(hgc->sockfd, &msgh, 0);
376 if (r < 0)
384 if (r < 0)
377 abortmsgerrno("sendmsg failed");
385 abortmsgerrno("sendmsg failed");
378
386
379 handleresponse(hgc);
387 handleresponse(hgc);
380 int32_t n;
388 int32_t n;
381 if (ctx->datasize != sizeof(n))
389 if (ctx->datasize != sizeof(n))
382 abortmsg("unexpected size of attachio result");
390 abortmsg("unexpected size of attachio result");
383 memcpy(&n, ctx->data, sizeof(n));
391 memcpy(&n, ctx->data, sizeof(n));
384 n = ntohl(n);
392 n = ntohl(n);
385 if (n != sizeof(fds) / sizeof(fds[0]))
393 if (n != sizeof(fds) / sizeof(fds[0]))
386 abortmsg("failed to send fds (n = %d)", n);
394 abortmsg("failed to send fds (n = %d)", n);
387 }
395 }
388
396
389 static void chdirtocwd(hgclient_t *hgc)
397 static void chdirtocwd(hgclient_t *hgc)
390 {
398 {
391 if (!getcwd(hgc->ctx.data, hgc->ctx.maxdatasize))
399 if (!getcwd(hgc->ctx.data, hgc->ctx.maxdatasize))
392 abortmsgerrno("failed to getcwd");
400 abortmsgerrno("failed to getcwd");
393 hgc->ctx.datasize = strlen(hgc->ctx.data);
401 hgc->ctx.datasize = strlen(hgc->ctx.data);
394 writeblockrequest(hgc, "chdir");
402 writeblockrequest(hgc, "chdir");
395 }
403 }
396
404
397 static void forwardumask(hgclient_t *hgc)
405 static void forwardumask(hgclient_t *hgc)
398 {
406 {
399 mode_t mask = umask(0);
407 mode_t mask = umask(0);
400 umask(mask);
408 umask(mask);
401
409
402 static const char command[] = "setumask\n";
410 static const char command[] = "setumask\n";
403 sendall(hgc->sockfd, command, sizeof(command) - 1);
411 sendall(hgc->sockfd, command, sizeof(command) - 1);
404 uint32_t data = htonl(mask);
412 uint32_t data = htonl(mask);
405 sendall(hgc->sockfd, &data, sizeof(data));
413 sendall(hgc->sockfd, &data, sizeof(data));
406 }
414 }
407
415
408 /*!
416 /*!
409 * Open connection to per-user cmdserver
417 * Open connection to per-user cmdserver
410 *
418 *
411 * If no background server running, returns NULL.
419 * If no background server running, returns NULL.
412 */
420 */
413 hgclient_t *hgc_open(const char *sockname)
421 hgclient_t *hgc_open(const char *sockname)
414 {
422 {
415 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
423 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
416 if (fd < 0)
424 if (fd < 0)
417 abortmsgerrno("cannot create socket");
425 abortmsgerrno("cannot create socket");
418
426
419 /* don't keep fd on fork(), so that it can be closed when the parent
427 /* don't keep fd on fork(), so that it can be closed when the parent
420 * process get terminated. */
428 * process get terminated. */
421 fsetcloexec(fd);
429 fsetcloexec(fd);
422
430
423 struct sockaddr_un addr;
431 struct sockaddr_un addr;
424 addr.sun_family = AF_UNIX;
432 addr.sun_family = AF_UNIX;
425 strncpy(addr.sun_path, sockname, sizeof(addr.sun_path));
433 strncpy(addr.sun_path, sockname, sizeof(addr.sun_path));
426 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
434 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
427
435
428 int r = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
436 int r = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
429 if (r < 0) {
437 if (r < 0) {
430 close(fd);
438 close(fd);
431 if (errno == ENOENT || errno == ECONNREFUSED)
439 if (errno == ENOENT || errno == ECONNREFUSED)
432 return NULL;
440 return NULL;
433 abortmsgerrno("cannot connect to %s", addr.sun_path);
441 abortmsgerrno("cannot connect to %s", addr.sun_path);
434 }
442 }
435 debugmsg("connected to %s", addr.sun_path);
443 debugmsg("connected to %s", addr.sun_path);
436
444
437 hgclient_t *hgc = mallocx(sizeof(hgclient_t));
445 hgclient_t *hgc = mallocx(sizeof(hgclient_t));
438 memset(hgc, 0, sizeof(*hgc));
446 memset(hgc, 0, sizeof(*hgc));
439 hgc->sockfd = fd;
447 hgc->sockfd = fd;
440 initcontext(&hgc->ctx);
448 initcontext(&hgc->ctx);
441
449
442 readhello(hgc);
450 readhello(hgc);
443 if (!(hgc->capflags & CAP_RUNCOMMAND))
451 if (!(hgc->capflags & CAP_RUNCOMMAND))
444 abortmsg("insufficient capability: runcommand");
452 abortmsg("insufficient capability: runcommand");
445 if (hgc->capflags & CAP_ATTACHIO)
453 if (hgc->capflags & CAP_ATTACHIO)
446 attachio(hgc);
454 attachio(hgc);
447 if (hgc->capflags & CAP_CHDIR)
455 if (hgc->capflags & CAP_CHDIR)
448 chdirtocwd(hgc);
456 chdirtocwd(hgc);
449 if (hgc->capflags & CAP_SETUMASK)
457 if (hgc->capflags & CAP_SETUMASK)
450 forwardumask(hgc);
458 forwardumask(hgc);
451
459
452 return hgc;
460 return hgc;
453 }
461 }
454
462
455 /*!
463 /*!
456 * Close connection and free allocated memory
464 * Close connection and free allocated memory
457 */
465 */
458 void hgc_close(hgclient_t *hgc)
466 void hgc_close(hgclient_t *hgc)
459 {
467 {
460 assert(hgc);
468 assert(hgc);
461 freecontext(&hgc->ctx);
469 freecontext(&hgc->ctx);
462 close(hgc->sockfd);
470 close(hgc->sockfd);
463 free(hgc);
471 free(hgc);
464 }
472 }
465
473
474 pid_t hgc_peerpgid(const hgclient_t *hgc)
475 {
476 assert(hgc);
477 return hgc->pgid;
478 }
479
466 pid_t hgc_peerpid(const hgclient_t *hgc)
480 pid_t hgc_peerpid(const hgclient_t *hgc)
467 {
481 {
468 assert(hgc);
482 assert(hgc);
469 return hgc->pid;
483 return hgc->pid;
470 }
484 }
471
485
472 /*!
486 /*!
473 * Send command line arguments to let the server load the repo config and check
487 * Send command line arguments to let the server load the repo config and check
474 * whether it can process our request directly or not.
488 * whether it can process our request directly or not.
475 * Make sure hgc_setenv is called before calling this.
489 * Make sure hgc_setenv is called before calling this.
476 *
490 *
477 * @return - NULL, the server believes it can handle our request, or does not
491 * @return - NULL, the server believes it can handle our request, or does not
478 * support "validate" command.
492 * support "validate" command.
479 * - a list of strings, the server probably cannot handle our request
493 * - a list of strings, the server probably cannot handle our request
480 * and it sent instructions telling us what to do next. See
494 * and it sent instructions telling us what to do next. See
481 * chgserver.py for possible instruction formats.
495 * chgserver.py for possible instruction formats.
482 * the list should be freed by the caller.
496 * the list should be freed by the caller.
483 * the last string is guaranteed to be NULL.
497 * the last string is guaranteed to be NULL.
484 */
498 */
485 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
499 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
486 size_t argsize)
500 size_t argsize)
487 {
501 {
488 assert(hgc);
502 assert(hgc);
489 if (!(hgc->capflags & CAP_VALIDATE))
503 if (!(hgc->capflags & CAP_VALIDATE))
490 return NULL;
504 return NULL;
491
505
492 packcmdargs(&hgc->ctx, args, argsize);
506 packcmdargs(&hgc->ctx, args, argsize);
493 writeblockrequest(hgc, "validate");
507 writeblockrequest(hgc, "validate");
494 handleresponse(hgc);
508 handleresponse(hgc);
495
509
496 /* the server returns '\0' if it can handle our request */
510 /* the server returns '\0' if it can handle our request */
497 if (hgc->ctx.datasize <= 1)
511 if (hgc->ctx.datasize <= 1)
498 return NULL;
512 return NULL;
499
513
500 /* make sure the buffer is '\0' terminated */
514 /* make sure the buffer is '\0' terminated */
501 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
515 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
502 hgc->ctx.data[hgc->ctx.datasize] = '\0';
516 hgc->ctx.data[hgc->ctx.datasize] = '\0';
503 return unpackcmdargsnul(&hgc->ctx);
517 return unpackcmdargsnul(&hgc->ctx);
504 }
518 }
505
519
506 /*!
520 /*!
507 * Execute the specified Mercurial command
521 * Execute the specified Mercurial command
508 *
522 *
509 * @return result code
523 * @return result code
510 */
524 */
511 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize)
525 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize)
512 {
526 {
513 assert(hgc);
527 assert(hgc);
514
528
515 packcmdargs(&hgc->ctx, args, argsize);
529 packcmdargs(&hgc->ctx, args, argsize);
516 writeblockrequest(hgc, "runcommand");
530 writeblockrequest(hgc, "runcommand");
517 handleresponse(hgc);
531 handleresponse(hgc);
518
532
519 int32_t exitcode_n;
533 int32_t exitcode_n;
520 if (hgc->ctx.datasize != sizeof(exitcode_n)) {
534 if (hgc->ctx.datasize != sizeof(exitcode_n)) {
521 abortmsg("unexpected size of exitcode");
535 abortmsg("unexpected size of exitcode");
522 }
536 }
523 memcpy(&exitcode_n, hgc->ctx.data, sizeof(exitcode_n));
537 memcpy(&exitcode_n, hgc->ctx.data, sizeof(exitcode_n));
524 return ntohl(exitcode_n);
538 return ntohl(exitcode_n);
525 }
539 }
526
540
527 /*!
541 /*!
528 * (Re-)send client's stdio channels so that the server can access to tty
542 * (Re-)send client's stdio channels so that the server can access to tty
529 */
543 */
530 void hgc_attachio(hgclient_t *hgc)
544 void hgc_attachio(hgclient_t *hgc)
531 {
545 {
532 assert(hgc);
546 assert(hgc);
533 if (!(hgc->capflags & CAP_ATTACHIO))
547 if (!(hgc->capflags & CAP_ATTACHIO))
534 return;
548 return;
535 attachio(hgc);
549 attachio(hgc);
536 }
550 }
537
551
538 /*!
552 /*!
539 * Get pager command for the given Mercurial command args
553 * Get pager command for the given Mercurial command args
540 *
554 *
541 * If no pager enabled, returns NULL. The return value becomes invalid
555 * If no pager enabled, returns NULL. The return value becomes invalid
542 * once you run another request to hgc.
556 * once you run another request to hgc.
543 */
557 */
544 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
558 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
545 size_t argsize)
559 size_t argsize)
546 {
560 {
547 assert(hgc);
561 assert(hgc);
548
562
549 if (!(hgc->capflags & CAP_GETPAGER))
563 if (!(hgc->capflags & CAP_GETPAGER))
550 return NULL;
564 return NULL;
551
565
552 packcmdargs(&hgc->ctx, args, argsize);
566 packcmdargs(&hgc->ctx, args, argsize);
553 writeblockrequest(hgc, "getpager");
567 writeblockrequest(hgc, "getpager");
554 handleresponse(hgc);
568 handleresponse(hgc);
555
569
556 if (hgc->ctx.datasize < 1 || hgc->ctx.data[0] == '\0')
570 if (hgc->ctx.datasize < 1 || hgc->ctx.data[0] == '\0')
557 return NULL;
571 return NULL;
558 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
572 enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1);
559 hgc->ctx.data[hgc->ctx.datasize] = '\0';
573 hgc->ctx.data[hgc->ctx.datasize] = '\0';
560 return hgc->ctx.data;
574 return hgc->ctx.data;
561 }
575 }
562
576
563 /*!
577 /*!
564 * Update server's environment variables
578 * Update server's environment variables
565 *
579 *
566 * @param envp list of environment variables in "NAME=VALUE" format,
580 * @param envp list of environment variables in "NAME=VALUE" format,
567 * terminated by NULL.
581 * terminated by NULL.
568 */
582 */
569 void hgc_setenv(hgclient_t *hgc, const char *const envp[])
583 void hgc_setenv(hgclient_t *hgc, const char *const envp[])
570 {
584 {
571 assert(hgc && envp);
585 assert(hgc && envp);
572 if (!(hgc->capflags & CAP_SETENV))
586 if (!(hgc->capflags & CAP_SETENV))
573 return;
587 return;
574 packcmdargs(&hgc->ctx, envp, /*argsize*/ -1);
588 packcmdargs(&hgc->ctx, envp, /*argsize*/ -1);
575 writeblockrequest(hgc, "setenv");
589 writeblockrequest(hgc, "setenv");
576 }
590 }
@@ -1,31 +1,32
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 #ifndef HGCLIENT_H_
10 #ifndef HGCLIENT_H_
11 #define HGCLIENT_H_
11 #define HGCLIENT_H_
12
12
13 #include <sys/types.h>
13 #include <sys/types.h>
14
14
15 struct hgclient_tag_;
15 struct hgclient_tag_;
16 typedef struct hgclient_tag_ hgclient_t;
16 typedef struct hgclient_tag_ hgclient_t;
17
17
18 hgclient_t *hgc_open(const char *sockname);
18 hgclient_t *hgc_open(const char *sockname);
19 void hgc_close(hgclient_t *hgc);
19 void hgc_close(hgclient_t *hgc);
20
20
21 pid_t hgc_peerpgid(const hgclient_t *hgc);
21 pid_t hgc_peerpid(const hgclient_t *hgc);
22 pid_t hgc_peerpid(const hgclient_t *hgc);
22
23
23 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
24 const char **hgc_validate(hgclient_t *hgc, const char *const args[],
24 size_t argsize);
25 size_t argsize);
25 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize);
26 int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize);
26 void hgc_attachio(hgclient_t *hgc);
27 void hgc_attachio(hgclient_t *hgc);
27 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
28 const char *hgc_getpager(hgclient_t *hgc, const char *const args[],
28 size_t argsize);
29 size_t argsize);
29 void hgc_setenv(hgclient_t *hgc, const char *const envp[]);
30 void hgc_setenv(hgclient_t *hgc, const char *const envp[]);
30
31
31 #endif /* HGCLIENT_H_ */
32 #endif /* HGCLIENT_H_ */
@@ -1,33 +1,35
1 /*
1 /*
2 * Utility functions
2 * Utility functions
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 #ifndef UTIL_H_
10 #ifndef UTIL_H_
11 #define UTIL_H_
11 #define UTIL_H_
12
12
13 #ifdef __GNUC__
13 #ifdef __GNUC__
14 #define PRINTF_FORMAT_ __attribute__((format(printf, 1, 2)))
14 #define PRINTF_FORMAT_ __attribute__((format(printf, 1, 2)))
15 #define UNUSED_ __attribute__((unused))
15 #else
16 #else
16 #define PRINTF_FORMAT_
17 #define PRINTF_FORMAT_
18 #define UNUSED_
17 #endif
19 #endif
18
20
19 void abortmsg(const char *fmt, ...) PRINTF_FORMAT_;
21 void abortmsg(const char *fmt, ...) PRINTF_FORMAT_;
20 void abortmsgerrno(const char *fmt, ...) PRINTF_FORMAT_;
22 void abortmsgerrno(const char *fmt, ...) PRINTF_FORMAT_;
21
23
22 void enablecolor(void);
24 void enablecolor(void);
23 void enabledebugmsg(void);
25 void enabledebugmsg(void);
24 void debugmsg(const char *fmt, ...) PRINTF_FORMAT_;
26 void debugmsg(const char *fmt, ...) PRINTF_FORMAT_;
25
27
26 void fchdirx(int dirfd);
28 void fchdirx(int dirfd);
27 void fsetcloexec(int fd);
29 void fsetcloexec(int fd);
28 void *mallocx(size_t size);
30 void *mallocx(size_t size);
29 void *reallocx(void *ptr, size_t size);
31 void *reallocx(void *ptr, size_t size);
30
32
31 int runshellcmd(const char *cmd, const char *envp[], const char *cwd);
33 int runshellcmd(const char *cmd, const char *envp[], const char *cwd);
32
34
33 #endif /* UTIL_H_ */
35 #endif /* UTIL_H_ */
@@ -1,59 +1,59
1 # debugshell extension
1 # debugshell extension
2 """a python shell with repo, changelog & manifest objects"""
2 """a python shell with repo, changelog & manifest objects"""
3
3
4 from __future__ import absolute_import
4 from __future__ import absolute_import
5 import code
5 import code
6 import mercurial
6 import mercurial
7 import sys
7 import sys
8 from mercurial import (
8 from mercurial import (
9 cmdutil,
9 cmdutil,
10 demandimport,
10 demandimport,
11 )
11 )
12
12
13 cmdtable = {}
13 cmdtable = {}
14 command = cmdutil.command(cmdtable)
14 command = cmdutil.command(cmdtable)
15
15
16 def pdb(ui, repo, msg, **opts):
16 def pdb(ui, repo, msg, **opts):
17 objects = {
17 objects = {
18 'mercurial': mercurial,
18 'mercurial': mercurial,
19 'repo': repo,
19 'repo': repo,
20 'cl': repo.changelog,
20 'cl': repo.changelog,
21 'mf': repo.manifest,
21 'mf': repo.manifest,
22 }
22 }
23
23
24 code.interact(msg, local=objects)
24 code.interact(msg, local=objects)
25
25
26 def ipdb(ui, repo, msg, **opts):
26 def ipdb(ui, repo, msg, **opts):
27 import IPython
27 import IPython
28
28
29 cl = repo.changelog
29 cl = repo.changelog
30 mf = repo.manifest
30 mf = repo.manifest
31 cl, mf # use variables to appease pyflakes
31 cl, mf # use variables to appease pyflakes
32
32
33 IPython.embed()
33 IPython.embed()
34
34
35 @command('debugshell|dbsh', [])
35 @command('debugshell|dbsh', [])
36 def debugshell(ui, repo, **opts):
36 def debugshell(ui, repo, **opts):
37 bannermsg = "loaded repo : %s\n" \
37 bannermsg = "loaded repo : %s\n" \
38 "using source: %s" % (repo.root,
38 "using source: %s" % (repo.root,
39 mercurial.__path__[0])
39 mercurial.__path__[0])
40
40
41 pdbmap = {
41 pdbmap = {
42 'pdb' : 'code',
42 'pdb' : 'code',
43 'ipdb' : 'IPython'
43 'ipdb' : 'IPython'
44 }
44 }
45
45
46 debugger = ui.config("ui", "debugger")
46 debugger = ui.config("ui", "debugger")
47 if not debugger:
47 if not debugger:
48 debugger = 'pdb'
48 debugger = 'pdb'
49
49
50 # if IPython doesn't exist, fallback to code.interact
50 # if IPython doesn't exist, fallback to code.interact
51 try:
51 try:
52 with demandimport.deactivated():
52 with demandimport.deactivated():
53 __import__(pdbmap[debugger])
53 __import__(pdbmap[debugger])
54 except ImportError:
54 except ImportError:
55 ui.warn("%s debugger specified but %s module was not found\n"
55 ui.warn(("%s debugger specified but %s module was not found\n")
56 % (debugger, pdbmap[debugger]))
56 % (debugger, pdbmap[debugger]))
57 debugger = 'pdb'
57 debugger = 'pdb'
58
58
59 getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
59 getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
@@ -1,57 +1,57
1 # dirstatenonnormalcheck.py - extension to check the consistency of the
1 # dirstatenonnormalcheck.py - extension to check the consistency of the
2 # dirstate's non-normal map
2 # dirstate's non-normal map
3 #
3 #
4 # For most operations on dirstate, this extensions checks that the nonnormalset
4 # For most operations on dirstate, this extensions checks that the nonnormalset
5 # contains the right entries.
5 # contains the right entries.
6 # It compares the nonnormal file to a nonnormalset built from the map of all
6 # It compares the nonnormal file to a nonnormalset built from the map of all
7 # the files in the dirstate to check that they contain the same files.
7 # the files in the dirstate to check that they contain the same files.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 from mercurial import (
11 from mercurial import (
12 dirstate,
12 dirstate,
13 extensions,
13 extensions,
14 )
14 )
15
15
16 def nonnormalentries(dmap):
16 def nonnormalentries(dmap):
17 """Compute nonnormal entries from dirstate's dmap"""
17 """Compute nonnormal entries from dirstate's dmap"""
18 res = set()
18 res = set()
19 for f, e in dmap.iteritems():
19 for f, e in dmap.iteritems():
20 if e[0] != 'n' or e[3] == -1:
20 if e[0] != 'n' or e[3] == -1:
21 res.add(f)
21 res.add(f)
22 return res
22 return res
23
23
24 def checkconsistency(ui, orig, dmap, _nonnormalset, label):
24 def checkconsistency(ui, orig, dmap, _nonnormalset, label):
25 """Compute nonnormalset from dmap, check that it matches _nonnormalset"""
25 """Compute nonnormalset from dmap, check that it matches _nonnormalset"""
26 nonnormalcomputedmap = nonnormalentries(dmap)
26 nonnormalcomputedmap = nonnormalentries(dmap)
27 if _nonnormalset != nonnormalcomputedmap:
27 if _nonnormalset != nonnormalcomputedmap:
28 ui.develwarn("%s call to %s\n" % (label, orig))
28 ui.develwarn("%s call to %s\n" % (label, orig), config='dirstate')
29 ui.develwarn("inconsistency in nonnormalset\n")
29 ui.develwarn("inconsistency in nonnormalset\n", config='dirstate')
30 ui.develwarn("[nonnormalset] %s\n" % _nonnormalset)
30 ui.develwarn("[nonnormalset] %s\n" % _nonnormalset, config='dirstate')
31 ui.develwarn("[map] %s\n" % nonnormalcomputedmap)
31 ui.develwarn("[map] %s\n" % nonnormalcomputedmap, config='dirstate')
32
32
33 def _checkdirstate(orig, self, arg):
33 def _checkdirstate(orig, self, arg):
34 """Check nonnormal set consistency before and after the call to orig"""
34 """Check nonnormal set consistency before and after the call to orig"""
35 checkconsistency(self._ui, orig, self._map, self._nonnormalset, "before")
35 checkconsistency(self._ui, orig, self._map, self._nonnormalset, "before")
36 r = orig(self, arg)
36 r = orig(self, arg)
37 checkconsistency(self._ui, orig, self._map, self._nonnormalset, "after")
37 checkconsistency(self._ui, orig, self._map, self._nonnormalset, "after")
38 return r
38 return r
39
39
40 def extsetup(ui):
40 def extsetup(ui):
41 """Wrap functions modifying dirstate to check nonnormalset consistency"""
41 """Wrap functions modifying dirstate to check nonnormalset consistency"""
42 dirstatecl = dirstate.dirstate
42 dirstatecl = dirstate.dirstate
43 devel = ui.configbool('devel', 'all-warnings')
43 devel = ui.configbool('devel', 'all-warnings')
44 paranoid = ui.configbool('experimental', 'nonnormalparanoidcheck')
44 paranoid = ui.configbool('experimental', 'nonnormalparanoidcheck')
45 if devel:
45 if devel:
46 extensions.wrapfunction(dirstatecl, '_writedirstate', _checkdirstate)
46 extensions.wrapfunction(dirstatecl, '_writedirstate', _checkdirstate)
47 if paranoid:
47 if paranoid:
48 # We don't do all these checks when paranoid is disable as it would
48 # We don't do all these checks when paranoid is disable as it would
49 # make the extension run very slowly on large repos
49 # make the extension run very slowly on large repos
50 extensions.wrapfunction(dirstatecl, 'normallookup', _checkdirstate)
50 extensions.wrapfunction(dirstatecl, 'normallookup', _checkdirstate)
51 extensions.wrapfunction(dirstatecl, 'otherparent', _checkdirstate)
51 extensions.wrapfunction(dirstatecl, 'otherparent', _checkdirstate)
52 extensions.wrapfunction(dirstatecl, 'normal', _checkdirstate)
52 extensions.wrapfunction(dirstatecl, 'normal', _checkdirstate)
53 extensions.wrapfunction(dirstatecl, 'write', _checkdirstate)
53 extensions.wrapfunction(dirstatecl, 'write', _checkdirstate)
54 extensions.wrapfunction(dirstatecl, 'add', _checkdirstate)
54 extensions.wrapfunction(dirstatecl, 'add', _checkdirstate)
55 extensions.wrapfunction(dirstatecl, 'remove', _checkdirstate)
55 extensions.wrapfunction(dirstatecl, 'remove', _checkdirstate)
56 extensions.wrapfunction(dirstatecl, 'merge', _checkdirstate)
56 extensions.wrapfunction(dirstatecl, 'merge', _checkdirstate)
57 extensions.wrapfunction(dirstatecl, 'drop', _checkdirstate)
57 extensions.wrapfunction(dirstatecl, 'drop', _checkdirstate)
@@ -1,25 +1,31
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # Dump revlogs as raw data stream
2 # Dump revlogs as raw data stream
3 # $ find .hg/store/ -name "*.i" | xargs dumprevlog > repo.dump
3 # $ find .hg/store/ -name "*.i" | xargs dumprevlog > repo.dump
4
4
5 from __future__ import absolute_import, print_function
6
5 import sys
7 import sys
6 from mercurial import revlog, node, util
8 from mercurial import (
9 node,
10 revlog,
11 util,
12 )
7
13
8 for fp in (sys.stdin, sys.stdout, sys.stderr):
14 for fp in (sys.stdin, sys.stdout, sys.stderr):
9 util.setbinary(fp)
15 util.setbinary(fp)
10
16
11 for f in sys.argv[1:]:
17 for f in sys.argv[1:]:
12 binopen = lambda fn: open(fn, 'rb')
18 binopen = lambda fn: open(fn, 'rb')
13 r = revlog.revlog(binopen, f)
19 r = revlog.revlog(binopen, f)
14 print "file:", f
20 print("file:", f)
15 for i in r:
21 for i in r:
16 n = r.node(i)
22 n = r.node(i)
17 p = r.parents(n)
23 p = r.parents(n)
18 d = r.revision(n)
24 d = r.revision(n)
19 print "node:", node.hex(n)
25 print("node:", node.hex(n))
20 print "linkrev:", r.linkrev(i)
26 print("linkrev:", r.linkrev(i))
21 print "parents:", node.hex(p[0]), node.hex(p[1])
27 print("parents:", node.hex(p[0]), node.hex(p[1]))
22 print "length:", len(d)
28 print("length:", len(d))
23 print "-start-"
29 print("-start-")
24 print d
30 print(d)
25 print "-end-"
31 print("-end-")
@@ -1,706 +1,722
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import ast
5 import ast
6 import collections
6 import collections
7 import os
7 import os
8 import re
8 import re
9 import sys
9 import sys
10
10
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
12 # to work when run from a virtualenv. The modules were chosen empirically
12 # to work when run from a virtualenv. The modules were chosen empirically
13 # so that the return value matches the return value without virtualenv.
13 # so that the return value matches the return value without virtualenv.
14 import BaseHTTPServer
14 if True: # disable lexical sorting checks
15 import zlib
15 import BaseHTTPServer
16 import zlib
16
17
17 # Whitelist of modules that symbols can be directly imported from.
18 # Whitelist of modules that symbols can be directly imported from.
18 allowsymbolimports = (
19 allowsymbolimports = (
19 '__future__',
20 '__future__',
20 'mercurial.hgweb.common',
21 'mercurial.hgweb.common',
21 'mercurial.hgweb.request',
22 'mercurial.hgweb.request',
22 'mercurial.i18n',
23 'mercurial.i18n',
23 'mercurial.node',
24 'mercurial.node',
24 )
25 )
25
26
26 # Modules that must be aliased because they are commonly confused with
27 # Modules that must be aliased because they are commonly confused with
27 # common variables and can create aliasing and readability issues.
28 # common variables and can create aliasing and readability issues.
28 requirealias = {
29 requirealias = {
29 'ui': 'uimod',
30 'ui': 'uimod',
30 }
31 }
31
32
32 def usingabsolute(root):
33 def usingabsolute(root):
33 """Whether absolute imports are being used."""
34 """Whether absolute imports are being used."""
34 if sys.version_info[0] >= 3:
35 if sys.version_info[0] >= 3:
35 return True
36 return True
36
37
37 for node in ast.walk(root):
38 for node in ast.walk(root):
38 if isinstance(node, ast.ImportFrom):
39 if isinstance(node, ast.ImportFrom):
39 if node.module == '__future__':
40 if node.module == '__future__':
40 for n in node.names:
41 for n in node.names:
41 if n.name == 'absolute_import':
42 if n.name == 'absolute_import':
42 return True
43 return True
43
44
44 return False
45 return False
45
46
46 def walklocal(root):
47 def walklocal(root):
47 """Recursively yield all descendant nodes but not in a different scope"""
48 """Recursively yield all descendant nodes but not in a different scope"""
48 todo = collections.deque(ast.iter_child_nodes(root))
49 todo = collections.deque(ast.iter_child_nodes(root))
49 yield root, False
50 yield root, False
50 while todo:
51 while todo:
51 node = todo.popleft()
52 node = todo.popleft()
52 newscope = isinstance(node, ast.FunctionDef)
53 newscope = isinstance(node, ast.FunctionDef)
53 if not newscope:
54 if not newscope:
54 todo.extend(ast.iter_child_nodes(node))
55 todo.extend(ast.iter_child_nodes(node))
55 yield node, newscope
56 yield node, newscope
56
57
57 def dotted_name_of_path(path, trimpure=False):
58 def dotted_name_of_path(path, trimpure=False):
58 """Given a relative path to a source file, return its dotted module name.
59 """Given a relative path to a source file, return its dotted module name.
59
60
60 >>> dotted_name_of_path('mercurial/error.py')
61 >>> dotted_name_of_path('mercurial/error.py')
61 'mercurial.error'
62 'mercurial.error'
62 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
63 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
63 'mercurial.parsers'
64 'mercurial.parsers'
64 >>> dotted_name_of_path('zlibmodule.so')
65 >>> dotted_name_of_path('zlibmodule.so')
65 'zlib'
66 'zlib'
66 """
67 """
67 parts = path.replace(os.sep, '/').split('/')
68 parts = path.replace(os.sep, '/').split('/')
68 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
69 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
69 if parts[-1].endswith('module'):
70 if parts[-1].endswith('module'):
70 parts[-1] = parts[-1][:-6]
71 parts[-1] = parts[-1][:-6]
71 if trimpure:
72 if trimpure:
72 return '.'.join(p for p in parts if p != 'pure')
73 return '.'.join(p for p in parts if p != 'pure')
73 return '.'.join(parts)
74 return '.'.join(parts)
74
75
75 def fromlocalfunc(modulename, localmods):
76 def fromlocalfunc(modulename, localmods):
76 """Get a function to examine which locally defined module the
77 """Get a function to examine which locally defined module the
77 target source imports via a specified name.
78 target source imports via a specified name.
78
79
79 `modulename` is an `dotted_name_of_path()`-ed source file path,
80 `modulename` is an `dotted_name_of_path()`-ed source file path,
80 which may have `.__init__` at the end of it, of the target source.
81 which may have `.__init__` at the end of it, of the target source.
81
82
82 `localmods` is a dict (or set), of which key is an absolute
83 `localmods` is a dict (or set), of which key is an absolute
83 `dotted_name_of_path()`-ed source file path of locally defined (=
84 `dotted_name_of_path()`-ed source file path of locally defined (=
84 Mercurial specific) modules.
85 Mercurial specific) modules.
85
86
86 This function assumes that module names not existing in
87 This function assumes that module names not existing in
87 `localmods` are from the Python standard library.
88 `localmods` are from the Python standard library.
88
89
89 This function returns the function, which takes `name` argument,
90 This function returns the function, which takes `name` argument,
90 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
91 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
91 matches against locally defined module. Otherwise, it returns
92 matches against locally defined module. Otherwise, it returns
92 False.
93 False.
93
94
94 It is assumed that `name` doesn't have `.__init__`.
95 It is assumed that `name` doesn't have `.__init__`.
95
96
96 `absname` is an absolute module name of specified `name`
97 `absname` is an absolute module name of specified `name`
97 (e.g. "hgext.convert"). This can be used to compose prefix for sub
98 (e.g. "hgext.convert"). This can be used to compose prefix for sub
98 modules or so.
99 modules or so.
99
100
100 `dottedpath` is a `dotted_name_of_path()`-ed source file path
101 `dottedpath` is a `dotted_name_of_path()`-ed source file path
101 (e.g. "hgext.convert.__init__") of `name`. This is used to look
102 (e.g. "hgext.convert.__init__") of `name`. This is used to look
102 module up in `localmods` again.
103 module up in `localmods` again.
103
104
104 `hassubmod` is whether it may have sub modules under it (for
105 `hassubmod` is whether it may have sub modules under it (for
105 convenient, even though this is also equivalent to "absname !=
106 convenient, even though this is also equivalent to "absname !=
106 dottednpath")
107 dottednpath")
107
108
108 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
109 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
109 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
110 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
110 ... 'baz.__init__': True, 'baz.baz1': True }
111 ... 'baz.__init__': True, 'baz.baz1': True }
111 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
112 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
112 >>> # relative
113 >>> # relative
113 >>> fromlocal('foo1')
114 >>> fromlocal('foo1')
114 ('foo.foo1', 'foo.foo1', False)
115 ('foo.foo1', 'foo.foo1', False)
115 >>> fromlocal('bar')
116 >>> fromlocal('bar')
116 ('foo.bar', 'foo.bar.__init__', True)
117 ('foo.bar', 'foo.bar.__init__', True)
117 >>> fromlocal('bar.bar1')
118 >>> fromlocal('bar.bar1')
118 ('foo.bar.bar1', 'foo.bar.bar1', False)
119 ('foo.bar.bar1', 'foo.bar.bar1', False)
119 >>> # absolute
120 >>> # absolute
120 >>> fromlocal('baz')
121 >>> fromlocal('baz')
121 ('baz', 'baz.__init__', True)
122 ('baz', 'baz.__init__', True)
122 >>> fromlocal('baz.baz1')
123 >>> fromlocal('baz.baz1')
123 ('baz.baz1', 'baz.baz1', False)
124 ('baz.baz1', 'baz.baz1', False)
124 >>> # unknown = maybe standard library
125 >>> # unknown = maybe standard library
125 >>> fromlocal('os')
126 >>> fromlocal('os')
126 False
127 False
127 >>> fromlocal(None, 1)
128 >>> fromlocal(None, 1)
128 ('foo', 'foo.__init__', True)
129 ('foo', 'foo.__init__', True)
130 >>> fromlocal('foo1', 1)
131 ('foo.foo1', 'foo.foo1', False)
129 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
132 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
130 >>> fromlocal2(None, 2)
133 >>> fromlocal2(None, 2)
131 ('foo', 'foo.__init__', True)
134 ('foo', 'foo.__init__', True)
135 >>> fromlocal2('bar2', 1)
136 False
137 >>> fromlocal2('bar', 2)
138 ('foo.bar', 'foo.bar.__init__', True)
132 """
139 """
133 prefix = '.'.join(modulename.split('.')[:-1])
140 prefix = '.'.join(modulename.split('.')[:-1])
134 if prefix:
141 if prefix:
135 prefix += '.'
142 prefix += '.'
136 def fromlocal(name, level=0):
143 def fromlocal(name, level=0):
137 # name is None when relative imports are used.
144 # name is false value when relative imports are used.
138 if name is None:
145 if not name:
139 # If relative imports are used, level must not be absolute.
146 # If relative imports are used, level must not be absolute.
140 assert level > 0
147 assert level > 0
141 candidates = ['.'.join(modulename.split('.')[:-level])]
148 candidates = ['.'.join(modulename.split('.')[:-level])]
142 else:
149 else:
143 # Check relative name first.
150 if not level:
144 candidates = [prefix + name, name]
151 # Check relative name first.
152 candidates = [prefix + name, name]
153 else:
154 candidates = ['.'.join(modulename.split('.')[:-level]) +
155 '.' + name]
145
156
146 for n in candidates:
157 for n in candidates:
147 if n in localmods:
158 if n in localmods:
148 return (n, n, False)
159 return (n, n, False)
149 dottedpath = n + '.__init__'
160 dottedpath = n + '.__init__'
150 if dottedpath in localmods:
161 if dottedpath in localmods:
151 return (n, dottedpath, True)
162 return (n, dottedpath, True)
152 return False
163 return False
153 return fromlocal
164 return fromlocal
154
165
155 def list_stdlib_modules():
166 def list_stdlib_modules():
156 """List the modules present in the stdlib.
167 """List the modules present in the stdlib.
157
168
158 >>> mods = set(list_stdlib_modules())
169 >>> mods = set(list_stdlib_modules())
159 >>> 'BaseHTTPServer' in mods
170 >>> 'BaseHTTPServer' in mods
160 True
171 True
161
172
162 os.path isn't really a module, so it's missing:
173 os.path isn't really a module, so it's missing:
163
174
164 >>> 'os.path' in mods
175 >>> 'os.path' in mods
165 False
176 False
166
177
167 sys requires special treatment, because it's baked into the
178 sys requires special treatment, because it's baked into the
168 interpreter, but it should still appear:
179 interpreter, but it should still appear:
169
180
170 >>> 'sys' in mods
181 >>> 'sys' in mods
171 True
182 True
172
183
173 >>> 'collections' in mods
184 >>> 'collections' in mods
174 True
185 True
175
186
176 >>> 'cStringIO' in mods
187 >>> 'cStringIO' in mods
177 True
188 True
189
190 >>> 'cffi' in mods
191 True
178 """
192 """
179 for m in sys.builtin_module_names:
193 for m in sys.builtin_module_names:
180 yield m
194 yield m
181 # These modules only exist on windows, but we should always
195 # These modules only exist on windows, but we should always
182 # consider them stdlib.
196 # consider them stdlib.
183 for m in ['msvcrt', '_winreg']:
197 for m in ['msvcrt', '_winreg']:
184 yield m
198 yield m
185 yield 'builtins' # python3 only
199 yield 'builtins' # python3 only
186 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
200 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
187 yield m
201 yield m
188 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
202 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
189 yield m
203 yield m
204 for m in ['cffi']:
205 yield m
190 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
206 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
191 # We need to supplement the list of prefixes for the search to work
207 # We need to supplement the list of prefixes for the search to work
192 # when run from within a virtualenv.
208 # when run from within a virtualenv.
193 for mod in (BaseHTTPServer, zlib):
209 for mod in (BaseHTTPServer, zlib):
194 try:
210 try:
195 # Not all module objects have a __file__ attribute.
211 # Not all module objects have a __file__ attribute.
196 filename = mod.__file__
212 filename = mod.__file__
197 except AttributeError:
213 except AttributeError:
198 continue
214 continue
199 dirname = os.path.dirname(filename)
215 dirname = os.path.dirname(filename)
200 for prefix in stdlib_prefixes:
216 for prefix in stdlib_prefixes:
201 if dirname.startswith(prefix):
217 if dirname.startswith(prefix):
202 # Then this directory is redundant.
218 # Then this directory is redundant.
203 break
219 break
204 else:
220 else:
205 stdlib_prefixes.add(dirname)
221 stdlib_prefixes.add(dirname)
206 for libpath in sys.path:
222 for libpath in sys.path:
207 # We want to walk everything in sys.path that starts with
223 # We want to walk everything in sys.path that starts with
208 # something in stdlib_prefixes.
224 # something in stdlib_prefixes.
209 if not any(libpath.startswith(p) for p in stdlib_prefixes):
225 if not any(libpath.startswith(p) for p in stdlib_prefixes):
210 continue
226 continue
211 for top, dirs, files in os.walk(libpath):
227 for top, dirs, files in os.walk(libpath):
212 for i, d in reversed(list(enumerate(dirs))):
228 for i, d in reversed(list(enumerate(dirs))):
213 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
229 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
214 or top == libpath and d in ('hgext', 'mercurial')):
230 or top == libpath and d in ('hgext', 'mercurial')):
215 del dirs[i]
231 del dirs[i]
216 for name in files:
232 for name in files:
217 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
233 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
218 continue
234 continue
219 if name.startswith('__init__.py'):
235 if name.startswith('__init__.py'):
220 full_path = top
236 full_path = top
221 else:
237 else:
222 full_path = os.path.join(top, name)
238 full_path = os.path.join(top, name)
223 rel_path = full_path[len(libpath) + 1:]
239 rel_path = full_path[len(libpath) + 1:]
224 mod = dotted_name_of_path(rel_path)
240 mod = dotted_name_of_path(rel_path)
225 yield mod
241 yield mod
226
242
227 stdlib_modules = set(list_stdlib_modules())
243 stdlib_modules = set(list_stdlib_modules())
228
244
229 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
245 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
230 """Given the source of a file as a string, yield the names
246 """Given the source of a file as a string, yield the names
231 imported by that file.
247 imported by that file.
232
248
233 Args:
249 Args:
234 source: The python source to examine as a string.
250 source: The python source to examine as a string.
235 modulename: of specified python source (may have `__init__`)
251 modulename: of specified python source (may have `__init__`)
236 localmods: dict of locally defined module names (may have `__init__`)
252 localmods: dict of locally defined module names (may have `__init__`)
237 ignore_nested: If true, import statements that do not start in
253 ignore_nested: If true, import statements that do not start in
238 column zero will be ignored.
254 column zero will be ignored.
239
255
240 Returns:
256 Returns:
241 A list of absolute module names imported by the given source.
257 A list of absolute module names imported by the given source.
242
258
243 >>> f = 'foo/xxx.py'
259 >>> f = 'foo/xxx.py'
244 >>> modulename = 'foo.xxx'
260 >>> modulename = 'foo.xxx'
245 >>> localmods = {'foo.__init__': True,
261 >>> localmods = {'foo.__init__': True,
246 ... 'foo.foo1': True, 'foo.foo2': True,
262 ... 'foo.foo1': True, 'foo.foo2': True,
247 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
263 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
248 ... 'baz.__init__': True, 'baz.baz1': True }
264 ... 'baz.__init__': True, 'baz.baz1': True }
249 >>> # standard library (= not locally defined ones)
265 >>> # standard library (= not locally defined ones)
250 >>> sorted(imported_modules(
266 >>> sorted(imported_modules(
251 ... 'from stdlib1 import foo, bar; import stdlib2',
267 ... 'from stdlib1 import foo, bar; import stdlib2',
252 ... modulename, f, localmods))
268 ... modulename, f, localmods))
253 []
269 []
254 >>> # relative importing
270 >>> # relative importing
255 >>> sorted(imported_modules(
271 >>> sorted(imported_modules(
256 ... 'import foo1; from bar import bar1',
272 ... 'import foo1; from bar import bar1',
257 ... modulename, f, localmods))
273 ... modulename, f, localmods))
258 ['foo.bar.bar1', 'foo.foo1']
274 ['foo.bar.bar1', 'foo.foo1']
259 >>> sorted(imported_modules(
275 >>> sorted(imported_modules(
260 ... 'from bar.bar1 import name1, name2, name3',
276 ... 'from bar.bar1 import name1, name2, name3',
261 ... modulename, f, localmods))
277 ... modulename, f, localmods))
262 ['foo.bar.bar1']
278 ['foo.bar.bar1']
263 >>> # absolute importing
279 >>> # absolute importing
264 >>> sorted(imported_modules(
280 >>> sorted(imported_modules(
265 ... 'from baz import baz1, name1',
281 ... 'from baz import baz1, name1',
266 ... modulename, f, localmods))
282 ... modulename, f, localmods))
267 ['baz.__init__', 'baz.baz1']
283 ['baz.__init__', 'baz.baz1']
268 >>> # mixed importing, even though it shouldn't be recommended
284 >>> # mixed importing, even though it shouldn't be recommended
269 >>> sorted(imported_modules(
285 >>> sorted(imported_modules(
270 ... 'import stdlib, foo1, baz',
286 ... 'import stdlib, foo1, baz',
271 ... modulename, f, localmods))
287 ... modulename, f, localmods))
272 ['baz.__init__', 'foo.foo1']
288 ['baz.__init__', 'foo.foo1']
273 >>> # ignore_nested
289 >>> # ignore_nested
274 >>> sorted(imported_modules(
290 >>> sorted(imported_modules(
275 ... '''import foo
291 ... '''import foo
276 ... def wat():
292 ... def wat():
277 ... import bar
293 ... import bar
278 ... ''', modulename, f, localmods))
294 ... ''', modulename, f, localmods))
279 ['foo.__init__', 'foo.bar.__init__']
295 ['foo.__init__', 'foo.bar.__init__']
280 >>> sorted(imported_modules(
296 >>> sorted(imported_modules(
281 ... '''import foo
297 ... '''import foo
282 ... def wat():
298 ... def wat():
283 ... import bar
299 ... import bar
284 ... ''', modulename, f, localmods, ignore_nested=True))
300 ... ''', modulename, f, localmods, ignore_nested=True))
285 ['foo.__init__']
301 ['foo.__init__']
286 """
302 """
287 fromlocal = fromlocalfunc(modulename, localmods)
303 fromlocal = fromlocalfunc(modulename, localmods)
288 for node in ast.walk(ast.parse(source, f)):
304 for node in ast.walk(ast.parse(source, f)):
289 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
305 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
290 continue
306 continue
291 if isinstance(node, ast.Import):
307 if isinstance(node, ast.Import):
292 for n in node.names:
308 for n in node.names:
293 found = fromlocal(n.name)
309 found = fromlocal(n.name)
294 if not found:
310 if not found:
295 # this should import standard library
311 # this should import standard library
296 continue
312 continue
297 yield found[1]
313 yield found[1]
298 elif isinstance(node, ast.ImportFrom):
314 elif isinstance(node, ast.ImportFrom):
299 found = fromlocal(node.module, node.level)
315 found = fromlocal(node.module, node.level)
300 if not found:
316 if not found:
301 # this should import standard library
317 # this should import standard library
302 continue
318 continue
303
319
304 absname, dottedpath, hassubmod = found
320 absname, dottedpath, hassubmod = found
305 if not hassubmod:
321 if not hassubmod:
306 # "dottedpath" is not a package; must be imported
322 # "dottedpath" is not a package; must be imported
307 yield dottedpath
323 yield dottedpath
308 # examination of "node.names" should be redundant
324 # examination of "node.names" should be redundant
309 # e.g.: from mercurial.node import nullid, nullrev
325 # e.g.: from mercurial.node import nullid, nullrev
310 continue
326 continue
311
327
312 modnotfound = False
328 modnotfound = False
313 prefix = absname + '.'
329 prefix = absname + '.'
314 for n in node.names:
330 for n in node.names:
315 found = fromlocal(prefix + n.name)
331 found = fromlocal(prefix + n.name)
316 if not found:
332 if not found:
317 # this should be a function or a property of "node.module"
333 # this should be a function or a property of "node.module"
318 modnotfound = True
334 modnotfound = True
319 continue
335 continue
320 yield found[1]
336 yield found[1]
321 if modnotfound:
337 if modnotfound:
322 # "dottedpath" is a package, but imported because of non-module
338 # "dottedpath" is a package, but imported because of non-module
323 # lookup
339 # lookup
324 yield dottedpath
340 yield dottedpath
325
341
326 def verify_import_convention(module, source, localmods):
342 def verify_import_convention(module, source, localmods):
327 """Verify imports match our established coding convention.
343 """Verify imports match our established coding convention.
328
344
329 We have 2 conventions: legacy and modern. The modern convention is in
345 We have 2 conventions: legacy and modern. The modern convention is in
330 effect when using absolute imports.
346 effect when using absolute imports.
331
347
332 The legacy convention only looks for mixed imports. The modern convention
348 The legacy convention only looks for mixed imports. The modern convention
333 is much more thorough.
349 is much more thorough.
334 """
350 """
335 root = ast.parse(source)
351 root = ast.parse(source)
336 absolute = usingabsolute(root)
352 absolute = usingabsolute(root)
337
353
338 if absolute:
354 if absolute:
339 return verify_modern_convention(module, root, localmods)
355 return verify_modern_convention(module, root, localmods)
340 else:
356 else:
341 return verify_stdlib_on_own_line(root)
357 return verify_stdlib_on_own_line(root)
342
358
343 def verify_modern_convention(module, root, localmods, root_col_offset=0):
359 def verify_modern_convention(module, root, localmods, root_col_offset=0):
344 """Verify a file conforms to the modern import convention rules.
360 """Verify a file conforms to the modern import convention rules.
345
361
346 The rules of the modern convention are:
362 The rules of the modern convention are:
347
363
348 * Ordering is stdlib followed by local imports. Each group is lexically
364 * Ordering is stdlib followed by local imports. Each group is lexically
349 sorted.
365 sorted.
350 * Importing multiple modules via "import X, Y" is not allowed: use
366 * Importing multiple modules via "import X, Y" is not allowed: use
351 separate import statements.
367 separate import statements.
352 * Importing multiple modules via "from X import ..." is allowed if using
368 * Importing multiple modules via "from X import ..." is allowed if using
353 parenthesis and one entry per line.
369 parenthesis and one entry per line.
354 * Only 1 relative import statement per import level ("from .", "from ..")
370 * Only 1 relative import statement per import level ("from .", "from ..")
355 is allowed.
371 is allowed.
356 * Relative imports from higher levels must occur before lower levels. e.g.
372 * Relative imports from higher levels must occur before lower levels. e.g.
357 "from .." must be before "from .".
373 "from .." must be before "from .".
358 * Imports from peer packages should use relative import (e.g. do not
374 * Imports from peer packages should use relative import (e.g. do not
359 "import mercurial.foo" from a "mercurial.*" module).
375 "import mercurial.foo" from a "mercurial.*" module).
360 * Symbols can only be imported from specific modules (see
376 * Symbols can only be imported from specific modules (see
361 `allowsymbolimports`). For other modules, first import the module then
377 `allowsymbolimports`). For other modules, first import the module then
362 assign the symbol to a module-level variable. In addition, these imports
378 assign the symbol to a module-level variable. In addition, these imports
363 must be performed before other relative imports. This rule only
379 must be performed before other local imports. This rule only
364 applies to import statements outside of any blocks.
380 applies to import statements outside of any blocks.
365 * Relative imports from the standard library are not allowed.
381 * Relative imports from the standard library are not allowed.
366 * Certain modules must be aliased to alternate names to avoid aliasing
382 * Certain modules must be aliased to alternate names to avoid aliasing
367 and readability problems. See `requirealias`.
383 and readability problems. See `requirealias`.
368 """
384 """
369 topmodule = module.split('.')[0]
385 topmodule = module.split('.')[0]
370 fromlocal = fromlocalfunc(module, localmods)
386 fromlocal = fromlocalfunc(module, localmods)
371
387
372 # Whether a local/non-stdlib import has been performed.
388 # Whether a local/non-stdlib import has been performed.
373 seenlocal = None
389 seenlocal = None
374 # Whether a relative, non-symbol import has been seen.
390 # Whether a local/non-stdlib, non-symbol import has been seen.
375 seennonsymbolrelative = False
391 seennonsymbollocal = False
376 # The last name to be imported (for sorting).
392 # The last name to be imported (for sorting).
377 lastname = None
393 lastname = None
378 # Relative import levels encountered so far.
394 # Relative import levels encountered so far.
379 seenlevels = set()
395 seenlevels = set()
380
396
381 for node, newscope in walklocal(root):
397 for node, newscope in walklocal(root):
382 def msg(fmt, *args):
398 def msg(fmt, *args):
383 return (fmt % args, node.lineno)
399 return (fmt % args, node.lineno)
384 if newscope:
400 if newscope:
385 # Check for local imports in function
401 # Check for local imports in function
386 for r in verify_modern_convention(module, node, localmods,
402 for r in verify_modern_convention(module, node, localmods,
387 node.col_offset + 4):
403 node.col_offset + 4):
388 yield r
404 yield r
389 elif isinstance(node, ast.Import):
405 elif isinstance(node, ast.Import):
390 # Disallow "import foo, bar" and require separate imports
406 # Disallow "import foo, bar" and require separate imports
391 # for each module.
407 # for each module.
392 if len(node.names) > 1:
408 if len(node.names) > 1:
393 yield msg('multiple imported names: %s',
409 yield msg('multiple imported names: %s',
394 ', '.join(n.name for n in node.names))
410 ', '.join(n.name for n in node.names))
395
411
396 name = node.names[0].name
412 name = node.names[0].name
397 asname = node.names[0].asname
413 asname = node.names[0].asname
398
414
399 # Ignore sorting rules on imports inside blocks.
415 # Ignore sorting rules on imports inside blocks.
400 if node.col_offset == root_col_offset:
416 if node.col_offset == root_col_offset:
401 if lastname and name < lastname:
417 if lastname and name < lastname:
402 yield msg('imports not lexically sorted: %s < %s',
418 yield msg('imports not lexically sorted: %s < %s',
403 name, lastname)
419 name, lastname)
404
420
405 lastname = name
421 lastname = name
406
422
407 # stdlib imports should be before local imports.
423 # stdlib imports should be before local imports.
408 stdlib = name in stdlib_modules
424 stdlib = name in stdlib_modules
409 if stdlib and seenlocal and node.col_offset == root_col_offset:
425 if stdlib and seenlocal and node.col_offset == root_col_offset:
410 yield msg('stdlib import "%s" follows local import: %s',
426 yield msg('stdlib import "%s" follows local import: %s',
411 name, seenlocal)
427 name, seenlocal)
412
428
413 if not stdlib:
429 if not stdlib:
414 seenlocal = name
430 seenlocal = name
415
431
416 # Import of sibling modules should use relative imports.
432 # Import of sibling modules should use relative imports.
417 topname = name.split('.')[0]
433 topname = name.split('.')[0]
418 if topname == topmodule:
434 if topname == topmodule:
419 yield msg('import should be relative: %s', name)
435 yield msg('import should be relative: %s', name)
420
436
421 if name in requirealias and asname != requirealias[name]:
437 if name in requirealias and asname != requirealias[name]:
422 yield msg('%s module must be "as" aliased to %s',
438 yield msg('%s module must be "as" aliased to %s',
423 name, requirealias[name])
439 name, requirealias[name])
424
440
425 elif isinstance(node, ast.ImportFrom):
441 elif isinstance(node, ast.ImportFrom):
426 # Resolve the full imported module name.
442 # Resolve the full imported module name.
427 if node.level > 0:
443 if node.level > 0:
428 fullname = '.'.join(module.split('.')[:-node.level])
444 fullname = '.'.join(module.split('.')[:-node.level])
429 if node.module:
445 if node.module:
430 fullname += '.%s' % node.module
446 fullname += '.%s' % node.module
431 else:
447 else:
432 assert node.module
448 assert node.module
433 fullname = node.module
449 fullname = node.module
434
450
435 topname = fullname.split('.')[0]
451 topname = fullname.split('.')[0]
436 if topname == topmodule:
452 if topname == topmodule:
437 yield msg('import should be relative: %s', fullname)
453 yield msg('import should be relative: %s', fullname)
438
454
439 # __future__ is special since it needs to come first and use
455 # __future__ is special since it needs to come first and use
440 # symbol import.
456 # symbol import.
441 if fullname != '__future__':
457 if fullname != '__future__':
442 if not fullname or fullname in stdlib_modules:
458 if not fullname or fullname in stdlib_modules:
443 yield msg('relative import of stdlib module')
459 yield msg('relative import of stdlib module')
444 else:
460 else:
445 seenlocal = fullname
461 seenlocal = fullname
446
462
447 # Direct symbol import is only allowed from certain modules and
463 # Direct symbol import is only allowed from certain modules and
448 # must occur before non-symbol imports.
464 # must occur before non-symbol imports.
465 found = fromlocal(node.module, node.level)
466 if found and found[2]: # node.module is a package
467 prefix = found[0] + '.'
468 symbols = [n.name for n in node.names
469 if not fromlocal(prefix + n.name)]
470 else:
471 symbols = [n.name for n in node.names]
449 if node.module and node.col_offset == root_col_offset:
472 if node.module and node.col_offset == root_col_offset:
450 found = fromlocal(node.module, node.level)
451 if found and found[2]: # node.module is a package
452 prefix = found[0] + '.'
453 symbols = [n.name for n in node.names
454 if not fromlocal(prefix + n.name)]
455 else:
456 symbols = [n.name for n in node.names]
457
458 if symbols and fullname not in allowsymbolimports:
473 if symbols and fullname not in allowsymbolimports:
459 yield msg('direct symbol import %s from %s',
474 yield msg('direct symbol import %s from %s',
460 ', '.join(symbols), fullname)
475 ', '.join(symbols), fullname)
461
476
462 if symbols and seennonsymbolrelative:
477 if symbols and seennonsymbollocal:
463 yield msg('symbol import follows non-symbol import: %s',
478 yield msg('symbol import follows non-symbol import: %s',
464 fullname)
479 fullname)
480 if not symbols and fullname not in stdlib_modules:
481 seennonsymbollocal = True
465
482
466 if not node.module:
483 if not node.module:
467 assert node.level
484 assert node.level
468 seennonsymbolrelative = True
469
485
470 # Only allow 1 group per level.
486 # Only allow 1 group per level.
471 if (node.level in seenlevels
487 if (node.level in seenlevels
472 and node.col_offset == root_col_offset):
488 and node.col_offset == root_col_offset):
473 yield msg('multiple "from %s import" statements',
489 yield msg('multiple "from %s import" statements',
474 '.' * node.level)
490 '.' * node.level)
475
491
476 # Higher-level groups come before lower-level groups.
492 # Higher-level groups come before lower-level groups.
477 if any(node.level > l for l in seenlevels):
493 if any(node.level > l for l in seenlevels):
478 yield msg('higher-level import should come first: %s',
494 yield msg('higher-level import should come first: %s',
479 fullname)
495 fullname)
480
496
481 seenlevels.add(node.level)
497 seenlevels.add(node.level)
482
498
483 # Entries in "from .X import ( ... )" lists must be lexically
499 # Entries in "from .X import ( ... )" lists must be lexically
484 # sorted.
500 # sorted.
485 lastentryname = None
501 lastentryname = None
486
502
487 for n in node.names:
503 for n in node.names:
488 if lastentryname and n.name < lastentryname:
504 if lastentryname and n.name < lastentryname:
489 yield msg('imports from %s not lexically sorted: %s < %s',
505 yield msg('imports from %s not lexically sorted: %s < %s',
490 fullname, n.name, lastentryname)
506 fullname, n.name, lastentryname)
491
507
492 lastentryname = n.name
508 lastentryname = n.name
493
509
494 if n.name in requirealias and n.asname != requirealias[n.name]:
510 if n.name in requirealias and n.asname != requirealias[n.name]:
495 yield msg('%s from %s must be "as" aliased to %s',
511 yield msg('%s from %s must be "as" aliased to %s',
496 n.name, fullname, requirealias[n.name])
512 n.name, fullname, requirealias[n.name])
497
513
498 def verify_stdlib_on_own_line(root):
514 def verify_stdlib_on_own_line(root):
499 """Given some python source, verify that stdlib imports are done
515 """Given some python source, verify that stdlib imports are done
500 in separate statements from relative local module imports.
516 in separate statements from relative local module imports.
501
517
502 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
518 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
503 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
519 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
504 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
520 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
505 []
521 []
506 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
522 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
507 []
523 []
508 """
524 """
509 for node in ast.walk(root):
525 for node in ast.walk(root):
510 if isinstance(node, ast.Import):
526 if isinstance(node, ast.Import):
511 from_stdlib = {False: [], True: []}
527 from_stdlib = {False: [], True: []}
512 for n in node.names:
528 for n in node.names:
513 from_stdlib[n.name in stdlib_modules].append(n.name)
529 from_stdlib[n.name in stdlib_modules].append(n.name)
514 if from_stdlib[True] and from_stdlib[False]:
530 if from_stdlib[True] and from_stdlib[False]:
515 yield ('mixed imports\n stdlib: %s\n relative: %s' %
531 yield ('mixed imports\n stdlib: %s\n relative: %s' %
516 (', '.join(sorted(from_stdlib[True])),
532 (', '.join(sorted(from_stdlib[True])),
517 ', '.join(sorted(from_stdlib[False]))), node.lineno)
533 ', '.join(sorted(from_stdlib[False]))), node.lineno)
518
534
519 class CircularImport(Exception):
535 class CircularImport(Exception):
520 pass
536 pass
521
537
522 def checkmod(mod, imports):
538 def checkmod(mod, imports):
523 shortest = {}
539 shortest = {}
524 visit = [[mod]]
540 visit = [[mod]]
525 while visit:
541 while visit:
526 path = visit.pop(0)
542 path = visit.pop(0)
527 for i in sorted(imports.get(path[-1], [])):
543 for i in sorted(imports.get(path[-1], [])):
528 if len(path) < shortest.get(i, 1000):
544 if len(path) < shortest.get(i, 1000):
529 shortest[i] = len(path)
545 shortest[i] = len(path)
530 if i in path:
546 if i in path:
531 if i == path[0]:
547 if i == path[0]:
532 raise CircularImport(path)
548 raise CircularImport(path)
533 continue
549 continue
534 visit.append(path + [i])
550 visit.append(path + [i])
535
551
536 def rotatecycle(cycle):
552 def rotatecycle(cycle):
537 """arrange a cycle so that the lexicographically first module listed first
553 """arrange a cycle so that the lexicographically first module listed first
538
554
539 >>> rotatecycle(['foo', 'bar'])
555 >>> rotatecycle(['foo', 'bar'])
540 ['bar', 'foo', 'bar']
556 ['bar', 'foo', 'bar']
541 """
557 """
542 lowest = min(cycle)
558 lowest = min(cycle)
543 idx = cycle.index(lowest)
559 idx = cycle.index(lowest)
544 return cycle[idx:] + cycle[:idx] + [lowest]
560 return cycle[idx:] + cycle[:idx] + [lowest]
545
561
546 def find_cycles(imports):
562 def find_cycles(imports):
547 """Find cycles in an already-loaded import graph.
563 """Find cycles in an already-loaded import graph.
548
564
549 All module names recorded in `imports` should be absolute one.
565 All module names recorded in `imports` should be absolute one.
550
566
551 >>> from __future__ import print_function
567 >>> from __future__ import print_function
552 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
568 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
553 ... 'top.bar': ['top.baz', 'sys'],
569 ... 'top.bar': ['top.baz', 'sys'],
554 ... 'top.baz': ['top.foo'],
570 ... 'top.baz': ['top.foo'],
555 ... 'top.qux': ['top.foo']}
571 ... 'top.qux': ['top.foo']}
556 >>> print('\\n'.join(sorted(find_cycles(imports))))
572 >>> print('\\n'.join(sorted(find_cycles(imports))))
557 top.bar -> top.baz -> top.foo -> top.bar
573 top.bar -> top.baz -> top.foo -> top.bar
558 top.foo -> top.qux -> top.foo
574 top.foo -> top.qux -> top.foo
559 """
575 """
560 cycles = set()
576 cycles = set()
561 for mod in sorted(imports.keys()):
577 for mod in sorted(imports.keys()):
562 try:
578 try:
563 checkmod(mod, imports)
579 checkmod(mod, imports)
564 except CircularImport as e:
580 except CircularImport as e:
565 cycle = e.args[0]
581 cycle = e.args[0]
566 cycles.add(" -> ".join(rotatecycle(cycle)))
582 cycles.add(" -> ".join(rotatecycle(cycle)))
567 return cycles
583 return cycles
568
584
569 def _cycle_sortkey(c):
585 def _cycle_sortkey(c):
570 return len(c), c
586 return len(c), c
571
587
572 def embedded(f, modname, src):
588 def embedded(f, modname, src):
573 """Extract embedded python code
589 """Extract embedded python code
574
590
575 >>> def test(fn, lines):
591 >>> def test(fn, lines):
576 ... for s, m, f, l in embedded(fn, "example", lines):
592 ... for s, m, f, l in embedded(fn, "example", lines):
577 ... print("%s %s %s" % (m, f, l))
593 ... print("%s %s %s" % (m, f, l))
578 ... print(repr(s))
594 ... print(repr(s))
579 >>> lines = [
595 >>> lines = [
580 ... 'comment',
596 ... 'comment',
581 ... ' >>> from __future__ import print_function',
597 ... ' >>> from __future__ import print_function',
582 ... " >>> ' multiline",
598 ... " >>> ' multiline",
583 ... " ... string'",
599 ... " ... string'",
584 ... ' ',
600 ... ' ',
585 ... 'comment',
601 ... 'comment',
586 ... ' $ cat > foo.py <<EOF',
602 ... ' $ cat > foo.py <<EOF',
587 ... ' > from __future__ import print_function',
603 ... ' > from __future__ import print_function',
588 ... ' > EOF',
604 ... ' > EOF',
589 ... ]
605 ... ]
590 >>> test("example.t", lines)
606 >>> test("example.t", lines)
591 example[2] doctest.py 2
607 example[2] doctest.py 2
592 "from __future__ import print_function\\n' multiline\\nstring'\\n"
608 "from __future__ import print_function\\n' multiline\\nstring'\\n"
593 example[7] foo.py 7
609 example[7] foo.py 7
594 'from __future__ import print_function\\n'
610 'from __future__ import print_function\\n'
595 """
611 """
596 inlinepython = 0
612 inlinepython = 0
597 shpython = 0
613 shpython = 0
598 script = []
614 script = []
599 prefix = 6
615 prefix = 6
600 t = ''
616 t = ''
601 n = 0
617 n = 0
602 for l in src:
618 for l in src:
603 n += 1
619 n += 1
604 if not l.endswith(b'\n'):
620 if not l.endswith(b'\n'):
605 l += b'\n'
621 l += b'\n'
606 if l.startswith(b' >>> '): # python inlines
622 if l.startswith(b' >>> '): # python inlines
607 if shpython:
623 if shpython:
608 print("%s:%d: Parse Error" % (f, n))
624 print("%s:%d: Parse Error" % (f, n))
609 if not inlinepython:
625 if not inlinepython:
610 # We've just entered a Python block.
626 # We've just entered a Python block.
611 inlinepython = n
627 inlinepython = n
612 t = 'doctest.py'
628 t = 'doctest.py'
613 script.append(l[prefix:])
629 script.append(l[prefix:])
614 continue
630 continue
615 if l.startswith(b' ... '): # python inlines
631 if l.startswith(b' ... '): # python inlines
616 script.append(l[prefix:])
632 script.append(l[prefix:])
617 continue
633 continue
618 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
634 cat = re.search(r"\$ \s*cat\s*>\s*(\S+\.py)\s*<<\s*EOF", l)
619 if cat:
635 if cat:
620 if inlinepython:
636 if inlinepython:
621 yield ''.join(script), ("%s[%d]" %
637 yield ''.join(script), ("%s[%d]" %
622 (modname, inlinepython)), t, inlinepython
638 (modname, inlinepython)), t, inlinepython
623 script = []
639 script = []
624 inlinepython = 0
640 inlinepython = 0
625 shpython = n
641 shpython = n
626 t = cat.group(1)
642 t = cat.group(1)
627 continue
643 continue
628 if shpython and l.startswith(b' > '): # sh continuation
644 if shpython and l.startswith(b' > '): # sh continuation
629 if l == b' > EOF\n':
645 if l == b' > EOF\n':
630 yield ''.join(script), ("%s[%d]" %
646 yield ''.join(script), ("%s[%d]" %
631 (modname, shpython)), t, shpython
647 (modname, shpython)), t, shpython
632 script = []
648 script = []
633 shpython = 0
649 shpython = 0
634 else:
650 else:
635 script.append(l[4:])
651 script.append(l[4:])
636 continue
652 continue
637 if inlinepython and l == b' \n':
653 if inlinepython and l == b' \n':
638 yield ''.join(script), ("%s[%d]" %
654 yield ''.join(script), ("%s[%d]" %
639 (modname, inlinepython)), t, inlinepython
655 (modname, inlinepython)), t, inlinepython
640 script = []
656 script = []
641 inlinepython = 0
657 inlinepython = 0
642 continue
658 continue
643
659
644 def sources(f, modname):
660 def sources(f, modname):
645 """Yields possibly multiple sources from a filepath
661 """Yields possibly multiple sources from a filepath
646
662
647 input: filepath, modulename
663 input: filepath, modulename
648 yields: script(string), modulename, filepath, linenumber
664 yields: script(string), modulename, filepath, linenumber
649
665
650 For embedded scripts, the modulename and filepath will be different
666 For embedded scripts, the modulename and filepath will be different
651 from the function arguments. linenumber is an offset relative to
667 from the function arguments. linenumber is an offset relative to
652 the input file.
668 the input file.
653 """
669 """
654 py = False
670 py = False
655 if f.endswith('.py'):
671 if not f.endswith('.t'):
656 with open(f) as src:
672 with open(f) as src:
657 yield src.read(), modname, f, 0
673 yield src.read(), modname, f, 0
658 py = True
674 py = True
659 if py or f.endswith('.t'):
675 if py or f.endswith('.t'):
660 with open(f) as src:
676 with open(f) as src:
661 for script, modname, t, line in embedded(f, modname, src):
677 for script, modname, t, line in embedded(f, modname, src):
662 yield script, modname, t, line
678 yield script, modname, t, line
663
679
664 def main(argv):
680 def main(argv):
665 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
681 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
666 print('Usage: %s {-|file [file] [file] ...}')
682 print('Usage: %s {-|file [file] [file] ...}')
667 return 1
683 return 1
668 if argv[1] == '-':
684 if argv[1] == '-':
669 argv = argv[:1]
685 argv = argv[:1]
670 argv.extend(l.rstrip() for l in sys.stdin.readlines())
686 argv.extend(l.rstrip() for l in sys.stdin.readlines())
671 localmods = {}
687 localmods = {}
672 used_imports = {}
688 used_imports = {}
673 any_errors = False
689 any_errors = False
674 for source_path in argv[1:]:
690 for source_path in argv[1:]:
675 modname = dotted_name_of_path(source_path, trimpure=True)
691 modname = dotted_name_of_path(source_path, trimpure=True)
676 localmods[modname] = source_path
692 localmods[modname] = source_path
677 for localmodname, source_path in sorted(localmods.items()):
693 for localmodname, source_path in sorted(localmods.items()):
678 for src, modname, name, line in sources(source_path, localmodname):
694 for src, modname, name, line in sources(source_path, localmodname):
679 try:
695 try:
680 used_imports[modname] = sorted(
696 used_imports[modname] = sorted(
681 imported_modules(src, modname, name, localmods,
697 imported_modules(src, modname, name, localmods,
682 ignore_nested=True))
698 ignore_nested=True))
683 for error, lineno in verify_import_convention(modname, src,
699 for error, lineno in verify_import_convention(modname, src,
684 localmods):
700 localmods):
685 any_errors = True
701 any_errors = True
686 print('%s:%d: %s' % (source_path, lineno + line, error))
702 print('%s:%d: %s' % (source_path, lineno + line, error))
687 except SyntaxError as e:
703 except SyntaxError as e:
688 print('%s:%d: SyntaxError: %s' %
704 print('%s:%d: SyntaxError: %s' %
689 (source_path, e.lineno + line, e))
705 (source_path, e.lineno + line, e))
690 cycles = find_cycles(used_imports)
706 cycles = find_cycles(used_imports)
691 if cycles:
707 if cycles:
692 firstmods = set()
708 firstmods = set()
693 for c in sorted(cycles, key=_cycle_sortkey):
709 for c in sorted(cycles, key=_cycle_sortkey):
694 first = c.split()[0]
710 first = c.split()[0]
695 # As a rough cut, ignore any cycle that starts with the
711 # As a rough cut, ignore any cycle that starts with the
696 # same module as some other cycle. Otherwise we see lots
712 # same module as some other cycle. Otherwise we see lots
697 # of cycles that are effectively duplicates.
713 # of cycles that are effectively duplicates.
698 if first in firstmods:
714 if first in firstmods:
699 continue
715 continue
700 print('Import cycle:', c)
716 print('Import cycle:', c)
701 firstmods.add(first)
717 firstmods.add(first)
702 any_errors = True
718 any_errors = True
703 return any_errors != 0
719 return any_errors != 0
704
720
705 if __name__ == '__main__':
721 if __name__ == '__main__':
706 sys.exit(int(main(sys.argv)))
722 sys.exit(int(main(sys.argv)))
@@ -1,20 +1,20
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2 <!-- This is the second screen displayed during the install. -->
2 <!-- This is the first screen displayed during the install. -->
3 <html>
3 <html>
4 <head>
4 <head>
5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
6 <meta http-equiv="Content-Style-Type" content="text/css">
6 <meta http-equiv="Content-Style-Type" content="text/css">
7 <title></title>
7 <title></title>
8 <style type="text/css">
8 <style type="text/css">
9 p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Helvetica}
9 p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Helvetica}
10 p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica; min-height: 14.0px}
10 p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica; min-height: 14.0px}
11 </style>
11 </style>
12 </head>
12 </head>
13 <body>
13 <body>
14 <p class="p1">This is a prepackaged release of <a href="https://mercurial-scm.org/">Mercurial</a> for Mac OS X.</p>
14 <p class="p1">This is a prepackaged release of <a href="https://mercurial-scm.org/">Mercurial</a> for Mac OS X.</p>
15 <p class="p2"><br></p>
15 <p class="p2"><br></p>
16 <br>
16 <br>
17 <p>
17 <p>
18 Please be sure to read the latest <a href="https://mercurial-scm.org/wiki/WhatsNew">release notes</a>.</p>
18 Please be sure to read the latest <a href="https://mercurial-scm.org/wiki/WhatsNew">release notes</a>.</p>
19 </body>
19 </body>
20 </html>
20 </html>
@@ -1,798 +1,902
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:
5 #
6 # We have to do:
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.
9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
10 # - make historical perf command work correctly with as wide Mercurial
11 # version as possible
12 #
13 # We have to do, if possible with reasonable cost:
14 # - make recent perf command for historical feature work correctly
15 # with early Mercurial
16 #
17 # We don't have to do:
18 # - make perf command for recent feature work correctly with early
19 # Mercurial
20
4 from __future__ import absolute_import
21 from __future__ import absolute_import
5 import functools
22 import functools
6 import os
23 import os
7 import random
24 import random
8 import sys
25 import sys
9 import time
26 import time
10 from mercurial import (
27 from mercurial import (
11 branchmap,
12 cmdutil,
28 cmdutil,
13 commands,
29 commands,
14 copies,
30 copies,
15 error,
31 error,
32 extensions,
16 mdiff,
33 mdiff,
17 merge,
34 merge,
18 obsolete,
19 repoview,
20 revlog,
35 revlog,
21 scmutil,
22 util,
36 util,
23 )
37 )
24
38
25 formatteropts = commands.formatteropts
39 # for "historical portability":
26 revlogopts = commands.debugrevlogopts
40 # try to import modules separately (in dict order), and ignore
41 # failure, because these aren't available with early Mercurial
42 try:
43 from mercurial import branchmap # since 2.5 (or bcee63733aad)
44 except ImportError:
45 pass
46 try:
47 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
48 except ImportError:
49 pass
50 try:
51 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
52 except ImportError:
53 pass
54 try:
55 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
56 except ImportError:
57 pass
58
59 # for "historical portability":
60 # define util.safehasattr forcibly, because util.safehasattr has been
61 # available since 1.9.3 (or 94b200a11cf7)
62 _undefined = object()
63 def safehasattr(thing, attr):
64 return getattr(thing, attr, _undefined) is not _undefined
65 setattr(util, 'safehasattr', safehasattr)
66
67 # for "historical portability":
68 # use locally defined empty option list, if formatteropts isn't
69 # available, because commands.formatteropts has been available since
70 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
71 # available since 2.2 (or ae5f92e154d3)
72 formatteropts = getattr(commands, "formatteropts", [])
73
74 # for "historical portability":
75 # use locally defined option list, if debugrevlogopts isn't available,
76 # because commands.debugrevlogopts has been available since 3.7 (or
77 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
78 # since 1.9 (or a79fea6b3e77).
79 revlogopts = getattr(commands, "debugrevlogopts", [
80 ('c', 'changelog', False, ('open changelog')),
81 ('m', 'manifest', False, ('open manifest')),
82 ('', 'dir', False, ('open directory manifest')),
83 ])
27
84
28 cmdtable = {}
85 cmdtable = {}
29 command = cmdutil.command(cmdtable)
86
87 # for "historical portability":
88 # define parsealiases locally, because cmdutil.parsealiases has been
89 # available since 1.5 (or 6252852b4332)
90 def parsealiases(cmd):
91 return cmd.lstrip("^").split("|")
92
93 if safehasattr(cmdutil, 'command'):
94 import inspect
95 command = cmdutil.command(cmdtable)
96 if 'norepo' not in inspect.getargspec(command)[0]:
97 # for "historical portability":
98 # wrap original cmdutil.command, because "norepo" option has
99 # been available since 3.1 (or 75a96326cecb)
100 _command = command
101 def command(name, options=(), synopsis=None, norepo=False):
102 if norepo:
103 commands.norepo += ' %s' % ' '.join(parsealiases(name))
104 return _command(name, list(options), synopsis)
105 else:
106 # for "historical portability":
107 # define "@command" annotation locally, because cmdutil.command
108 # has been available since 1.9 (or 2daa5179e73f)
109 def command(name, options=(), synopsis=None, norepo=False):
110 def decorator(func):
111 if synopsis:
112 cmdtable[name] = func, list(options), synopsis
113 else:
114 cmdtable[name] = func, list(options)
115 if norepo:
116 commands.norepo += ' %s' % ' '.join(parsealiases(name))
117 return func
118 return decorator
30
119
31 def getlen(ui):
120 def getlen(ui):
32 if ui.configbool("perf", "stub"):
121 if ui.configbool("perf", "stub"):
33 return lambda x: 1
122 return lambda x: 1
34 return len
123 return len
35
124
36 def gettimer(ui, opts=None):
125 def gettimer(ui, opts=None):
37 """return a timer function and formatter: (timer, formatter)
126 """return a timer function and formatter: (timer, formatter)
38
127
39 This function exists to gather the creation of formatter in a single
128 This function exists to gather the creation of formatter in a single
40 place instead of duplicating it in all performance commands."""
129 place instead of duplicating it in all performance commands."""
41
130
42 # enforce an idle period before execution to counteract power management
131 # enforce an idle period before execution to counteract power management
43 # experimental config: perf.presleep
132 # experimental config: perf.presleep
44 time.sleep(ui.configint("perf", "presleep", 1))
133 time.sleep(ui.configint("perf", "presleep", 1))
45
134
46 if opts is None:
135 if opts is None:
47 opts = {}
136 opts = {}
48 # redirect all to stderr
137 # redirect all to stderr
49 ui = ui.copy()
138 ui = ui.copy()
50 ui.fout = ui.ferr
139 ui.fout = ui.ferr
51 # get a formatter
140 # get a formatter
52 fm = ui.formatter('perf', opts)
141 fm = ui.formatter('perf', opts)
53 # stub function, runs code only once instead of in a loop
142 # stub function, runs code only once instead of in a loop
54 # experimental config: perf.stub
143 # experimental config: perf.stub
55 if ui.configbool("perf", "stub"):
144 if ui.configbool("perf", "stub"):
56 return functools.partial(stub_timer, fm), fm
145 return functools.partial(stub_timer, fm), fm
57 return functools.partial(_timer, fm), fm
146 return functools.partial(_timer, fm), fm
58
147
59 def stub_timer(fm, func, title=None):
148 def stub_timer(fm, func, title=None):
60 func()
149 func()
61
150
62 def _timer(fm, func, title=None):
151 def _timer(fm, func, title=None):
63 results = []
152 results = []
64 begin = time.time()
153 begin = time.time()
65 count = 0
154 count = 0
66 while True:
155 while True:
67 ostart = os.times()
156 ostart = os.times()
68 cstart = time.time()
157 cstart = time.time()
69 r = func()
158 r = func()
70 cstop = time.time()
159 cstop = time.time()
71 ostop = os.times()
160 ostop = os.times()
72 count += 1
161 count += 1
73 a, b = ostart, ostop
162 a, b = ostart, ostop
74 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
163 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
75 if cstop - begin > 3 and count >= 100:
164 if cstop - begin > 3 and count >= 100:
76 break
165 break
77 if cstop - begin > 10 and count >= 3:
166 if cstop - begin > 10 and count >= 3:
78 break
167 break
79
168
80 fm.startitem()
169 fm.startitem()
81
170
82 if title:
171 if title:
83 fm.write('title', '! %s\n', title)
172 fm.write('title', '! %s\n', title)
84 if r:
173 if r:
85 fm.write('result', '! result: %s\n', r)
174 fm.write('result', '! result: %s\n', r)
86 m = min(results)
175 m = min(results)
87 fm.plain('!')
176 fm.plain('!')
88 fm.write('wall', ' wall %f', m[0])
177 fm.write('wall', ' wall %f', m[0])
89 fm.write('comb', ' comb %f', m[1] + m[2])
178 fm.write('comb', ' comb %f', m[1] + m[2])
90 fm.write('user', ' user %f', m[1])
179 fm.write('user', ' user %f', m[1])
91 fm.write('sys', ' sys %f', m[2])
180 fm.write('sys', ' sys %f', m[2])
92 fm.write('count', ' (best of %d)', count)
181 fm.write('count', ' (best of %d)', count)
93 fm.plain('\n')
182 fm.plain('\n')
94
183
95 @command('perfwalk', formatteropts)
184 @command('perfwalk', formatteropts)
96 def perfwalk(ui, repo, *pats, **opts):
185 def perfwalk(ui, repo, *pats, **opts):
97 timer, fm = gettimer(ui, opts)
186 timer, fm = gettimer(ui, opts)
98 try:
187 try:
99 m = scmutil.match(repo[None], pats, {})
188 m = scmutil.match(repo[None], pats, {})
100 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
189 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
101 except Exception:
190 except Exception:
102 try:
191 try:
103 m = scmutil.match(repo[None], pats, {})
192 m = scmutil.match(repo[None], pats, {})
104 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
193 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
105 except Exception:
194 except Exception:
106 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
195 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
107 fm.end()
196 fm.end()
108
197
109 @command('perfannotate', formatteropts)
198 @command('perfannotate', formatteropts)
110 def perfannotate(ui, repo, f, **opts):
199 def perfannotate(ui, repo, f, **opts):
111 timer, fm = gettimer(ui, opts)
200 timer, fm = gettimer(ui, opts)
112 fc = repo['.'][f]
201 fc = repo['.'][f]
113 timer(lambda: len(fc.annotate(True)))
202 timer(lambda: len(fc.annotate(True)))
114 fm.end()
203 fm.end()
115
204
116 @command('perfstatus',
205 @command('perfstatus',
117 [('u', 'unknown', False,
206 [('u', 'unknown', False,
118 'ask status to look for unknown files')] + formatteropts)
207 'ask status to look for unknown files')] + formatteropts)
119 def perfstatus(ui, repo, **opts):
208 def perfstatus(ui, repo, **opts):
120 #m = match.always(repo.root, repo.getcwd())
209 #m = match.always(repo.root, repo.getcwd())
121 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
210 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
122 # False))))
211 # False))))
123 timer, fm = gettimer(ui, opts)
212 timer, fm = gettimer(ui, opts)
124 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
213 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
125 fm.end()
214 fm.end()
126
215
127 @command('perfaddremove', formatteropts)
216 @command('perfaddremove', formatteropts)
128 def perfaddremove(ui, repo, **opts):
217 def perfaddremove(ui, repo, **opts):
129 timer, fm = gettimer(ui, opts)
218 timer, fm = gettimer(ui, opts)
130 try:
219 try:
131 oldquiet = repo.ui.quiet
220 oldquiet = repo.ui.quiet
132 repo.ui.quiet = True
221 repo.ui.quiet = True
133 matcher = scmutil.match(repo[None])
222 matcher = scmutil.match(repo[None])
134 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
223 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
135 finally:
224 finally:
136 repo.ui.quiet = oldquiet
225 repo.ui.quiet = oldquiet
137 fm.end()
226 fm.end()
138
227
139 def clearcaches(cl):
228 def clearcaches(cl):
140 # behave somewhat consistently across internal API changes
229 # behave somewhat consistently across internal API changes
141 if util.safehasattr(cl, 'clearcaches'):
230 if util.safehasattr(cl, 'clearcaches'):
142 cl.clearcaches()
231 cl.clearcaches()
143 elif util.safehasattr(cl, '_nodecache'):
232 elif util.safehasattr(cl, '_nodecache'):
144 from mercurial.node import nullid, nullrev
233 from mercurial.node import nullid, nullrev
145 cl._nodecache = {nullid: nullrev}
234 cl._nodecache = {nullid: nullrev}
146 cl._nodepos = None
235 cl._nodepos = None
147
236
148 @command('perfheads', formatteropts)
237 @command('perfheads', formatteropts)
149 def perfheads(ui, repo, **opts):
238 def perfheads(ui, repo, **opts):
150 timer, fm = gettimer(ui, opts)
239 timer, fm = gettimer(ui, opts)
151 cl = repo.changelog
240 cl = repo.changelog
152 def d():
241 def d():
153 len(cl.headrevs())
242 len(cl.headrevs())
154 clearcaches(cl)
243 clearcaches(cl)
155 timer(d)
244 timer(d)
156 fm.end()
245 fm.end()
157
246
158 @command('perftags', formatteropts)
247 @command('perftags', formatteropts)
159 def perftags(ui, repo, **opts):
248 def perftags(ui, repo, **opts):
160 import mercurial.changelog
249 import mercurial.changelog
161 import mercurial.manifest
250 import mercurial.manifest
162 timer, fm = gettimer(ui, opts)
251 timer, fm = gettimer(ui, opts)
163 def t():
252 def t():
164 repo.changelog = mercurial.changelog.changelog(repo.svfs)
253 repo.changelog = mercurial.changelog.changelog(repo.svfs)
165 repo.manifest = mercurial.manifest.manifest(repo.svfs)
254 repo.manifest = mercurial.manifest.manifest(repo.svfs)
166 repo._tags = None
255 repo._tags = None
167 return len(repo.tags())
256 return len(repo.tags())
168 timer(t)
257 timer(t)
169 fm.end()
258 fm.end()
170
259
171 @command('perfancestors', formatteropts)
260 @command('perfancestors', formatteropts)
172 def perfancestors(ui, repo, **opts):
261 def perfancestors(ui, repo, **opts):
173 timer, fm = gettimer(ui, opts)
262 timer, fm = gettimer(ui, opts)
174 heads = repo.changelog.headrevs()
263 heads = repo.changelog.headrevs()
175 def d():
264 def d():
176 for a in repo.changelog.ancestors(heads):
265 for a in repo.changelog.ancestors(heads):
177 pass
266 pass
178 timer(d)
267 timer(d)
179 fm.end()
268 fm.end()
180
269
181 @command('perfancestorset', formatteropts)
270 @command('perfancestorset', formatteropts)
182 def perfancestorset(ui, repo, revset, **opts):
271 def perfancestorset(ui, repo, revset, **opts):
183 timer, fm = gettimer(ui, opts)
272 timer, fm = gettimer(ui, opts)
184 revs = repo.revs(revset)
273 revs = repo.revs(revset)
185 heads = repo.changelog.headrevs()
274 heads = repo.changelog.headrevs()
186 def d():
275 def d():
187 s = repo.changelog.ancestors(heads)
276 s = repo.changelog.ancestors(heads)
188 for rev in revs:
277 for rev in revs:
189 rev in s
278 rev in s
190 timer(d)
279 timer(d)
191 fm.end()
280 fm.end()
192
281
193 @command('perfdirs', formatteropts)
282 @command('perfdirs', formatteropts)
194 def perfdirs(ui, repo, **opts):
283 def perfdirs(ui, repo, **opts):
195 timer, fm = gettimer(ui, opts)
284 timer, fm = gettimer(ui, opts)
196 dirstate = repo.dirstate
285 dirstate = repo.dirstate
197 'a' in dirstate
286 'a' in dirstate
198 def d():
287 def d():
199 dirstate.dirs()
288 dirstate.dirs()
200 del dirstate._dirs
289 del dirstate._dirs
201 timer(d)
290 timer(d)
202 fm.end()
291 fm.end()
203
292
204 @command('perfdirstate', formatteropts)
293 @command('perfdirstate', formatteropts)
205 def perfdirstate(ui, repo, **opts):
294 def perfdirstate(ui, repo, **opts):
206 timer, fm = gettimer(ui, opts)
295 timer, fm = gettimer(ui, opts)
207 "a" in repo.dirstate
296 "a" in repo.dirstate
208 def d():
297 def d():
209 repo.dirstate.invalidate()
298 repo.dirstate.invalidate()
210 "a" in repo.dirstate
299 "a" in repo.dirstate
211 timer(d)
300 timer(d)
212 fm.end()
301 fm.end()
213
302
214 @command('perfdirstatedirs', formatteropts)
303 @command('perfdirstatedirs', formatteropts)
215 def perfdirstatedirs(ui, repo, **opts):
304 def perfdirstatedirs(ui, repo, **opts):
216 timer, fm = gettimer(ui, opts)
305 timer, fm = gettimer(ui, opts)
217 "a" in repo.dirstate
306 "a" in repo.dirstate
218 def d():
307 def d():
219 "a" in repo.dirstate._dirs
308 "a" in repo.dirstate._dirs
220 del repo.dirstate._dirs
309 del repo.dirstate._dirs
221 timer(d)
310 timer(d)
222 fm.end()
311 fm.end()
223
312
224 @command('perfdirstatefoldmap', formatteropts)
313 @command('perfdirstatefoldmap', formatteropts)
225 def perfdirstatefoldmap(ui, repo, **opts):
314 def perfdirstatefoldmap(ui, repo, **opts):
226 timer, fm = gettimer(ui, opts)
315 timer, fm = gettimer(ui, opts)
227 dirstate = repo.dirstate
316 dirstate = repo.dirstate
228 'a' in dirstate
317 'a' in dirstate
229 def d():
318 def d():
230 dirstate._filefoldmap.get('a')
319 dirstate._filefoldmap.get('a')
231 del dirstate._filefoldmap
320 del dirstate._filefoldmap
232 timer(d)
321 timer(d)
233 fm.end()
322 fm.end()
234
323
235 @command('perfdirfoldmap', formatteropts)
324 @command('perfdirfoldmap', formatteropts)
236 def perfdirfoldmap(ui, repo, **opts):
325 def perfdirfoldmap(ui, repo, **opts):
237 timer, fm = gettimer(ui, opts)
326 timer, fm = gettimer(ui, opts)
238 dirstate = repo.dirstate
327 dirstate = repo.dirstate
239 'a' in dirstate
328 'a' in dirstate
240 def d():
329 def d():
241 dirstate._dirfoldmap.get('a')
330 dirstate._dirfoldmap.get('a')
242 del dirstate._dirfoldmap
331 del dirstate._dirfoldmap
243 del dirstate._dirs
332 del dirstate._dirs
244 timer(d)
333 timer(d)
245 fm.end()
334 fm.end()
246
335
247 @command('perfdirstatewrite', formatteropts)
336 @command('perfdirstatewrite', formatteropts)
248 def perfdirstatewrite(ui, repo, **opts):
337 def perfdirstatewrite(ui, repo, **opts):
249 timer, fm = gettimer(ui, opts)
338 timer, fm = gettimer(ui, opts)
250 ds = repo.dirstate
339 ds = repo.dirstate
251 "a" in ds
340 "a" in ds
252 def d():
341 def d():
253 ds._dirty = True
342 ds._dirty = True
254 ds.write(repo.currenttransaction())
343 ds.write(repo.currenttransaction())
255 timer(d)
344 timer(d)
256 fm.end()
345 fm.end()
257
346
258 @command('perfmergecalculate',
347 @command('perfmergecalculate',
259 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
348 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
260 def perfmergecalculate(ui, repo, rev, **opts):
349 def perfmergecalculate(ui, repo, rev, **opts):
261 timer, fm = gettimer(ui, opts)
350 timer, fm = gettimer(ui, opts)
262 wctx = repo[None]
351 wctx = repo[None]
263 rctx = scmutil.revsingle(repo, rev, rev)
352 rctx = scmutil.revsingle(repo, rev, rev)
264 ancestor = wctx.ancestor(rctx)
353 ancestor = wctx.ancestor(rctx)
265 # we don't want working dir files to be stat'd in the benchmark, so prime
354 # we don't want working dir files to be stat'd in the benchmark, so prime
266 # that cache
355 # that cache
267 wctx.dirty()
356 wctx.dirty()
268 def d():
357 def d():
269 # acceptremote is True because we don't want prompts in the middle of
358 # acceptremote is True because we don't want prompts in the middle of
270 # our benchmark
359 # our benchmark
271 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
360 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
272 acceptremote=True, followcopies=True)
361 acceptremote=True, followcopies=True)
273 timer(d)
362 timer(d)
274 fm.end()
363 fm.end()
275
364
276 @command('perfpathcopies', [], "REV REV")
365 @command('perfpathcopies', [], "REV REV")
277 def perfpathcopies(ui, repo, rev1, rev2, **opts):
366 def perfpathcopies(ui, repo, rev1, rev2, **opts):
278 timer, fm = gettimer(ui, opts)
367 timer, fm = gettimer(ui, opts)
279 ctx1 = scmutil.revsingle(repo, rev1, rev1)
368 ctx1 = scmutil.revsingle(repo, rev1, rev1)
280 ctx2 = scmutil.revsingle(repo, rev2, rev2)
369 ctx2 = scmutil.revsingle(repo, rev2, rev2)
281 def d():
370 def d():
282 copies.pathcopies(ctx1, ctx2)
371 copies.pathcopies(ctx1, ctx2)
283 timer(d)
372 timer(d)
284 fm.end()
373 fm.end()
285
374
286 @command('perfmanifest', [], 'REV')
375 @command('perfmanifest', [], 'REV')
287 def perfmanifest(ui, repo, rev, **opts):
376 def perfmanifest(ui, repo, rev, **opts):
288 timer, fm = gettimer(ui, opts)
377 timer, fm = gettimer(ui, opts)
289 ctx = scmutil.revsingle(repo, rev, rev)
378 ctx = scmutil.revsingle(repo, rev, rev)
290 t = ctx.manifestnode()
379 t = ctx.manifestnode()
291 def d():
380 def d():
292 repo.manifest.clearcaches()
381 repo.manifest.clearcaches()
293 repo.manifest.read(t)
382 repo.manifest.read(t)
294 timer(d)
383 timer(d)
295 fm.end()
384 fm.end()
296
385
297 @command('perfchangeset', formatteropts)
386 @command('perfchangeset', formatteropts)
298 def perfchangeset(ui, repo, rev, **opts):
387 def perfchangeset(ui, repo, rev, **opts):
299 timer, fm = gettimer(ui, opts)
388 timer, fm = gettimer(ui, opts)
300 n = repo[rev].node()
389 n = repo[rev].node()
301 def d():
390 def d():
302 repo.changelog.read(n)
391 repo.changelog.read(n)
303 #repo.changelog._cache = None
392 #repo.changelog._cache = None
304 timer(d)
393 timer(d)
305 fm.end()
394 fm.end()
306
395
307 @command('perfindex', formatteropts)
396 @command('perfindex', formatteropts)
308 def perfindex(ui, repo, **opts):
397 def perfindex(ui, repo, **opts):
309 import mercurial.revlog
398 import mercurial.revlog
310 timer, fm = gettimer(ui, opts)
399 timer, fm = gettimer(ui, opts)
311 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
400 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
312 n = repo["tip"].node()
401 n = repo["tip"].node()
313 def d():
402 def d():
314 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
403 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
315 cl.rev(n)
404 cl.rev(n)
316 timer(d)
405 timer(d)
317 fm.end()
406 fm.end()
318
407
319 @command('perfstartup', formatteropts)
408 @command('perfstartup', formatteropts)
320 def perfstartup(ui, repo, **opts):
409 def perfstartup(ui, repo, **opts):
321 timer, fm = gettimer(ui, opts)
410 timer, fm = gettimer(ui, opts)
322 cmd = sys.argv[0]
411 cmd = sys.argv[0]
323 def d():
412 def d():
324 if os.name != 'nt':
413 if os.name != 'nt':
325 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
414 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
326 else:
415 else:
327 os.environ['HGRCPATH'] = ''
416 os.environ['HGRCPATH'] = ''
328 os.system("%s version -q > NUL" % cmd)
417 os.system("%s version -q > NUL" % cmd)
329 timer(d)
418 timer(d)
330 fm.end()
419 fm.end()
331
420
332 @command('perfparents', formatteropts)
421 @command('perfparents', formatteropts)
333 def perfparents(ui, repo, **opts):
422 def perfparents(ui, repo, **opts):
334 timer, fm = gettimer(ui, opts)
423 timer, fm = gettimer(ui, opts)
335 # control the number of commits perfparents iterates over
424 # control the number of commits perfparents iterates over
336 # experimental config: perf.parentscount
425 # experimental config: perf.parentscount
337 count = ui.configint("perf", "parentscount", 1000)
426 count = ui.configint("perf", "parentscount", 1000)
338 if len(repo.changelog) < count:
427 if len(repo.changelog) < count:
339 raise error.Abort("repo needs %d commits for this test" % count)
428 raise error.Abort("repo needs %d commits for this test" % count)
340 repo = repo.unfiltered()
429 repo = repo.unfiltered()
341 nl = [repo.changelog.node(i) for i in xrange(count)]
430 nl = [repo.changelog.node(i) for i in xrange(count)]
342 def d():
431 def d():
343 for n in nl:
432 for n in nl:
344 repo.changelog.parents(n)
433 repo.changelog.parents(n)
345 timer(d)
434 timer(d)
346 fm.end()
435 fm.end()
347
436
348 @command('perfctxfiles', formatteropts)
437 @command('perfctxfiles', formatteropts)
349 def perfctxfiles(ui, repo, x, **opts):
438 def perfctxfiles(ui, repo, x, **opts):
350 x = int(x)
439 x = int(x)
351 timer, fm = gettimer(ui, opts)
440 timer, fm = gettimer(ui, opts)
352 def d():
441 def d():
353 len(repo[x].files())
442 len(repo[x].files())
354 timer(d)
443 timer(d)
355 fm.end()
444 fm.end()
356
445
357 @command('perfrawfiles', formatteropts)
446 @command('perfrawfiles', formatteropts)
358 def perfrawfiles(ui, repo, x, **opts):
447 def perfrawfiles(ui, repo, x, **opts):
359 x = int(x)
448 x = int(x)
360 timer, fm = gettimer(ui, opts)
449 timer, fm = gettimer(ui, opts)
361 cl = repo.changelog
450 cl = repo.changelog
362 def d():
451 def d():
363 len(cl.read(x)[3])
452 len(cl.read(x)[3])
364 timer(d)
453 timer(d)
365 fm.end()
454 fm.end()
366
455
367 @command('perflookup', formatteropts)
456 @command('perflookup', formatteropts)
368 def perflookup(ui, repo, rev, **opts):
457 def perflookup(ui, repo, rev, **opts):
369 timer, fm = gettimer(ui, opts)
458 timer, fm = gettimer(ui, opts)
370 timer(lambda: len(repo.lookup(rev)))
459 timer(lambda: len(repo.lookup(rev)))
371 fm.end()
460 fm.end()
372
461
373 @command('perfrevrange', formatteropts)
462 @command('perfrevrange', formatteropts)
374 def perfrevrange(ui, repo, *specs, **opts):
463 def perfrevrange(ui, repo, *specs, **opts):
375 timer, fm = gettimer(ui, opts)
464 timer, fm = gettimer(ui, opts)
376 revrange = scmutil.revrange
465 revrange = scmutil.revrange
377 timer(lambda: len(revrange(repo, specs)))
466 timer(lambda: len(revrange(repo, specs)))
378 fm.end()
467 fm.end()
379
468
380 @command('perfnodelookup', formatteropts)
469 @command('perfnodelookup', formatteropts)
381 def perfnodelookup(ui, repo, rev, **opts):
470 def perfnodelookup(ui, repo, rev, **opts):
382 timer, fm = gettimer(ui, opts)
471 timer, fm = gettimer(ui, opts)
383 import mercurial.revlog
472 import mercurial.revlog
384 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
473 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
385 n = repo[rev].node()
474 n = repo[rev].node()
386 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
475 cl = mercurial.revlog.revlog(repo.svfs, "00changelog.i")
387 def d():
476 def d():
388 cl.rev(n)
477 cl.rev(n)
389 clearcaches(cl)
478 clearcaches(cl)
390 timer(d)
479 timer(d)
391 fm.end()
480 fm.end()
392
481
393 @command('perflog',
482 @command('perflog',
394 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
483 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
395 def perflog(ui, repo, rev=None, **opts):
484 def perflog(ui, repo, rev=None, **opts):
396 if rev is None:
485 if rev is None:
397 rev=[]
486 rev=[]
398 timer, fm = gettimer(ui, opts)
487 timer, fm = gettimer(ui, opts)
399 ui.pushbuffer()
488 ui.pushbuffer()
400 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
489 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
401 copies=opts.get('rename')))
490 copies=opts.get('rename')))
402 ui.popbuffer()
491 ui.popbuffer()
403 fm.end()
492 fm.end()
404
493
405 @command('perfmoonwalk', formatteropts)
494 @command('perfmoonwalk', formatteropts)
406 def perfmoonwalk(ui, repo, **opts):
495 def perfmoonwalk(ui, repo, **opts):
407 """benchmark walking the changelog backwards
496 """benchmark walking the changelog backwards
408
497
409 This also loads the changelog data for each revision in the changelog.
498 This also loads the changelog data for each revision in the changelog.
410 """
499 """
411 timer, fm = gettimer(ui, opts)
500 timer, fm = gettimer(ui, opts)
412 def moonwalk():
501 def moonwalk():
413 for i in xrange(len(repo), -1, -1):
502 for i in xrange(len(repo), -1, -1):
414 ctx = repo[i]
503 ctx = repo[i]
415 ctx.branch() # read changelog data (in addition to the index)
504 ctx.branch() # read changelog data (in addition to the index)
416 timer(moonwalk)
505 timer(moonwalk)
417 fm.end()
506 fm.end()
418
507
419 @command('perftemplating', formatteropts)
508 @command('perftemplating', formatteropts)
420 def perftemplating(ui, repo, rev=None, **opts):
509 def perftemplating(ui, repo, rev=None, **opts):
421 if rev is None:
510 if rev is None:
422 rev=[]
511 rev=[]
423 timer, fm = gettimer(ui, opts)
512 timer, fm = gettimer(ui, opts)
424 ui.pushbuffer()
513 ui.pushbuffer()
425 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
514 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
426 template='{date|shortdate} [{rev}:{node|short}]'
515 template='{date|shortdate} [{rev}:{node|short}]'
427 ' {author|person}: {desc|firstline}\n'))
516 ' {author|person}: {desc|firstline}\n'))
428 ui.popbuffer()
517 ui.popbuffer()
429 fm.end()
518 fm.end()
430
519
431 @command('perfcca', formatteropts)
520 @command('perfcca', formatteropts)
432 def perfcca(ui, repo, **opts):
521 def perfcca(ui, repo, **opts):
433 timer, fm = gettimer(ui, opts)
522 timer, fm = gettimer(ui, opts)
434 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
523 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
435 fm.end()
524 fm.end()
436
525
437 @command('perffncacheload', formatteropts)
526 @command('perffncacheload', formatteropts)
438 def perffncacheload(ui, repo, **opts):
527 def perffncacheload(ui, repo, **opts):
439 timer, fm = gettimer(ui, opts)
528 timer, fm = gettimer(ui, opts)
440 s = repo.store
529 s = repo.store
441 def d():
530 def d():
442 s.fncache._load()
531 s.fncache._load()
443 timer(d)
532 timer(d)
444 fm.end()
533 fm.end()
445
534
446 @command('perffncachewrite', formatteropts)
535 @command('perffncachewrite', formatteropts)
447 def perffncachewrite(ui, repo, **opts):
536 def perffncachewrite(ui, repo, **opts):
448 timer, fm = gettimer(ui, opts)
537 timer, fm = gettimer(ui, opts)
449 s = repo.store
538 s = repo.store
450 s.fncache._load()
539 s.fncache._load()
451 lock = repo.lock()
540 lock = repo.lock()
452 tr = repo.transaction('perffncachewrite')
541 tr = repo.transaction('perffncachewrite')
453 def d():
542 def d():
454 s.fncache._dirty = True
543 s.fncache._dirty = True
455 s.fncache.write(tr)
544 s.fncache.write(tr)
456 timer(d)
545 timer(d)
457 lock.release()
546 lock.release()
458 tr.close()
547 tr.close()
459 fm.end()
548 fm.end()
460
549
461 @command('perffncacheencode', formatteropts)
550 @command('perffncacheencode', formatteropts)
462 def perffncacheencode(ui, repo, **opts):
551 def perffncacheencode(ui, repo, **opts):
463 timer, fm = gettimer(ui, opts)
552 timer, fm = gettimer(ui, opts)
464 s = repo.store
553 s = repo.store
465 s.fncache._load()
554 s.fncache._load()
466 def d():
555 def d():
467 for p in s.fncache.entries:
556 for p in s.fncache.entries:
468 s.encode(p)
557 s.encode(p)
469 timer(d)
558 timer(d)
470 fm.end()
559 fm.end()
471
560
472 @command('perfdiffwd', formatteropts)
561 @command('perfdiffwd', formatteropts)
473 def perfdiffwd(ui, repo, **opts):
562 def perfdiffwd(ui, repo, **opts):
474 """Profile diff of working directory changes"""
563 """Profile diff of working directory changes"""
475 timer, fm = gettimer(ui, opts)
564 timer, fm = gettimer(ui, opts)
476 options = {
565 options = {
477 'w': 'ignore_all_space',
566 'w': 'ignore_all_space',
478 'b': 'ignore_space_change',
567 'b': 'ignore_space_change',
479 'B': 'ignore_blank_lines',
568 'B': 'ignore_blank_lines',
480 }
569 }
481
570
482 for diffopt in ('', 'w', 'b', 'B', 'wB'):
571 for diffopt in ('', 'w', 'b', 'B', 'wB'):
483 opts = dict((options[c], '1') for c in diffopt)
572 opts = dict((options[c], '1') for c in diffopt)
484 def d():
573 def d():
485 ui.pushbuffer()
574 ui.pushbuffer()
486 commands.diff(ui, repo, **opts)
575 commands.diff(ui, repo, **opts)
487 ui.popbuffer()
576 ui.popbuffer()
488 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
577 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
489 timer(d, title)
578 timer(d, title)
490 fm.end()
579 fm.end()
491
580
492 @command('perfrevlog', revlogopts + formatteropts +
581 @command('perfrevlog', revlogopts + formatteropts +
493 [('d', 'dist', 100, 'distance between the revisions'),
582 [('d', 'dist', 100, 'distance between the revisions'),
494 ('s', 'startrev', 0, 'revision to start reading at')],
583 ('s', 'startrev', 0, 'revision to start reading at')],
495 '-c|-m|FILE')
584 '-c|-m|FILE')
496 def perfrevlog(ui, repo, file_=None, startrev=0, **opts):
585 def perfrevlog(ui, repo, file_=None, startrev=0, **opts):
497 """Benchmark reading a series of revisions from a revlog.
586 """Benchmark reading a series of revisions from a revlog.
498
587
499 By default, we read every ``-d/--dist`` revision from 0 to tip of
588 By default, we read every ``-d/--dist`` revision from 0 to tip of
500 the specified revlog.
589 the specified revlog.
501
590
502 The start revision can be defined via ``-s/--startrev``.
591 The start revision can be defined via ``-s/--startrev``.
503 """
592 """
504 timer, fm = gettimer(ui, opts)
593 timer, fm = gettimer(ui, opts)
505 dist = opts['dist']
594 dist = opts['dist']
506 _len = getlen(ui)
595 _len = getlen(ui)
507 def d():
596 def d():
508 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
597 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
509 for x in xrange(startrev, _len(r), dist):
598 for x in xrange(startrev, _len(r), dist):
510 r.revision(r.node(x))
599 r.revision(r.node(x))
511
600
512 timer(d)
601 timer(d)
513 fm.end()
602 fm.end()
514
603
515 @command('perfrevlogrevision', revlogopts + formatteropts +
604 @command('perfrevlogrevision', revlogopts + formatteropts +
516 [('', 'cache', False, 'use caches instead of clearing')],
605 [('', 'cache', False, 'use caches instead of clearing')],
517 '-c|-m|FILE REV')
606 '-c|-m|FILE REV')
518 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
607 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
519 """Benchmark obtaining a revlog revision.
608 """Benchmark obtaining a revlog revision.
520
609
521 Obtaining a revlog revision consists of roughly the following steps:
610 Obtaining a revlog revision consists of roughly the following steps:
522
611
523 1. Compute the delta chain
612 1. Compute the delta chain
524 2. Obtain the raw chunks for that delta chain
613 2. Obtain the raw chunks for that delta chain
525 3. Decompress each raw chunk
614 3. Decompress each raw chunk
526 4. Apply binary patches to obtain fulltext
615 4. Apply binary patches to obtain fulltext
527 5. Verify hash of fulltext
616 5. Verify hash of fulltext
528
617
529 This command measures the time spent in each of these phases.
618 This command measures the time spent in each of these phases.
530 """
619 """
531 if opts.get('changelog') or opts.get('manifest'):
620 if opts.get('changelog') or opts.get('manifest'):
532 file_, rev = None, file_
621 file_, rev = None, file_
533 elif rev is None:
622 elif rev is None:
534 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
623 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
535
624
536 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
625 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
537 node = r.lookup(rev)
626 node = r.lookup(rev)
538 rev = r.rev(node)
627 rev = r.rev(node)
539
628
540 def dodeltachain(rev):
629 def dodeltachain(rev):
541 if not cache:
630 if not cache:
542 r.clearcaches()
631 r.clearcaches()
543 r._deltachain(rev)
632 r._deltachain(rev)
544
633
545 def doread(chain):
634 def doread(chain):
546 if not cache:
635 if not cache:
547 r.clearcaches()
636 r.clearcaches()
548 r._chunkraw(chain[0], chain[-1])
637 r._chunkraw(chain[0], chain[-1])
549
638
550 def dodecompress(data, chain):
639 def dodecompress(data, chain):
551 if not cache:
640 if not cache:
552 r.clearcaches()
641 r.clearcaches()
553
642
554 start = r.start
643 start = r.start
555 length = r.length
644 length = r.length
556 inline = r._inline
645 inline = r._inline
557 iosize = r._io.size
646 iosize = r._io.size
558 buffer = util.buffer
647 buffer = util.buffer
559 offset = start(chain[0])
648 offset = start(chain[0])
560
649
561 for rev in chain:
650 for rev in chain:
562 chunkstart = start(rev)
651 chunkstart = start(rev)
563 if inline:
652 if inline:
564 chunkstart += (rev + 1) * iosize
653 chunkstart += (rev + 1) * iosize
565 chunklength = length(rev)
654 chunklength = length(rev)
566 b = buffer(data, chunkstart - offset, chunklength)
655 b = buffer(data, chunkstart - offset, chunklength)
567 revlog.decompress(b)
656 revlog.decompress(b)
568
657
569 def dopatch(text, bins):
658 def dopatch(text, bins):
570 if not cache:
659 if not cache:
571 r.clearcaches()
660 r.clearcaches()
572 mdiff.patches(text, bins)
661 mdiff.patches(text, bins)
573
662
574 def dohash(text):
663 def dohash(text):
575 if not cache:
664 if not cache:
576 r.clearcaches()
665 r.clearcaches()
577 r._checkhash(text, node, rev)
666 r._checkhash(text, node, rev)
578
667
579 def dorevision():
668 def dorevision():
580 if not cache:
669 if not cache:
581 r.clearcaches()
670 r.clearcaches()
582 r.revision(node)
671 r.revision(node)
583
672
584 chain = r._deltachain(rev)[0]
673 chain = r._deltachain(rev)[0]
585 data = r._chunkraw(chain[0], chain[-1])[1]
674 data = r._chunkraw(chain[0], chain[-1])[1]
586 bins = r._chunks(chain)
675 bins = r._chunks(chain)
587 text = str(bins[0])
676 text = str(bins[0])
588 bins = bins[1:]
677 bins = bins[1:]
589 text = mdiff.patches(text, bins)
678 text = mdiff.patches(text, bins)
590
679
591 benches = [
680 benches = [
592 (lambda: dorevision(), 'full'),
681 (lambda: dorevision(), 'full'),
593 (lambda: dodeltachain(rev), 'deltachain'),
682 (lambda: dodeltachain(rev), 'deltachain'),
594 (lambda: doread(chain), 'read'),
683 (lambda: doread(chain), 'read'),
595 (lambda: dodecompress(data, chain), 'decompress'),
684 (lambda: dodecompress(data, chain), 'decompress'),
596 (lambda: dopatch(text, bins), 'patch'),
685 (lambda: dopatch(text, bins), 'patch'),
597 (lambda: dohash(text), 'hash'),
686 (lambda: dohash(text), 'hash'),
598 ]
687 ]
599
688
600 for fn, title in benches:
689 for fn, title in benches:
601 timer, fm = gettimer(ui, opts)
690 timer, fm = gettimer(ui, opts)
602 timer(fn, title=title)
691 timer(fn, title=title)
603 fm.end()
692 fm.end()
604
693
605 @command('perfrevset',
694 @command('perfrevset',
606 [('C', 'clear', False, 'clear volatile cache between each call.'),
695 [('C', 'clear', False, 'clear volatile cache between each call.'),
607 ('', 'contexts', False, 'obtain changectx for each revision')]
696 ('', 'contexts', False, 'obtain changectx for each revision')]
608 + formatteropts, "REVSET")
697 + formatteropts, "REVSET")
609 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
698 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
610 """benchmark the execution time of a revset
699 """benchmark the execution time of a revset
611
700
612 Use the --clean option if need to evaluate the impact of build volatile
701 Use the --clean option if need to evaluate the impact of build volatile
613 revisions set cache on the revset execution. Volatile cache hold filtered
702 revisions set cache on the revset execution. Volatile cache hold filtered
614 and obsolete related cache."""
703 and obsolete related cache."""
615 timer, fm = gettimer(ui, opts)
704 timer, fm = gettimer(ui, opts)
616 def d():
705 def d():
617 if clear:
706 if clear:
618 repo.invalidatevolatilesets()
707 repo.invalidatevolatilesets()
619 if contexts:
708 if contexts:
620 for ctx in repo.set(expr): pass
709 for ctx in repo.set(expr): pass
621 else:
710 else:
622 for r in repo.revs(expr): pass
711 for r in repo.revs(expr): pass
623 timer(d)
712 timer(d)
624 fm.end()
713 fm.end()
625
714
626 @command('perfvolatilesets', formatteropts)
715 @command('perfvolatilesets', formatteropts)
627 def perfvolatilesets(ui, repo, *names, **opts):
716 def perfvolatilesets(ui, repo, *names, **opts):
628 """benchmark the computation of various volatile set
717 """benchmark the computation of various volatile set
629
718
630 Volatile set computes element related to filtering and obsolescence."""
719 Volatile set computes element related to filtering and obsolescence."""
631 timer, fm = gettimer(ui, opts)
720 timer, fm = gettimer(ui, opts)
632 repo = repo.unfiltered()
721 repo = repo.unfiltered()
633
722
634 def getobs(name):
723 def getobs(name):
635 def d():
724 def d():
636 repo.invalidatevolatilesets()
725 repo.invalidatevolatilesets()
637 obsolete.getrevs(repo, name)
726 obsolete.getrevs(repo, name)
638 return d
727 return d
639
728
640 allobs = sorted(obsolete.cachefuncs)
729 allobs = sorted(obsolete.cachefuncs)
641 if names:
730 if names:
642 allobs = [n for n in allobs if n in names]
731 allobs = [n for n in allobs if n in names]
643
732
644 for name in allobs:
733 for name in allobs:
645 timer(getobs(name), title=name)
734 timer(getobs(name), title=name)
646
735
647 def getfiltered(name):
736 def getfiltered(name):
648 def d():
737 def d():
649 repo.invalidatevolatilesets()
738 repo.invalidatevolatilesets()
650 repoview.filterrevs(repo, name)
739 repoview.filterrevs(repo, name)
651 return d
740 return d
652
741
653 allfilter = sorted(repoview.filtertable)
742 allfilter = sorted(repoview.filtertable)
654 if names:
743 if names:
655 allfilter = [n for n in allfilter if n in names]
744 allfilter = [n for n in allfilter if n in names]
656
745
657 for name in allfilter:
746 for name in allfilter:
658 timer(getfiltered(name), title=name)
747 timer(getfiltered(name), title=name)
659 fm.end()
748 fm.end()
660
749
661 @command('perfbranchmap',
750 @command('perfbranchmap',
662 [('f', 'full', False,
751 [('f', 'full', False,
663 'Includes build time of subset'),
752 'Includes build time of subset'),
664 ] + formatteropts)
753 ] + formatteropts)
665 def perfbranchmap(ui, repo, full=False, **opts):
754 def perfbranchmap(ui, repo, full=False, **opts):
666 """benchmark the update of a branchmap
755 """benchmark the update of a branchmap
667
756
668 This benchmarks the full repo.branchmap() call with read and write disabled
757 This benchmarks the full repo.branchmap() call with read and write disabled
669 """
758 """
670 timer, fm = gettimer(ui, opts)
759 timer, fm = gettimer(ui, opts)
671 def getbranchmap(filtername):
760 def getbranchmap(filtername):
672 """generate a benchmark function for the filtername"""
761 """generate a benchmark function for the filtername"""
673 if filtername is None:
762 if filtername is None:
674 view = repo
763 view = repo
675 else:
764 else:
676 view = repo.filtered(filtername)
765 view = repo.filtered(filtername)
677 def d():
766 def d():
678 if full:
767 if full:
679 view._branchcaches.clear()
768 view._branchcaches.clear()
680 else:
769 else:
681 view._branchcaches.pop(filtername, None)
770 view._branchcaches.pop(filtername, None)
682 view.branchmap()
771 view.branchmap()
683 return d
772 return d
684 # add filter in smaller subset to bigger subset
773 # add filter in smaller subset to bigger subset
685 possiblefilters = set(repoview.filtertable)
774 possiblefilters = set(repoview.filtertable)
686 allfilters = []
775 allfilters = []
687 while possiblefilters:
776 while possiblefilters:
688 for name in possiblefilters:
777 for name in possiblefilters:
689 subset = branchmap.subsettable.get(name)
778 subset = branchmap.subsettable.get(name)
690 if subset not in possiblefilters:
779 if subset not in possiblefilters:
691 break
780 break
692 else:
781 else:
693 assert False, 'subset cycle %s!' % possiblefilters
782 assert False, 'subset cycle %s!' % possiblefilters
694 allfilters.append(name)
783 allfilters.append(name)
695 possiblefilters.remove(name)
784 possiblefilters.remove(name)
696
785
697 # warm the cache
786 # warm the cache
698 if not full:
787 if not full:
699 for name in allfilters:
788 for name in allfilters:
700 repo.filtered(name).branchmap()
789 repo.filtered(name).branchmap()
701 # add unfiltered
790 # add unfiltered
702 allfilters.append(None)
791 allfilters.append(None)
703 oldread = branchmap.read
792 oldread = branchmap.read
704 oldwrite = branchmap.branchcache.write
793 oldwrite = branchmap.branchcache.write
705 try:
794 try:
706 branchmap.read = lambda repo: None
795 branchmap.read = lambda repo: None
707 branchmap.write = lambda repo: None
796 branchmap.write = lambda repo: None
708 for name in allfilters:
797 for name in allfilters:
709 timer(getbranchmap(name), title=str(name))
798 timer(getbranchmap(name), title=str(name))
710 finally:
799 finally:
711 branchmap.read = oldread
800 branchmap.read = oldread
712 branchmap.branchcache.write = oldwrite
801 branchmap.branchcache.write = oldwrite
713 fm.end()
802 fm.end()
714
803
715 @command('perfloadmarkers')
804 @command('perfloadmarkers')
716 def perfloadmarkers(ui, repo):
805 def perfloadmarkers(ui, repo):
717 """benchmark the time to parse the on-disk markers for a repo
806 """benchmark the time to parse the on-disk markers for a repo
718
807
719 Result is the number of markers in the repo."""
808 Result is the number of markers in the repo."""
720 timer, fm = gettimer(ui)
809 timer, fm = gettimer(ui)
721 timer(lambda: len(obsolete.obsstore(repo.svfs)))
810 timer(lambda: len(obsolete.obsstore(repo.svfs)))
722 fm.end()
811 fm.end()
723
812
724 @command('perflrucachedict', formatteropts +
813 @command('perflrucachedict', formatteropts +
725 [('', 'size', 4, 'size of cache'),
814 [('', 'size', 4, 'size of cache'),
726 ('', 'gets', 10000, 'number of key lookups'),
815 ('', 'gets', 10000, 'number of key lookups'),
727 ('', 'sets', 10000, 'number of key sets'),
816 ('', 'sets', 10000, 'number of key sets'),
728 ('', 'mixed', 10000, 'number of mixed mode operations'),
817 ('', 'mixed', 10000, 'number of mixed mode operations'),
729 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
818 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
730 norepo=True)
819 norepo=True)
731 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
820 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
732 mixedgetfreq=50, **opts):
821 mixedgetfreq=50, **opts):
733 def doinit():
822 def doinit():
734 for i in xrange(10000):
823 for i in xrange(10000):
735 util.lrucachedict(size)
824 util.lrucachedict(size)
736
825
737 values = []
826 values = []
738 for i in xrange(size):
827 for i in xrange(size):
739 values.append(random.randint(0, sys.maxint))
828 values.append(random.randint(0, sys.maxint))
740
829
741 # Get mode fills the cache and tests raw lookup performance with no
830 # Get mode fills the cache and tests raw lookup performance with no
742 # eviction.
831 # eviction.
743 getseq = []
832 getseq = []
744 for i in xrange(gets):
833 for i in xrange(gets):
745 getseq.append(random.choice(values))
834 getseq.append(random.choice(values))
746
835
747 def dogets():
836 def dogets():
748 d = util.lrucachedict(size)
837 d = util.lrucachedict(size)
749 for v in values:
838 for v in values:
750 d[v] = v
839 d[v] = v
751 for key in getseq:
840 for key in getseq:
752 value = d[key]
841 value = d[key]
753 value # silence pyflakes warning
842 value # silence pyflakes warning
754
843
755 # Set mode tests insertion speed with cache eviction.
844 # Set mode tests insertion speed with cache eviction.
756 setseq = []
845 setseq = []
757 for i in xrange(sets):
846 for i in xrange(sets):
758 setseq.append(random.randint(0, sys.maxint))
847 setseq.append(random.randint(0, sys.maxint))
759
848
760 def dosets():
849 def dosets():
761 d = util.lrucachedict(size)
850 d = util.lrucachedict(size)
762 for v in setseq:
851 for v in setseq:
763 d[v] = v
852 d[v] = v
764
853
765 # Mixed mode randomly performs gets and sets with eviction.
854 # Mixed mode randomly performs gets and sets with eviction.
766 mixedops = []
855 mixedops = []
767 for i in xrange(mixed):
856 for i in xrange(mixed):
768 r = random.randint(0, 100)
857 r = random.randint(0, 100)
769 if r < mixedgetfreq:
858 if r < mixedgetfreq:
770 op = 0
859 op = 0
771 else:
860 else:
772 op = 1
861 op = 1
773
862
774 mixedops.append((op, random.randint(0, size * 2)))
863 mixedops.append((op, random.randint(0, size * 2)))
775
864
776 def domixed():
865 def domixed():
777 d = util.lrucachedict(size)
866 d = util.lrucachedict(size)
778
867
779 for op, v in mixedops:
868 for op, v in mixedops:
780 if op == 0:
869 if op == 0:
781 try:
870 try:
782 d[v]
871 d[v]
783 except KeyError:
872 except KeyError:
784 pass
873 pass
785 else:
874 else:
786 d[v] = v
875 d[v] = v
787
876
788 benches = [
877 benches = [
789 (doinit, 'init'),
878 (doinit, 'init'),
790 (dogets, 'gets'),
879 (dogets, 'gets'),
791 (dosets, 'sets'),
880 (dosets, 'sets'),
792 (domixed, 'mixed')
881 (domixed, 'mixed')
793 ]
882 ]
794
883
795 for fn, title in benches:
884 for fn, title in benches:
796 timer, fm = gettimer(ui, opts)
885 timer, fm = gettimer(ui, opts)
797 timer(fn, title=title)
886 timer(fn, title=title)
798 fm.end()
887 fm.end()
888
889 def uisetup(ui):
890 if (util.safehasattr(cmdutil, 'openrevlog') and
891 not util.safehasattr(commands, 'debugrevlogopts')):
892 # for "historical portability":
893 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
894 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
895 # openrevlog() should cause failure, because it has been
896 # available since 3.5 (or 49c583ca48c4).
897 def openrevlog(orig, repo, cmd, file_, opts):
898 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
899 raise error.Abort("This version doesn't support --dir option",
900 hint="use 3.5 or later")
901 return orig(repo, cmd, file_, opts)
902 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
@@ -1,334 +1,325
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 # Measure the performance of a list of revsets against multiple revisions
3 # Measure the performance of a list of revsets against multiple revisions
4 # defined by parameter. Checkout one by one and run perfrevset with every
4 # defined by parameter. Checkout one by one and run perfrevset with every
5 # revset in the list to benchmark its performance.
5 # revset in the list to benchmark its performance.
6 #
6 #
7 # You should run this from the root of your mercurial repository.
7 # You should run this from the root of your mercurial repository.
8 #
8 #
9 # call with --help for details
9 # call with --help for details
10
10
11 from __future__ import absolute_import, print_function
11 from __future__ import absolute_import, print_function
12 import math
12 import math
13 import optparse # cannot use argparse, python 2.7 only
13 import os
14 import os
14 import re
15 import re
16 import subprocess
15 import sys
17 import sys
16 from subprocess import (
17 CalledProcessError,
18 check_call,
19 PIPE,
20 Popen,
21 STDOUT,
22 )
23 # cannot use argparse, python 2.7 only
24 from optparse import (
25 OptionParser,
26 )
27
18
28 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
29 'reverse', 'reverse+first', 'reverse+last',
20 'reverse', 'reverse+first', 'reverse+last',
30 'sort', 'sort+first', 'sort+last']
21 'sort', 'sort+first', 'sort+last']
31
22
32 def check_output(*args, **kwargs):
23 def check_output(*args, **kwargs):
33 kwargs.setdefault('stderr', PIPE)
24 kwargs.setdefault('stderr', subprocess.PIPE)
34 kwargs.setdefault('stdout', PIPE)
25 kwargs.setdefault('stdout', subprocess.PIPE)
35 proc = Popen(*args, **kwargs)
26 proc = subprocess.Popen(*args, **kwargs)
36 output, error = proc.communicate()
27 output, error = proc.communicate()
37 if proc.returncode != 0:
28 if proc.returncode != 0:
38 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
29 raise subprocess.CalledProcessError(proc.returncode, ' '.join(args[0]))
39 return output
30 return output
40
31
41 def update(rev):
32 def update(rev):
42 """update the repo to a revision"""
33 """update the repo to a revision"""
43 try:
34 try:
44 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
35 subprocess.check_call(['hg', 'update', '--quiet', '--check', str(rev)])
45 check_output(['make', 'local'],
36 check_output(['make', 'local'],
46 stderr=None) # suppress output except for error/warning
37 stderr=None) # suppress output except for error/warning
47 except CalledProcessError as exc:
38 except subprocess.CalledProcessError as exc:
48 print('update to revision %s failed, aborting'%rev, file=sys.stderr)
39 print('update to revision %s failed, aborting'%rev, file=sys.stderr)
49 sys.exit(exc.returncode)
40 sys.exit(exc.returncode)
50
41
51
42
52 def hg(cmd, repo=None):
43 def hg(cmd, repo=None):
53 """run a mercurial command
44 """run a mercurial command
54
45
55 <cmd> is the list of command + argument,
46 <cmd> is the list of command + argument,
56 <repo> is an optional repository path to run this command in."""
47 <repo> is an optional repository path to run this command in."""
57 fullcmd = ['./hg']
48 fullcmd = ['./hg']
58 if repo is not None:
49 if repo is not None:
59 fullcmd += ['-R', repo]
50 fullcmd += ['-R', repo]
60 fullcmd += ['--config',
51 fullcmd += ['--config',
61 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
52 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
62 fullcmd += cmd
53 fullcmd += cmd
63 return check_output(fullcmd, stderr=STDOUT)
54 return check_output(fullcmd, stderr=subprocess.STDOUT)
64
55
65 def perf(revset, target=None, contexts=False):
56 def perf(revset, target=None, contexts=False):
66 """run benchmark for this very revset"""
57 """run benchmark for this very revset"""
67 try:
58 try:
68 args = ['perfrevset', revset]
59 args = ['perfrevset', revset]
69 if contexts:
60 if contexts:
70 args.append('--contexts')
61 args.append('--contexts')
71 output = hg(args, repo=target)
62 output = hg(args, repo=target)
72 return parseoutput(output)
63 return parseoutput(output)
73 except CalledProcessError as exc:
64 except subprocess.CalledProcessError as exc:
74 print('abort: cannot run revset benchmark: %s'%exc.cmd, file=sys.stderr)
65 print('abort: cannot run revset benchmark: %s'%exc.cmd, file=sys.stderr)
75 if getattr(exc, 'output', None) is None: # no output before 2.7
66 if getattr(exc, 'output', None) is None: # no output before 2.7
76 print('(no output)', file=sys.stderr)
67 print('(no output)', file=sys.stderr)
77 else:
68 else:
78 print(exc.output, file=sys.stderr)
69 print(exc.output, file=sys.stderr)
79 return None
70 return None
80
71
81 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
72 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
82 'sys (\d+.\d+) \(best of (\d+)\)')
73 'sys (\d+.\d+) \(best of (\d+)\)')
83
74
84 def parseoutput(output):
75 def parseoutput(output):
85 """parse a textual output into a dict
76 """parse a textual output into a dict
86
77
87 We cannot just use json because we want to compare with old
78 We cannot just use json because we want to compare with old
88 versions of Mercurial that may not support json output.
79 versions of Mercurial that may not support json output.
89 """
80 """
90 match = outputre.search(output)
81 match = outputre.search(output)
91 if not match:
82 if not match:
92 print('abort: invalid output:', file=sys.stderr)
83 print('abort: invalid output:', file=sys.stderr)
93 print(output, file=sys.stderr)
84 print(output, file=sys.stderr)
94 sys.exit(1)
85 sys.exit(1)
95 return {'comb': float(match.group(2)),
86 return {'comb': float(match.group(2)),
96 'count': int(match.group(5)),
87 'count': int(match.group(5)),
97 'sys': float(match.group(3)),
88 'sys': float(match.group(3)),
98 'user': float(match.group(4)),
89 'user': float(match.group(4)),
99 'wall': float(match.group(1)),
90 'wall': float(match.group(1)),
100 }
91 }
101
92
102 def printrevision(rev):
93 def printrevision(rev):
103 """print data about a revision"""
94 """print data about a revision"""
104 sys.stdout.write("Revision ")
95 sys.stdout.write("Revision ")
105 sys.stdout.flush()
96 sys.stdout.flush()
106 check_call(['hg', 'log', '--rev', str(rev), '--template',
97 subprocess.check_call(['hg', 'log', '--rev', str(rev), '--template',
107 '{if(tags, " ({tags})")} '
98 '{if(tags, " ({tags})")} '
108 '{rev}:{node|short}: {desc|firstline}\n'])
99 '{rev}:{node|short}: {desc|firstline}\n'])
109
100
110 def idxwidth(nbidx):
101 def idxwidth(nbidx):
111 """return the max width of number used for index
102 """return the max width of number used for index
112
103
113 This is similar to log10(nbidx), but we use custom code here
104 This is similar to log10(nbidx), but we use custom code here
114 because we start with zero and we'd rather not deal with all the
105 because we start with zero and we'd rather not deal with all the
115 extra rounding business that log10 would imply.
106 extra rounding business that log10 would imply.
116 """
107 """
117 nbidx -= 1 # starts at 0
108 nbidx -= 1 # starts at 0
118 idxwidth = 0
109 idxwidth = 0
119 while nbidx:
110 while nbidx:
120 idxwidth += 1
111 idxwidth += 1
121 nbidx //= 10
112 nbidx //= 10
122 if not idxwidth:
113 if not idxwidth:
123 idxwidth = 1
114 idxwidth = 1
124 return idxwidth
115 return idxwidth
125
116
126 def getfactor(main, other, field, sensitivity=0.05):
117 def getfactor(main, other, field, sensitivity=0.05):
127 """return the relative factor between values for 'field' in main and other
118 """return the relative factor between values for 'field' in main and other
128
119
129 Return None if the factor is insignificant (less than <sensitivity>
120 Return None if the factor is insignificant (less than <sensitivity>
130 variation)."""
121 variation)."""
131 factor = 1
122 factor = 1
132 if main is not None:
123 if main is not None:
133 factor = other[field] / main[field]
124 factor = other[field] / main[field]
134 low, high = 1 - sensitivity, 1 + sensitivity
125 low, high = 1 - sensitivity, 1 + sensitivity
135 if (low < factor < high):
126 if (low < factor < high):
136 return None
127 return None
137 return factor
128 return factor
138
129
139 def formatfactor(factor):
130 def formatfactor(factor):
140 """format a factor into a 4 char string
131 """format a factor into a 4 char string
141
132
142 22%
133 22%
143 156%
134 156%
144 x2.4
135 x2.4
145 x23
136 x23
146 x789
137 x789
147 x1e4
138 x1e4
148 x5x7
139 x5x7
149
140
150 """
141 """
151 if factor is None:
142 if factor is None:
152 return ' '
143 return ' '
153 elif factor < 2:
144 elif factor < 2:
154 return '%3i%%' % (factor * 100)
145 return '%3i%%' % (factor * 100)
155 elif factor < 10:
146 elif factor < 10:
156 return 'x%3.1f' % factor
147 return 'x%3.1f' % factor
157 elif factor < 1000:
148 elif factor < 1000:
158 return '%4s' % ('x%i' % factor)
149 return '%4s' % ('x%i' % factor)
159 else:
150 else:
160 order = int(math.log(factor)) + 1
151 order = int(math.log(factor)) + 1
161 while 1 < math.log(factor):
152 while 1 < math.log(factor):
162 factor //= 0
153 factor //= 0
163 return 'x%ix%i' % (factor, order)
154 return 'x%ix%i' % (factor, order)
164
155
165 def formattiming(value):
156 def formattiming(value):
166 """format a value to strictly 8 char, dropping some precision if needed"""
157 """format a value to strictly 8 char, dropping some precision if needed"""
167 if value < 10**7:
158 if value < 10**7:
168 return ('%.6f' % value)[:8]
159 return ('%.6f' % value)[:8]
169 else:
160 else:
170 # value is HUGE very unlikely to happen (4+ month run)
161 # value is HUGE very unlikely to happen (4+ month run)
171 return '%i' % value
162 return '%i' % value
172
163
173 _marker = object()
164 _marker = object()
174 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
165 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
175 """print a line of result to stdout"""
166 """print a line of result to stdout"""
176 mask = '%%0%ii) %%s' % idxwidth(maxidx)
167 mask = '%%0%ii) %%s' % idxwidth(maxidx)
177
168
178 out = []
169 out = []
179 for var in variants:
170 for var in variants:
180 if data[var] is None:
171 if data[var] is None:
181 out.append('error ')
172 out.append('error ')
182 out.append(' ' * 4)
173 out.append(' ' * 4)
183 continue
174 continue
184 out.append(formattiming(data[var]['wall']))
175 out.append(formattiming(data[var]['wall']))
185 if reference is not _marker:
176 if reference is not _marker:
186 factor = None
177 factor = None
187 if reference is not None:
178 if reference is not None:
188 factor = getfactor(reference[var], data[var], 'wall')
179 factor = getfactor(reference[var], data[var], 'wall')
189 out.append(formatfactor(factor))
180 out.append(formatfactor(factor))
190 if verbose:
181 if verbose:
191 out.append(formattiming(data[var]['comb']))
182 out.append(formattiming(data[var]['comb']))
192 out.append(formattiming(data[var]['user']))
183 out.append(formattiming(data[var]['user']))
193 out.append(formattiming(data[var]['sys']))
184 out.append(formattiming(data[var]['sys']))
194 out.append('%6d' % data[var]['count'])
185 out.append('%6d' % data[var]['count'])
195 print(mask % (idx, ' '.join(out)))
186 print(mask % (idx, ' '.join(out)))
196
187
197 def printheader(variants, maxidx, verbose=False, relative=False):
188 def printheader(variants, maxidx, verbose=False, relative=False):
198 header = [' ' * (idxwidth(maxidx) + 1)]
189 header = [' ' * (idxwidth(maxidx) + 1)]
199 for var in variants:
190 for var in variants:
200 if not var:
191 if not var:
201 var = 'iter'
192 var = 'iter'
202 if 8 < len(var):
193 if 8 < len(var):
203 var = var[:3] + '..' + var[-3:]
194 var = var[:3] + '..' + var[-3:]
204 header.append('%-8s' % var)
195 header.append('%-8s' % var)
205 if relative:
196 if relative:
206 header.append(' ')
197 header.append(' ')
207 if verbose:
198 if verbose:
208 header.append('%-8s' % 'comb')
199 header.append('%-8s' % 'comb')
209 header.append('%-8s' % 'user')
200 header.append('%-8s' % 'user')
210 header.append('%-8s' % 'sys')
201 header.append('%-8s' % 'sys')
211 header.append('%6s' % 'count')
202 header.append('%6s' % 'count')
212 print(' '.join(header))
203 print(' '.join(header))
213
204
214 def getrevs(spec):
205 def getrevs(spec):
215 """get the list of rev matched by a revset"""
206 """get the list of rev matched by a revset"""
216 try:
207 try:
217 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
208 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
218 except CalledProcessError as exc:
209 except subprocess.CalledProcessError as exc:
219 print("abort, can't get revision from %s"%spec, file=sys.stderr)
210 print("abort, can't get revision from %s"%spec, file=sys.stderr)
220 sys.exit(exc.returncode)
211 sys.exit(exc.returncode)
221 return [r for r in out.split() if r]
212 return [r for r in out.split() if r]
222
213
223
214
224 def applyvariants(revset, variant):
215 def applyvariants(revset, variant):
225 if variant == 'plain':
216 if variant == 'plain':
226 return revset
217 return revset
227 for var in variant.split('+'):
218 for var in variant.split('+'):
228 revset = '%s(%s)' % (var, revset)
219 revset = '%s(%s)' % (var, revset)
229 return revset
220 return revset
230
221
231 helptext="""This script will run multiple variants of provided revsets using
222 helptext="""This script will run multiple variants of provided revsets using
232 different revisions in your mercurial repository. After the benchmark are run
223 different revisions in your mercurial repository. After the benchmark are run
233 summary output is provided. Use it to demonstrate speed improvements or pin
224 summary output is provided. Use it to demonstrate speed improvements or pin
234 point regressions. Revsets to run are specified in a file (or from stdin), one
225 point regressions. Revsets to run are specified in a file (or from stdin), one
235 revsets per line. Line starting with '#' will be ignored, allowing insertion of
226 revsets per line. Line starting with '#' will be ignored, allowing insertion of
236 comments."""
227 comments."""
237 parser = OptionParser(usage="usage: %prog [options] <revs>",
228 parser = optparse.OptionParser(usage="usage: %prog [options] <revs>",
238 description=helptext)
229 description=helptext)
239 parser.add_option("-f", "--file",
230 parser.add_option("-f", "--file",
240 help="read revset from FILE (stdin if omitted)",
231 help="read revset from FILE (stdin if omitted)",
241 metavar="FILE")
232 metavar="FILE")
242 parser.add_option("-R", "--repo",
233 parser.add_option("-R", "--repo",
243 help="run benchmark on REPO", metavar="REPO")
234 help="run benchmark on REPO", metavar="REPO")
244
235
245 parser.add_option("-v", "--verbose",
236 parser.add_option("-v", "--verbose",
246 action='store_true',
237 action='store_true',
247 help="display all timing data (not just best total time)")
238 help="display all timing data (not just best total time)")
248
239
249 parser.add_option("", "--variants",
240 parser.add_option("", "--variants",
250 default=','.join(DEFAULTVARIANTS),
241 default=','.join(DEFAULTVARIANTS),
251 help="comma separated list of variant to test "
242 help="comma separated list of variant to test "
252 "(eg: plain,min,sorted) (plain = no modification)")
243 "(eg: plain,min,sorted) (plain = no modification)")
253 parser.add_option('', '--contexts',
244 parser.add_option('', '--contexts',
254 action='store_true',
245 action='store_true',
255 help='obtain changectx from results instead of integer revs')
246 help='obtain changectx from results instead of integer revs')
256
247
257 (options, args) = parser.parse_args()
248 (options, args) = parser.parse_args()
258
249
259 if not args:
250 if not args:
260 parser.print_help()
251 parser.print_help()
261 sys.exit(255)
252 sys.exit(255)
262
253
263 # the directory where both this script and the perf.py extension live.
254 # the directory where both this script and the perf.py extension live.
264 contribdir = os.path.dirname(__file__)
255 contribdir = os.path.dirname(__file__)
265
256
266 revsetsfile = sys.stdin
257 revsetsfile = sys.stdin
267 if options.file:
258 if options.file:
268 revsetsfile = open(options.file)
259 revsetsfile = open(options.file)
269
260
270 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
261 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
271 revsets = [l for l in revsets if l]
262 revsets = [l for l in revsets if l]
272
263
273 print("Revsets to benchmark")
264 print("Revsets to benchmark")
274 print("----------------------------")
265 print("----------------------------")
275
266
276 for idx, rset in enumerate(revsets):
267 for idx, rset in enumerate(revsets):
277 print("%i) %s" % (idx, rset))
268 print("%i) %s" % (idx, rset))
278
269
279 print("----------------------------")
270 print("----------------------------")
280 print()
271 print()
281
272
282 revs = []
273 revs = []
283 for a in args:
274 for a in args:
284 revs.extend(getrevs(a))
275 revs.extend(getrevs(a))
285
276
286 variants = options.variants.split(',')
277 variants = options.variants.split(',')
287
278
288 results = []
279 results = []
289 for r in revs:
280 for r in revs:
290 print("----------------------------")
281 print("----------------------------")
291 printrevision(r)
282 printrevision(r)
292 print("----------------------------")
283 print("----------------------------")
293 update(r)
284 update(r)
294 res = []
285 res = []
295 results.append(res)
286 results.append(res)
296 printheader(variants, len(revsets), verbose=options.verbose)
287 printheader(variants, len(revsets), verbose=options.verbose)
297 for idx, rset in enumerate(revsets):
288 for idx, rset in enumerate(revsets):
298 varres = {}
289 varres = {}
299 for var in variants:
290 for var in variants:
300 varrset = applyvariants(rset, var)
291 varrset = applyvariants(rset, var)
301 data = perf(varrset, target=options.repo, contexts=options.contexts)
292 data = perf(varrset, target=options.repo, contexts=options.contexts)
302 varres[var] = data
293 varres[var] = data
303 res.append(varres)
294 res.append(varres)
304 printresult(variants, idx, varres, len(revsets),
295 printresult(variants, idx, varres, len(revsets),
305 verbose=options.verbose)
296 verbose=options.verbose)
306 sys.stdout.flush()
297 sys.stdout.flush()
307 print("----------------------------")
298 print("----------------------------")
308
299
309
300
310 print("""
301 print("""
311
302
312 Result by revset
303 Result by revset
313 ================
304 ================
314 """)
305 """)
315
306
316 print('Revision:')
307 print('Revision:')
317 for idx, rev in enumerate(revs):
308 for idx, rev in enumerate(revs):
318 sys.stdout.write('%i) ' % idx)
309 sys.stdout.write('%i) ' % idx)
319 sys.stdout.flush()
310 sys.stdout.flush()
320 printrevision(rev)
311 printrevision(rev)
321
312
322 print()
313 print()
323 print()
314 print()
324
315
325 for ridx, rset in enumerate(revsets):
316 for ridx, rset in enumerate(revsets):
326
317
327 print("revset #%i: %s" % (ridx, rset))
318 print("revset #%i: %s" % (ridx, rset))
328 printheader(variants, len(results), verbose=options.verbose, relative=True)
319 printheader(variants, len(results), verbose=options.verbose, relative=True)
329 ref = None
320 ref = None
330 for idx, data in enumerate(results):
321 for idx, data in enumerate(results):
331 printresult(variants, idx, data[ridx], len(results),
322 printresult(variants, idx, data[ridx], len(results),
332 verbose=options.verbose, reference=ref)
323 verbose=options.verbose, reference=ref)
333 ref = data[ridx]
324 ref = data[ridx]
334 print()
325 print()
@@ -1,515 +1,516
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
49 from mercurial.i18n import _
50 from mercurial.node import (
51 nullid,
52 nullrev,
53 short,
54 )
48 from mercurial import (
55 from mercurial import (
49 cmdutil,
56 cmdutil,
50 context,
57 context,
51 error,
58 error,
52 hg,
59 hg,
53 patch,
60 patch,
54 scmutil,
61 scmutil,
55 util,
62 util,
56 )
63 )
57 from mercurial.i18n import _
58 from mercurial.node import (
59 nullid,
60 nullrev,
61 short,
62 )
63
64
64 # Note for extension authors: ONLY specify testedwith = 'internal' for
65 # Note for extension authors: ONLY specify testedwith = 'internal' for
65 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
66 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
66 # 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
67 # leave the attribute unspecified.
68 # leave the attribute unspecified.
68 testedwith = 'internal'
69 testedwith = 'internal'
69
70
70 cmdtable = {}
71 cmdtable = {}
71 command = cmdutil.command(cmdtable)
72 command = cmdutil.command(cmdtable)
72
73
73 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
74 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
74
75
75 def zerodict():
76 def zerodict():
76 return collections.defaultdict(lambda: 0)
77 return collections.defaultdict(lambda: 0)
77
78
78 def roundto(x, k):
79 def roundto(x, k):
79 if x > k * 2:
80 if x > k * 2:
80 return int(round(x / float(k)) * k)
81 return int(round(x / float(k)) * k)
81 return int(round(x))
82 return int(round(x))
82
83
83 def parsegitdiff(lines):
84 def parsegitdiff(lines):
84 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
85 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
85 binary = False
86 binary = False
86 for line in lines:
87 for line in lines:
87 start = line[:6]
88 start = line[:6]
88 if start == 'diff -':
89 if start == 'diff -':
89 if filename:
90 if filename:
90 yield filename, mar, lineadd, lineremove, binary
91 yield filename, mar, lineadd, lineremove, binary
91 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
92 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
92 filename = patch.gitre.match(line).group(1)
93 filename = patch.gitre.match(line).group(1)
93 elif start in newfile:
94 elif start in newfile:
94 mar = 'a'
95 mar = 'a'
95 elif start == 'GIT bi':
96 elif start == 'GIT bi':
96 binary = True
97 binary = True
97 elif start == 'delete':
98 elif start == 'delete':
98 mar = 'r'
99 mar = 'r'
99 elif start:
100 elif start:
100 s = start[0]
101 s = start[0]
101 if s == '-' and not line.startswith('--- '):
102 if s == '-' and not line.startswith('--- '):
102 lineremove += 1
103 lineremove += 1
103 elif s == '+' and not line.startswith('+++ '):
104 elif s == '+' and not line.startswith('+++ '):
104 lineadd[roundto(len(line) - 1, 5)] += 1
105 lineadd[roundto(len(line) - 1, 5)] += 1
105 if filename:
106 if filename:
106 yield filename, mar, lineadd, lineremove, binary
107 yield filename, mar, lineadd, lineremove, binary
107
108
108 @command('analyze',
109 @command('analyze',
109 [('o', 'output', '', _('write output to given file'), _('FILE')),
110 [('o', 'output', '', _('write output to given file'), _('FILE')),
110 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
111 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
111 _('hg analyze'), optionalrepo=True)
112 _('hg analyze'), optionalrepo=True)
112 def analyze(ui, repo, *revs, **opts):
113 def analyze(ui, repo, *revs, **opts):
113 '''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
114
115
115 This command examines every changeset in the given range (or all
116 This command examines every changeset in the given range (or all
116 of history if none are specified) and creates a simple statistical
117 of history if none are specified) and creates a simple statistical
117 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
118 structure of the repository as checked out.
119 structure of the repository as checked out.
119
120
120 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
121 :hg:`synthesize` to create or augment a repository with synthetic
122 :hg:`synthesize` to create or augment a repository with synthetic
122 commits that have a structure that is statistically similar to the
123 commits that have a structure that is statistically similar to the
123 analyzed repository.
124 analyzed repository.
124 '''
125 '''
125 root = repo.root
126 root = repo.root
126 if not root.endswith(os.path.sep):
127 if not root.endswith(os.path.sep):
127 root += os.path.sep
128 root += os.path.sep
128
129
129 revs = list(revs)
130 revs = list(revs)
130 revs.extend(opts['rev'])
131 revs.extend(opts['rev'])
131 if not revs:
132 if not revs:
132 revs = [':']
133 revs = [':']
133
134
134 output = opts['output']
135 output = opts['output']
135 if not output:
136 if not output:
136 output = os.path.basename(root) + '.json'
137 output = os.path.basename(root) + '.json'
137
138
138 if output == '-':
139 if output == '-':
139 fp = sys.stdout
140 fp = sys.stdout
140 else:
141 else:
141 fp = open(output, 'w')
142 fp = open(output, 'w')
142
143
143 # 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.
144 def onerror(e):
145 def onerror(e):
145 ui.warn(_('error walking directory structure: %s\n') % e)
146 ui.warn(_('error walking directory structure: %s\n') % e)
146
147
147 dirs = {}
148 dirs = {}
148 rootprefixlen = len(root)
149 rootprefixlen = len(root)
149 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
150 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
150 dirpathfromroot = dirpath[rootprefixlen:]
151 dirpathfromroot = dirpath[rootprefixlen:]
151 dirs[dirpathfromroot] = len(filenames)
152 dirs[dirpathfromroot] = len(filenames)
152 if '.hg' in dirnames:
153 if '.hg' in dirnames:
153 dirnames.remove('.hg')
154 dirnames.remove('.hg')
154
155
155 lineschanged = zerodict()
156 lineschanged = zerodict()
156 children = zerodict()
157 children = zerodict()
157 p1distance = zerodict()
158 p1distance = zerodict()
158 p2distance = zerodict()
159 p2distance = zerodict()
159 linesinfilesadded = zerodict()
160 linesinfilesadded = zerodict()
160 fileschanged = zerodict()
161 fileschanged = zerodict()
161 filesadded = zerodict()
162 filesadded = zerodict()
162 filesremoved = zerodict()
163 filesremoved = zerodict()
163 linelengths = zerodict()
164 linelengths = zerodict()
164 interarrival = zerodict()
165 interarrival = zerodict()
165 parents = zerodict()
166 parents = zerodict()
166 dirsadded = zerodict()
167 dirsadded = zerodict()
167 tzoffset = zerodict()
168 tzoffset = zerodict()
168
169
169 # If a mercurial repo is available, also model the commit history.
170 # If a mercurial repo is available, also model the commit history.
170 if repo:
171 if repo:
171 revs = scmutil.revrange(repo, revs)
172 revs = scmutil.revrange(repo, revs)
172 revs.sort()
173 revs.sort()
173
174
174 progress = ui.progress
175 progress = ui.progress
175 _analyzing = _('analyzing')
176 _analyzing = _('analyzing')
176 _changesets = _('changesets')
177 _changesets = _('changesets')
177 _total = len(revs)
178 _total = len(revs)
178
179
179 for i, rev in enumerate(revs):
180 for i, rev in enumerate(revs):
180 progress(_analyzing, i, unit=_changesets, total=_total)
181 progress(_analyzing, i, unit=_changesets, total=_total)
181 ctx = repo[rev]
182 ctx = repo[rev]
182 pl = ctx.parents()
183 pl = ctx.parents()
183 pctx = pl[0]
184 pctx = pl[0]
184 prev = pctx.rev()
185 prev = pctx.rev()
185 children[prev] += 1
186 children[prev] += 1
186 p1distance[rev - prev] += 1
187 p1distance[rev - prev] += 1
187 parents[len(pl)] += 1
188 parents[len(pl)] += 1
188 tzoffset[ctx.date()[1]] += 1
189 tzoffset[ctx.date()[1]] += 1
189 if len(pl) > 1:
190 if len(pl) > 1:
190 p2distance[rev - pl[1].rev()] += 1
191 p2distance[rev - pl[1].rev()] += 1
191 if prev == rev - 1:
192 if prev == rev - 1:
192 lastctx = pctx
193 lastctx = pctx
193 else:
194 else:
194 lastctx = repo[rev - 1]
195 lastctx = repo[rev - 1]
195 if lastctx.rev() != nullrev:
196 if lastctx.rev() != nullrev:
196 timedelta = ctx.date()[0] - lastctx.date()[0]
197 timedelta = ctx.date()[0] - lastctx.date()[0]
197 interarrival[roundto(timedelta, 300)] += 1
198 interarrival[roundto(timedelta, 300)] += 1
198 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)), [])
199 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
200 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
200 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
201 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
201 if isbin:
202 if isbin:
202 continue
203 continue
203 added = sum(lineadd.itervalues(), 0)
204 added = sum(lineadd.itervalues(), 0)
204 if mar == 'm':
205 if mar == 'm':
205 if added and lineremove:
206 if added and lineremove:
206 lineschanged[roundto(added, 5),
207 lineschanged[roundto(added, 5),
207 roundto(lineremove, 5)] += 1
208 roundto(lineremove, 5)] += 1
208 filechanges += 1
209 filechanges += 1
209 elif mar == 'a':
210 elif mar == 'a':
210 fileadds += 1
211 fileadds += 1
211 if '/' in filename:
212 if '/' in filename:
212 filedir = filename.rsplit('/', 1)[0]
213 filedir = filename.rsplit('/', 1)[0]
213 if filedir not in pctx.dirs():
214 if filedir not in pctx.dirs():
214 diradds += 1
215 diradds += 1
215 linesinfilesadded[roundto(added, 5)] += 1
216 linesinfilesadded[roundto(added, 5)] += 1
216 elif mar == 'r':
217 elif mar == 'r':
217 fileremoves += 1
218 fileremoves += 1
218 for length, count in lineadd.iteritems():
219 for length, count in lineadd.iteritems():
219 linelengths[length] += count
220 linelengths[length] += count
220 fileschanged[filechanges] += 1
221 fileschanged[filechanges] += 1
221 filesadded[fileadds] += 1
222 filesadded[fileadds] += 1
222 dirsadded[diradds] += 1
223 dirsadded[diradds] += 1
223 filesremoved[fileremoves] += 1
224 filesremoved[fileremoves] += 1
224
225
225 invchildren = zerodict()
226 invchildren = zerodict()
226
227
227 for rev, count in children.iteritems():
228 for rev, count in children.iteritems():
228 invchildren[count] += 1
229 invchildren[count] += 1
229
230
230 if output != '-':
231 if output != '-':
231 ui.status(_('writing output to %s\n') % output)
232 ui.status(_('writing output to %s\n') % output)
232
233
233 def pronk(d):
234 def pronk(d):
234 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
235 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
235
236
236 json.dump({'revs': len(revs),
237 json.dump({'revs': len(revs),
237 'initdirs': pronk(dirs),
238 'initdirs': pronk(dirs),
238 'lineschanged': pronk(lineschanged),
239 'lineschanged': pronk(lineschanged),
239 'children': pronk(invchildren),
240 'children': pronk(invchildren),
240 'fileschanged': pronk(fileschanged),
241 'fileschanged': pronk(fileschanged),
241 'filesadded': pronk(filesadded),
242 'filesadded': pronk(filesadded),
242 'linesinfilesadded': pronk(linesinfilesadded),
243 'linesinfilesadded': pronk(linesinfilesadded),
243 'dirsadded': pronk(dirsadded),
244 'dirsadded': pronk(dirsadded),
244 'filesremoved': pronk(filesremoved),
245 'filesremoved': pronk(filesremoved),
245 'linelengths': pronk(linelengths),
246 'linelengths': pronk(linelengths),
246 'parents': pronk(parents),
247 'parents': pronk(parents),
247 'p1distance': pronk(p1distance),
248 'p1distance': pronk(p1distance),
248 'p2distance': pronk(p2distance),
249 'p2distance': pronk(p2distance),
249 'interarrival': pronk(interarrival),
250 'interarrival': pronk(interarrival),
250 'tzoffset': pronk(tzoffset),
251 'tzoffset': pronk(tzoffset),
251 },
252 },
252 fp)
253 fp)
253 fp.close()
254 fp.close()
254
255
255 @command('synthesize',
256 @command('synthesize',
256 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
257 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
257 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
258 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
258 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
259 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
259 _('hg synthesize [OPTION].. DESCFILE'))
260 _('hg synthesize [OPTION].. DESCFILE'))
260 def synthesize(ui, repo, descpath, **opts):
261 def synthesize(ui, repo, descpath, **opts):
261 '''synthesize commits based on a model of an existing repository
262 '''synthesize commits based on a model of an existing repository
262
263
263 The model must have been generated by :hg:`analyze`. Commits will
264 The model must have been generated by :hg:`analyze`. Commits will
264 be generated randomly according to the probabilities described in
265 be generated randomly according to the probabilities described in
265 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
266 the given number files following the modeled repository's directory
267 the given number files following the modeled repository's directory
267 structure.
268 structure.
268
269
269 When synthesizing new content, commit descriptions, and user
270 When synthesizing new content, commit descriptions, and user
270 names, words will be chosen randomly from a dictionary that is
271 names, words will be chosen randomly from a dictionary that is
271 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
272 path to an alternate dictionary to use.
273 path to an alternate dictionary to use.
273 '''
274 '''
274 try:
275 try:
275 fp = hg.openpath(ui, descpath)
276 fp = hg.openpath(ui, descpath)
276 except Exception as err:
277 except Exception as err:
277 raise error.Abort('%s: %s' % (descpath, err[0].strerror))
278 raise error.Abort('%s: %s' % (descpath, err[0].strerror))
278 desc = json.load(fp)
279 desc = json.load(fp)
279 fp.close()
280 fp.close()
280
281
281 def cdf(l):
282 def cdf(l):
282 if not l:
283 if not l:
283 return [], []
284 return [], []
284 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))
285 t = float(sum(probs, 0))
286 t = float(sum(probs, 0))
286 s, cdfs = 0, []
287 s, cdfs = 0, []
287 for v in probs:
288 for v in probs:
288 s += v
289 s += v
289 cdfs.append(s / t)
290 cdfs.append(s / t)
290 return vals, cdfs
291 return vals, cdfs
291
292
292 lineschanged = cdf(desc['lineschanged'])
293 lineschanged = cdf(desc['lineschanged'])
293 fileschanged = cdf(desc['fileschanged'])
294 fileschanged = cdf(desc['fileschanged'])
294 filesadded = cdf(desc['filesadded'])
295 filesadded = cdf(desc['filesadded'])
295 dirsadded = cdf(desc['dirsadded'])
296 dirsadded = cdf(desc['dirsadded'])
296 filesremoved = cdf(desc['filesremoved'])
297 filesremoved = cdf(desc['filesremoved'])
297 linelengths = cdf(desc['linelengths'])
298 linelengths = cdf(desc['linelengths'])
298 parents = cdf(desc['parents'])
299 parents = cdf(desc['parents'])
299 p1distance = cdf(desc['p1distance'])
300 p1distance = cdf(desc['p1distance'])
300 p2distance = cdf(desc['p2distance'])
301 p2distance = cdf(desc['p2distance'])
301 interarrival = cdf(desc['interarrival'])
302 interarrival = cdf(desc['interarrival'])
302 linesinfilesadded = cdf(desc['linesinfilesadded'])
303 linesinfilesadded = cdf(desc['linesinfilesadded'])
303 tzoffset = cdf(desc['tzoffset'])
304 tzoffset = cdf(desc['tzoffset'])
304
305
305 dictfile = opts.get('dict') or '/usr/share/dict/words'
306 dictfile = opts.get('dict') or '/usr/share/dict/words'
306 try:
307 try:
307 fp = open(dictfile, 'rU')
308 fp = open(dictfile, 'rU')
308 except IOError as err:
309 except IOError as err:
309 raise error.Abort('%s: %s' % (dictfile, err.strerror))
310 raise error.Abort('%s: %s' % (dictfile, err.strerror))
310 words = fp.read().splitlines()
311 words = fp.read().splitlines()
311 fp.close()
312 fp.close()
312
313
313 initdirs = {}
314 initdirs = {}
314 if desc['initdirs']:
315 if desc['initdirs']:
315 for k, v in desc['initdirs']:
316 for k, v in desc['initdirs']:
316 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
317 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
317 initdirs = renamedirs(initdirs, words)
318 initdirs = renamedirs(initdirs, words)
318 initdirscdf = cdf(initdirs)
319 initdirscdf = cdf(initdirs)
319
320
320 def pick(cdf):
321 def pick(cdf):
321 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
322 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
322
323
323 def pickpath():
324 def pickpath():
324 return os.path.join(pick(initdirscdf), random.choice(words))
325 return os.path.join(pick(initdirscdf), random.choice(words))
325
326
326 def makeline(minimum=0):
327 def makeline(minimum=0):
327 total = max(minimum, pick(linelengths))
328 total = max(minimum, pick(linelengths))
328 c, l = 0, []
329 c, l = 0, []
329 while c < total:
330 while c < total:
330 w = random.choice(words)
331 w = random.choice(words)
331 c += len(w) + 1
332 c += len(w) + 1
332 l.append(w)
333 l.append(w)
333 return ' '.join(l)
334 return ' '.join(l)
334
335
335 wlock = repo.wlock()
336 wlock = repo.wlock()
336 lock = repo.lock()
337 lock = repo.lock()
337
338
338 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
339 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
339
340
340 progress = ui.progress
341 progress = ui.progress
341 _synthesizing = _('synthesizing')
342 _synthesizing = _('synthesizing')
342 _files = _('initial files')
343 _files = _('initial files')
343 _changesets = _('changesets')
344 _changesets = _('changesets')
344
345
345 # Synthesize a single initial revision adding files to the repo according
346 # Synthesize a single initial revision adding files to the repo according
346 # to the modeled directory structure.
347 # to the modeled directory structure.
347 initcount = int(opts['initfiles'])
348 initcount = int(opts['initfiles'])
348 if initcount and initdirs:
349 if initcount and initdirs:
349 pctx = repo[None].parents()[0]
350 pctx = repo[None].parents()[0]
350 dirs = set(pctx.dirs())
351 dirs = set(pctx.dirs())
351 files = {}
352 files = {}
352
353
353 def validpath(path):
354 def validpath(path):
354 # Don't pick filenames which are already directory names.
355 # Don't pick filenames which are already directory names.
355 if path in dirs:
356 if path in dirs:
356 return False
357 return False
357 # Don't pick directories which were used as file names.
358 # Don't pick directories which were used as file names.
358 while path:
359 while path:
359 if path in files:
360 if path in files:
360 return False
361 return False
361 path = os.path.dirname(path)
362 path = os.path.dirname(path)
362 return True
363 return True
363
364
364 for i in xrange(0, initcount):
365 for i in xrange(0, initcount):
365 ui.progress(_synthesizing, i, unit=_files, total=initcount)
366 ui.progress(_synthesizing, i, unit=_files, total=initcount)
366
367
367 path = pickpath()
368 path = pickpath()
368 while not validpath(path):
369 while not validpath(path):
369 path = pickpath()
370 path = pickpath()
370 data = '%s contents\n' % path
371 data = '%s contents\n' % path
371 files[path] = context.memfilectx(repo, path, data)
372 files[path] = context.memfilectx(repo, path, data)
372 dir = os.path.dirname(path)
373 dir = os.path.dirname(path)
373 while dir and dir not in dirs:
374 while dir and dir not in dirs:
374 dirs.add(dir)
375 dirs.add(dir)
375 dir = os.path.dirname(dir)
376 dir = os.path.dirname(dir)
376
377
377 def filectxfn(repo, memctx, path):
378 def filectxfn(repo, memctx, path):
378 return files[path]
379 return files[path]
379
380
380 ui.progress(_synthesizing, None)
381 ui.progress(_synthesizing, None)
381 message = 'synthesized wide repo with %d files' % (len(files),)
382 message = 'synthesized wide repo with %d files' % (len(files),)
382 mc = context.memctx(repo, [pctx.node(), nullid], message,
383 mc = context.memctx(repo, [pctx.node(), nullid], message,
383 files.iterkeys(), filectxfn, ui.username(),
384 files.iterkeys(), filectxfn, ui.username(),
384 '%d %d' % util.makedate())
385 '%d %d' % util.makedate())
385 initnode = mc.commit()
386 initnode = mc.commit()
386 if ui.debugflag:
387 if ui.debugflag:
387 hexfn = hex
388 hexfn = hex
388 else:
389 else:
389 hexfn = short
390 hexfn = short
390 ui.status(_('added commit %s with %d files\n')
391 ui.status(_('added commit %s with %d files\n')
391 % (hexfn(initnode), len(files)))
392 % (hexfn(initnode), len(files)))
392
393
393 # Synthesize incremental revisions to the repository, adding repo depth.
394 # Synthesize incremental revisions to the repository, adding repo depth.
394 count = int(opts['count'])
395 count = int(opts['count'])
395 heads = set(map(repo.changelog.rev, repo.heads()))
396 heads = set(map(repo.changelog.rev, repo.heads()))
396 for i in xrange(count):
397 for i in xrange(count):
397 progress(_synthesizing, i, unit=_changesets, total=count)
398 progress(_synthesizing, i, unit=_changesets, total=count)
398
399
399 node = repo.changelog.node
400 node = repo.changelog.node
400 revs = len(repo)
401 revs = len(repo)
401
402
402 def pickhead(heads, distance):
403 def pickhead(heads, distance):
403 if heads:
404 if heads:
404 lheads = sorted(heads)
405 lheads = sorted(heads)
405 rev = revs - min(pick(distance), revs)
406 rev = revs - min(pick(distance), revs)
406 if rev < lheads[-1]:
407 if rev < lheads[-1]:
407 rev = lheads[bisect.bisect_left(lheads, rev)]
408 rev = lheads[bisect.bisect_left(lheads, rev)]
408 else:
409 else:
409 rev = lheads[-1]
410 rev = lheads[-1]
410 return rev, node(rev)
411 return rev, node(rev)
411 return nullrev, nullid
412 return nullrev, nullid
412
413
413 r1 = revs - min(pick(p1distance), revs)
414 r1 = revs - min(pick(p1distance), revs)
414 p1 = node(r1)
415 p1 = node(r1)
415
416
416 # 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
417 # model, so artificially constrain their proliferation
418 # model, so artificially constrain their proliferation
418 toomanyheads = len(heads) > random.randint(1, 20)
419 toomanyheads = len(heads) > random.randint(1, 20)
419 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
420 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
420 r2, p2 = pickhead(heads.difference([r1]), p2distance)
421 r2, p2 = pickhead(heads.difference([r1]), p2distance)
421 else:
422 else:
422 r2, p2 = nullrev, nullid
423 r2, p2 = nullrev, nullid
423
424
424 pl = [p1, p2]
425 pl = [p1, p2]
425 pctx = repo[r1]
426 pctx = repo[r1]
426 mf = pctx.manifest()
427 mf = pctx.manifest()
427 mfk = mf.keys()
428 mfk = mf.keys()
428 changes = {}
429 changes = {}
429 if mfk:
430 if mfk:
430 for __ in xrange(pick(fileschanged)):
431 for __ in xrange(pick(fileschanged)):
431 for __ in xrange(10):
432 for __ in xrange(10):
432 fctx = pctx.filectx(random.choice(mfk))
433 fctx = pctx.filectx(random.choice(mfk))
433 path = fctx.path()
434 path = fctx.path()
434 if not (path in nevertouch or fctx.isbinary() or
435 if not (path in nevertouch or fctx.isbinary() or
435 'l' in fctx.flags()):
436 'l' in fctx.flags()):
436 break
437 break
437 lines = fctx.data().splitlines()
438 lines = fctx.data().splitlines()
438 add, remove = pick(lineschanged)
439 add, remove = pick(lineschanged)
439 for __ in xrange(remove):
440 for __ in xrange(remove):
440 if not lines:
441 if not lines:
441 break
442 break
442 del lines[random.randrange(0, len(lines))]
443 del lines[random.randrange(0, len(lines))]
443 for __ in xrange(add):
444 for __ in xrange(add):
444 lines.insert(random.randint(0, len(lines)), makeline())
445 lines.insert(random.randint(0, len(lines)), makeline())
445 path = fctx.path()
446 path = fctx.path()
446 changes[path] = context.memfilectx(repo, path,
447 changes[path] = context.memfilectx(repo, path,
447 '\n'.join(lines) + '\n')
448 '\n'.join(lines) + '\n')
448 for __ in xrange(pick(filesremoved)):
449 for __ in xrange(pick(filesremoved)):
449 path = random.choice(mfk)
450 path = random.choice(mfk)
450 for __ in xrange(10):
451 for __ in xrange(10):
451 path = random.choice(mfk)
452 path = random.choice(mfk)
452 if path not in changes:
453 if path not in changes:
453 changes[path] = None
454 changes[path] = None
454 break
455 break
455 if filesadded:
456 if filesadded:
456 dirs = list(pctx.dirs())
457 dirs = list(pctx.dirs())
457 dirs.insert(0, '')
458 dirs.insert(0, '')
458 for __ in xrange(pick(filesadded)):
459 for __ in xrange(pick(filesadded)):
459 pathstr = ''
460 pathstr = ''
460 while pathstr in dirs:
461 while pathstr in dirs:
461 path = [random.choice(dirs)]
462 path = [random.choice(dirs)]
462 if pick(dirsadded):
463 if pick(dirsadded):
463 path.append(random.choice(words))
464 path.append(random.choice(words))
464 path.append(random.choice(words))
465 path.append(random.choice(words))
465 pathstr = '/'.join(filter(None, path))
466 pathstr = '/'.join(filter(None, path))
466 data = '\n'.join(makeline()
467 data = '\n'.join(makeline()
467 for __ in xrange(pick(linesinfilesadded))) + '\n'
468 for __ in xrange(pick(linesinfilesadded))) + '\n'
468 changes[pathstr] = context.memfilectx(repo, pathstr, data)
469 changes[pathstr] = context.memfilectx(repo, pathstr, data)
469 def filectxfn(repo, memctx, path):
470 def filectxfn(repo, memctx, path):
470 return changes[path]
471 return changes[path]
471 if not changes:
472 if not changes:
472 continue
473 continue
473 if revs:
474 if revs:
474 date = repo['tip'].date()[0] + pick(interarrival)
475 date = repo['tip'].date()[0] + pick(interarrival)
475 else:
476 else:
476 date = time.time() - (86400 * count)
477 date = time.time() - (86400 * count)
477 # 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.
478 date = min(0x7fffffff, max(0, date))
479 date = min(0x7fffffff, max(0, date))
479 user = random.choice(words) + '@' + random.choice(words)
480 user = random.choice(words) + '@' + random.choice(words)
480 mc = context.memctx(repo, pl, makeline(minimum=2),
481 mc = context.memctx(repo, pl, makeline(minimum=2),
481 sorted(changes.iterkeys()),
482 sorted(changes.iterkeys()),
482 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
483 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
483 newnode = mc.commit()
484 newnode = mc.commit()
484 heads.add(repo.changelog.rev(newnode))
485 heads.add(repo.changelog.rev(newnode))
485 heads.discard(r1)
486 heads.discard(r1)
486 heads.discard(r2)
487 heads.discard(r2)
487
488
488 lock.release()
489 lock.release()
489 wlock.release()
490 wlock.release()
490
491
491 def renamedirs(dirs, words):
492 def renamedirs(dirs, words):
492 '''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.'''
493 wordgen = itertools.cycle(words)
494 wordgen = itertools.cycle(words)
494 replacements = {'': ''}
495 replacements = {'': ''}
495 def rename(dirpath):
496 def rename(dirpath):
496 '''Recursively rename the directory and all path prefixes.
497 '''Recursively rename the directory and all path prefixes.
497
498
498 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
499 as in dynamic programming, ensuring linear runtime and consistent
500 as in dynamic programming, ensuring linear runtime and consistent
500 renaming regardless of iteration order through the model.
501 renaming regardless of iteration order through the model.
501 '''
502 '''
502 if dirpath in replacements:
503 if dirpath in replacements:
503 return replacements[dirpath]
504 return replacements[dirpath]
504 head, _ = os.path.split(dirpath)
505 head, _ = os.path.split(dirpath)
505 if head:
506 if head:
506 head = rename(head)
507 head = rename(head)
507 else:
508 else:
508 head = ''
509 head = ''
509 renamed = os.path.join(head, wordgen.next())
510 renamed = os.path.join(head, next(wordgen))
510 replacements[dirpath] = renamed
511 replacements[dirpath] = renamed
511 return renamed
512 return renamed
512 result = []
513 result = []
513 for dirpath, count in dirs.iteritems():
514 for dirpath, count in dirs.iteritems():
514 result.append([rename(dirpath.lstrip(os.sep)), count])
515 result.append([rename(dirpath.lstrip(os.sep)), count])
515 return result
516 return result
@@ -1,38 +1,46
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # Undump a dump from dumprevlog
2 # Undump a dump from dumprevlog
3 # $ hg init
3 # $ hg init
4 # $ undumprevlog < repo.dump
4 # $ undumprevlog < repo.dump
5
5
6 from __future__ import absolute_import
7
6 import sys
8 import sys
7 from mercurial import revlog, node, scmutil, util, transaction
9 from mercurial import (
10 node,
11 revlog,
12 scmutil,
13 transaction,
14 util,
15 )
8
16
9 for fp in (sys.stdin, sys.stdout, sys.stderr):
17 for fp in (sys.stdin, sys.stdout, sys.stderr):
10 util.setbinary(fp)
18 util.setbinary(fp)
11
19
12 opener = scmutil.opener('.', False)
20 opener = scmutil.opener('.', False)
13 tr = transaction.transaction(sys.stderr.write, opener, {'store': opener},
21 tr = transaction.transaction(sys.stderr.write, opener, {'store': opener},
14 "undump.journal")
22 "undump.journal")
15 while True:
23 while True:
16 l = sys.stdin.readline()
24 l = sys.stdin.readline()
17 if not l:
25 if not l:
18 break
26 break
19 if l.startswith("file:"):
27 if l.startswith("file:"):
20 f = l[6:-1]
28 f = l[6:-1]
21 r = revlog.revlog(opener, f)
29 r = revlog.revlog(opener, f)
22 print f
30 print f
23 elif l.startswith("node:"):
31 elif l.startswith("node:"):
24 n = node.bin(l[6:-1])
32 n = node.bin(l[6:-1])
25 elif l.startswith("linkrev:"):
33 elif l.startswith("linkrev:"):
26 lr = int(l[9:-1])
34 lr = int(l[9:-1])
27 elif l.startswith("parents:"):
35 elif l.startswith("parents:"):
28 p = l[9:-1].split()
36 p = l[9:-1].split()
29 p1 = node.bin(p[0])
37 p1 = node.bin(p[0])
30 p2 = node.bin(p[1])
38 p2 = node.bin(p[1])
31 elif l.startswith("length:"):
39 elif l.startswith("length:"):
32 length = int(l[8:-1])
40 length = int(l[8:-1])
33 sys.stdin.readline() # start marker
41 sys.stdin.readline() # start marker
34 d = sys.stdin.read(length)
42 d = sys.stdin.read(length)
35 sys.stdin.readline() # end marker
43 sys.stdin.readline() # end marker
36 r.addrevision(d, tr, lr, p1, p2)
44 r.addrevision(d, tr, lr, p1, p2)
37
45
38 tr.close()
46 tr.close()
@@ -1,134 +1,135
1 # An example WSGI script for IIS/isapi-wsgi to export multiple hgweb repos
1 # An example WSGI script for IIS/isapi-wsgi to export multiple hgweb repos
2 # Copyright 2010-2016 Sune Foldager <cyano@me.com>
2 # Copyright 2010-2016 Sune Foldager <cyano@me.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6 #
6 #
7 # Requirements:
7 # Requirements:
8 # - Python 2.7, preferably 64 bit
8 # - Python 2.7, preferably 64 bit
9 # - PyWin32 for Python 2.7 (32 or 64 bit)
9 # - PyWin32 for Python 2.7 (32 or 64 bit)
10 # - Mercurial installed from source (python setup.py install) or download the
10 # - Mercurial installed from source (python setup.py install) or download the
11 # python module installer from https://www.mercurial-scm.org/wiki/Download
11 # python module installer from https://www.mercurial-scm.org/wiki/Download
12 # - IIS 7 or newer
12 # - IIS 7 or newer
13 #
13 #
14 #
14 #
15 # Installation and use:
15 # Installation and use:
16 #
16 #
17 # - Download or clone the isapi-wsgi source and run python setup.py install.
17 # - Download or clone the isapi-wsgi source and run python setup.py install.
18 # https://github.com/hexdump42/isapi-wsgi
18 # https://github.com/hexdump42/isapi-wsgi
19 #
19 #
20 # - Create a directory to hold the shim dll, config files etc. This can reside
20 # - Create a directory to hold the shim dll, config files etc. This can reside
21 # inside the standard IIS directory, C:\inetpub, or anywhere else. Copy this
21 # inside the standard IIS directory, C:\inetpub, or anywhere else. Copy this
22 # script there.
22 # script there.
23 #
23 #
24 # - Run this script (i.e. python hgwebdir_wsgi.py) to get a shim dll. The
24 # - Run this script (i.e. python hgwebdir_wsgi.py) to get a shim dll. The
25 # shim is identical for all scripts, so you can just copy and rename one
25 # shim is identical for all scripts, so you can just copy and rename one
26 # from an earlier run, if you wish. The shim needs to reside in the same
26 # from an earlier run, if you wish. The shim needs to reside in the same
27 # directory as this script.
27 # directory as this script.
28 #
28 #
29 # - Start IIS manager and create a new app pool:
29 # - Start IIS manager and create a new app pool:
30 # .NET CLR Version: No Managed Code
30 # .NET CLR Version: No Managed Code
31 # Advanced Settings: Enable 32 Bit Applications, if using 32 bit Python.
31 # Advanced Settings: Enable 32 Bit Applications, if using 32 bit Python.
32 # You can adjust the identity and maximum worker processes if you wish. This
32 # You can adjust the identity and maximum worker processes if you wish. This
33 # setup works fine with multiple worker processes.
33 # setup works fine with multiple worker processes.
34 #
34 #
35 # - Create an IIS application where your hgwebdir is to be served from.
35 # - Create an IIS application where your hgwebdir is to be served from.
36 # Assign it the app pool you just created and point its physical path to the
36 # Assign it the app pool you just created and point its physical path to the
37 # directory you created.
37 # directory you created.
38 #
38 #
39 # - In the application, remove all handler mappings and setup a wildcard script
39 # - In the application, remove all handler mappings and setup a wildcard script
40 # handler mapping of type IsapiModule with the shim dll as its executable.
40 # handler mapping of type IsapiModule with the shim dll as its executable.
41 # This file MUST reside in the same directory as the shim. The easiest way
41 # This file MUST reside in the same directory as the shim. The easiest way
42 # to do all this is to close IIS manager, place a web.config file in your
42 # to do all this is to close IIS manager, place a web.config file in your
43 # directory and start IIS manager again. The file should contain:
43 # directory and start IIS manager again. The file should contain:
44 #
44 #
45 # <?xml version="1.0" encoding="UTF-8"?>
45 # <?xml version="1.0" encoding="UTF-8"?>
46 # <configuration>
46 # <configuration>
47 # <system.webServer>
47 # <system.webServer>
48 # <handlers accessPolicy="Read, Script">
48 # <handlers accessPolicy="Read, Script">
49 # <clear />
49 # <clear />
50 # <add name="hgwebdir" path="*" verb="*" modules="IsapiModule"
50 # <add name="hgwebdir" path="*" verb="*" modules="IsapiModule"
51 # scriptProcessor="C:\your\directory\_hgwebdir_wsgi.dll"
51 # scriptProcessor="C:\your\directory\_hgwebdir_wsgi.dll"
52 # resourceType="Unspecified" requireAccess="None"
52 # resourceType="Unspecified" requireAccess="None"
53 # preCondition="bitness64" />
53 # preCondition="bitness64" />
54 # </handlers>
54 # </handlers>
55 # </system.webServer>
55 # </system.webServer>
56 # </configuration>
56 # </configuration>
57 #
57 #
58 # Where "bitness64" should be replaced with "bitness32" for 32 bit Python.
58 # Where "bitness64" should be replaced with "bitness32" for 32 bit Python.
59 #
59 #
60 # - Edit ISAPI And CGI Restrictions on the web server (global setting). Add a
60 # - Edit ISAPI And CGI Restrictions on the web server (global setting). Add a
61 # restriction pointing to your shim dll and allow it to run.
61 # restriction pointing to your shim dll and allow it to run.
62 #
62 #
63 # - Create a configuration file in your directory and adjust the configuration
63 # - Create a configuration file in your directory and adjust the configuration
64 # variables below to match your needs. Example configuration:
64 # variables below to match your needs. Example configuration:
65 #
65 #
66 # [web]
66 # [web]
67 # style = gitweb
67 # style = gitweb
68 # push_ssl = false
68 # push_ssl = false
69 # allow_push = *
69 # allow_push = *
70 # encoding = utf8
70 # encoding = utf8
71 #
71 #
72 # [server]
72 # [server]
73 # validate = true
73 # validate = true
74 #
74 #
75 # [paths]
75 # [paths]
76 # repo1 = c:\your\directory\repo1
76 # repo1 = c:\your\directory\repo1
77 # repo2 = c:\your\directory\repo2
77 # repo2 = c:\your\directory\repo2
78 #
78 #
79 # - Restart the web server and see if things are running.
79 # - Restart the web server and see if things are running.
80 #
80 #
81
81
82 from __future__ import absolute_import
83
82 # Configuration file location
84 # Configuration file location
83 hgweb_config = r'c:\your\directory\wsgi.config'
85 hgweb_config = r'c:\your\directory\wsgi.config'
84
86
85 # Global settings for IIS path translation
87 # Global settings for IIS path translation
86 path_strip = 0 # Strip this many path elements off (when using url rewrite)
88 path_strip = 0 # Strip this many path elements off (when using url rewrite)
87 path_prefix = 1 # This many path elements are prefixes (depends on the
89 path_prefix = 1 # This many path elements are prefixes (depends on the
88 # virtual path of the IIS application).
90 # virtual path of the IIS application).
89
91
90 from __future__ import absolute_import
91 import sys
92 import sys
92
93
93 # Adjust python path if this is not a system-wide install
94 # Adjust python path if this is not a system-wide install
94 #sys.path.insert(0, r'C:\your\custom\hg\build\lib.win32-2.7')
95 #sys.path.insert(0, r'C:\your\custom\hg\build\lib.win32-2.7')
95
96
96 # Enable tracing. Run 'python -m win32traceutil' to debug
97 # Enable tracing. Run 'python -m win32traceutil' to debug
97 if getattr(sys, 'isapidllhandle', None) is not None:
98 if getattr(sys, 'isapidllhandle', None) is not None:
98 import win32traceutil
99 import win32traceutil
99 win32traceutil.SetupForPrint # silence unused import warning
100 win32traceutil.SetupForPrint # silence unused import warning
100
101
101 import isapi_wsgi
102 import isapi_wsgi
102 from mercurial.hgweb.hgwebdir_mod import hgwebdir
103 from mercurial.hgweb.hgwebdir_mod import hgwebdir
103
104
104 # Example tweak: Replace isapi_wsgi's handler to provide better error message
105 # Example tweak: Replace isapi_wsgi's handler to provide better error message
105 # Other stuff could also be done here, like logging errors etc.
106 # Other stuff could also be done here, like logging errors etc.
106 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
107 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
107 error_status = '500 Internal Server Error' # less silly error message
108 error_status = '500 Internal Server Error' # less silly error message
108
109
109 isapi_wsgi.IsapiWsgiHandler = WsgiHandler
110 isapi_wsgi.IsapiWsgiHandler = WsgiHandler
110
111
111 # Only create the hgwebdir instance once
112 # Only create the hgwebdir instance once
112 application = hgwebdir(hgweb_config)
113 application = hgwebdir(hgweb_config)
113
114
114 def handler(environ, start_response):
115 def handler(environ, start_response):
115
116
116 # Translate IIS's weird URLs
117 # Translate IIS's weird URLs
117 url = environ['SCRIPT_NAME'] + environ['PATH_INFO']
118 url = environ['SCRIPT_NAME'] + environ['PATH_INFO']
118 paths = url[1:].split('/')[path_strip:]
119 paths = url[1:].split('/')[path_strip:]
119 script_name = '/' + '/'.join(paths[:path_prefix])
120 script_name = '/' + '/'.join(paths[:path_prefix])
120 path_info = '/'.join(paths[path_prefix:])
121 path_info = '/'.join(paths[path_prefix:])
121 if path_info:
122 if path_info:
122 path_info = '/' + path_info
123 path_info = '/' + path_info
123 environ['SCRIPT_NAME'] = script_name
124 environ['SCRIPT_NAME'] = script_name
124 environ['PATH_INFO'] = path_info
125 environ['PATH_INFO'] = path_info
125
126
126 return application(environ, start_response)
127 return application(environ, start_response)
127
128
128 def __ExtensionFactory__():
129 def __ExtensionFactory__():
129 return isapi_wsgi.ISAPISimpleHandler(handler)
130 return isapi_wsgi.ISAPISimpleHandler(handler)
130
131
131 if __name__=='__main__':
132 if __name__=='__main__':
132 from isapi.install import ISAPIParameters, HandleCommandLine
133 from isapi.install import ISAPIParameters, HandleCommandLine
133 params = ISAPIParameters()
134 params = ISAPIParameters()
134 HandleCommandLine(params)
135 HandleCommandLine(params)
@@ -1,98 +1,97
1 ; System-wide Mercurial config file.
1 ; System-wide Mercurial config file.
2 ;
2 ;
3 ; !!! Do Not Edit This File !!!
3 ; !!! Do Not Edit This File !!!
4 ;
4 ;
5 ; This file will be replaced by the installer on every upgrade.
5 ; This file will be replaced by the installer on every upgrade.
6 ; Editing this file can cause strange side effects on Vista.
6 ; Editing this file can cause strange side effects on Vista.
7 ;
7 ;
8 ; http://bitbucket.org/tortoisehg/stable/issue/135
8 ; http://bitbucket.org/tortoisehg/stable/issue/135
9 ;
9 ;
10 ; To change settings you see in this file, override (or enable) them in
10 ; To change settings you see in this file, override (or enable) them in
11 ; your user Mercurial.ini file, where USERNAME is your Windows user name:
11 ; your user Mercurial.ini file, where USERNAME is your Windows user name:
12 ;
12 ;
13 ; XP or older - C:\Documents and Settings\USERNAME\Mercurial.ini
13 ; XP or older - C:\Documents and Settings\USERNAME\Mercurial.ini
14 ; Vista or later - C:\Users\USERNAME\Mercurial.ini
14 ; Vista or later - C:\Users\USERNAME\Mercurial.ini
15
15
16
16
17 [ui]
17 [ui]
18 ; editor used to enter commit logs, etc. Most text editors will work.
18 ; editor used to enter commit logs, etc. Most text editors will work.
19 editor = notepad
19 editor = notepad
20 ; show changed files and be a bit more verbose if True
20 ; show changed files and be a bit more verbose if True
21 ; verbose = True
21 ; verbose = True
22
22
23 ; username data to appear in commits
23 ; username data to appear in commits
24 ; it usually takes the form: Joe User <joe.user@host.com>
24 ; it usually takes the form: Joe User <joe.user@host.com>
25 ; username = Joe User <j.user@example.com>
25 ; username = Joe User <j.user@example.com>
26
26
27 ; In order to push/pull over ssh you must specify an ssh tool
27 ; In order to push/pull over ssh you must specify an ssh tool
28 ;ssh = "C:\Progra~1\TortoiseSVN\bin\TortoisePlink.exe" -ssh -2
28 ;ssh = "C:\Progra~1\TortoiseSVN\bin\TortoisePlink.exe" -ssh -2
29 ;ssh = C:\cygwin\bin\ssh
29 ;ssh = C:\cygwin\bin\ssh
30
30
31 ;
31 ;
32 ; For more information about mercurial extensions, start here
32 ; For more information about mercurial extensions, start here
33 ; http://www.selenic.com/mercurial/wiki/index.cgi/UsingExtensions
33 ; http://www.selenic.com/mercurial/wiki/index.cgi/UsingExtensions
34 ;
34 ;
35 ; Extensions shipped with Mercurial
35 ; Extensions shipped with Mercurial
36 ;
36 ;
37 [extensions]
37 [extensions]
38 ;acl =
38 ;acl =
39 ;blackbox =
39 ;blackbox =
40 ;bugzilla =
40 ;bugzilla =
41 ;children =
41 ;children =
42 ;churn =
42 ;churn =
43 ;color =
43 ;color =
44 ;convert =
44 ;convert =
45 ;eol =
45 ;eol =
46 ;extdiff =
46 ;extdiff =
47 ;fetch =
47 ;fetch =
48 ;gpg =
48 ;gpg =
49 ;hgcia =
50 ;hgk =
49 ;hgk =
51 ;highlight =
50 ;highlight =
52 ;histedit =
51 ;histedit =
53 ;interhg =
52 ;interhg =
54 ;largefiles =
53 ;largefiles =
55 ;keyword =
54 ;keyword =
56 ;mq =
55 ;mq =
57 ;notify =
56 ;notify =
58 ;pager =
57 ;pager =
59 ;patchbomb =
58 ;patchbomb =
60 ;progress =
59 ;progress =
61 ;purge =
60 ;purge =
62 ;rebase =
61 ;rebase =
63 ;record =
62 ;record =
64 ;relink =
63 ;relink =
65 ;schemes =
64 ;schemes =
66 ;share =
65 ;share =
67 ;shelve =
66 ;shelve =
68 ;transplant =
67 ;transplant =
69 ;win32mbcs =
68 ;win32mbcs =
70 ;zeroconf =
69 ;zeroconf =
71
70
72 ;
71 ;
73 ; Define external diff commands
72 ; Define external diff commands
74 ;
73 ;
75 [extdiff]
74 [extdiff]
76 ;cmd.bc3diff = C:\Program Files\Beyond Compare 3\BCompare.exe
75 ;cmd.bc3diff = C:\Program Files\Beyond Compare 3\BCompare.exe
77 ;cmd.vdiff = C:\Progra~1\TortoiseSVN\bin\TortoiseMerge.exe
76 ;cmd.vdiff = C:\Progra~1\TortoiseSVN\bin\TortoiseMerge.exe
78 ;cmd.vimdiff = gvim.exe
77 ;cmd.vimdiff = gvim.exe
79 ;opts.vimdiff = -f "+next" "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
78 ;opts.vimdiff = -f "+next" "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
80
79
81
80
82 [hgk]
81 [hgk]
83 ; Replace the following with your path to hgk, uncomment it and
82 ; Replace the following with your path to hgk, uncomment it and
84 ; install ActiveTcl (or another win32 port like tclkit)
83 ; install ActiveTcl (or another win32 port like tclkit)
85 ; path="C:\Program Files\Mercurial\Contrib\hgk.tcl"
84 ; path="C:\Program Files\Mercurial\Contrib\hgk.tcl"
86 ; vdiff=vdiff
85 ; vdiff=vdiff
87
86
88
87
89 ;
88 ;
90 ; The git extended diff format can represent binary files, file
89 ; The git extended diff format can represent binary files, file
91 ; permission changes, and rename information that the normal patch format
90 ; permission changes, and rename information that the normal patch format
92 ; cannot describe. However it is also not compatible with tools which
91 ; cannot describe. However it is also not compatible with tools which
93 ; expect normal patches. so enable git patches at your own risk.
92 ; expect normal patches. so enable git patches at your own risk.
94 ;
93 ;
95 [diff]
94 [diff]
96 ;git = false
95 ;git = false
97 ;nodates = false
96 ;nodates = false
98
97
@@ -1,63 +1,66
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # docchecker - look for problematic markup
3 # docchecker - look for problematic markup
4 #
4 #
5 # Copyright 2016 timeless <timeless@mozdev.org> and others
5 # Copyright 2016 timeless <timeless@mozdev.org> and others
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
10 from __future__ import absolute_import, print_function
11
12 import re
9 import sys
13 import sys
10 import re
11
14
12 leadingline = re.compile(r'(^\s*)(\S.*)$')
15 leadingline = re.compile(r'(^\s*)(\S.*)$')
13
16
14 checks = [
17 checks = [
15 (r""":hg:`[^`]*'[^`]*`""",
18 (r""":hg:`[^`]*'[^`]*`""",
16 """warning: please avoid nesting ' in :hg:`...`"""),
19 """warning: please avoid nesting ' in :hg:`...`"""),
17 (r'\w:hg:`',
20 (r'\w:hg:`',
18 'warning: please have a space before :hg:'),
21 'warning: please have a space before :hg:'),
19 (r"""(?:[^a-z][^'.])hg ([^,;"`]*'(?!hg)){2}""",
22 (r"""(?:[^a-z][^'.])hg ([^,;"`]*'(?!hg)){2}""",
20 '''warning: please use " instead of ' for hg ... "..."'''),
23 '''warning: please use " instead of ' for hg ... "..."'''),
21 ]
24 ]
22
25
23 def check(line):
26 def check(line):
24 messages = []
27 messages = []
25 for match, msg in checks:
28 for match, msg in checks:
26 if re.search(match, line):
29 if re.search(match, line):
27 messages.append(msg)
30 messages.append(msg)
28 if messages:
31 if messages:
29 print(line)
32 print(line)
30 for msg in messages:
33 for msg in messages:
31 print(msg)
34 print(msg)
32
35
33 def work(file):
36 def work(file):
34 (llead, lline) = ('', '')
37 (llead, lline) = ('', '')
35
38
36 for line in file:
39 for line in file:
37 # this section unwraps lines
40 # this section unwraps lines
38 match = leadingline.match(line)
41 match = leadingline.match(line)
39 if not match:
42 if not match:
40 check(lline)
43 check(lline)
41 (llead, lline) = ('', '')
44 (llead, lline) = ('', '')
42 continue
45 continue
43
46
44 lead, line = match.group(1), match.group(2)
47 lead, line = match.group(1), match.group(2)
45 if (lead == llead):
48 if (lead == llead):
46 if (lline != ''):
49 if (lline != ''):
47 lline += ' ' + line
50 lline += ' ' + line
48 else:
51 else:
49 lline = line
52 lline = line
50 else:
53 else:
51 check(lline)
54 check(lline)
52 (llead, lline) = (lead, line)
55 (llead, lline) = (lead, line)
53 check(lline)
56 check(lline)
54
57
55 def main():
58 def main():
56 for f in sys.argv[1:]:
59 for f in sys.argv[1:]:
57 try:
60 try:
58 with open(f) as file:
61 with open(f) as file:
59 work(file)
62 work(file)
60 except BaseException as e:
63 except BaseException as e:
61 print("failed to process %s: %s" % (f, e))
64 print("failed to process %s: %s" % (f, e))
62
65
63 main()
66 main()
@@ -1,224 +1,224
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """usage: %s DOC ...
2 """usage: %s DOC ...
3
3
4 where DOC is the name of a document
4 where DOC is the name of a document
5 """
5 """
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import sys
10 import sys
11 import textwrap
11 import textwrap
12
12
13 # This script is executed during installs and may not have C extensions
13 # This script is executed during installs and may not have C extensions
14 # available. Relax C module requirements.
14 # available. Relax C module requirements.
15 os.environ['HGMODULEPOLICY'] = 'allow'
15 os.environ['HGMODULEPOLICY'] = 'allow'
16 # import from the live mercurial repo
16 # import from the live mercurial repo
17 sys.path.insert(0, "..")
17 sys.path.insert(0, "..")
18 from mercurial import demandimport; demandimport.enable()
18 from mercurial import demandimport; demandimport.enable()
19 from mercurial import (
19 from mercurial import (
20 commands,
20 commands,
21 extensions,
21 extensions,
22 help,
22 help,
23 minirst,
23 minirst,
24 ui as uimod,
24 ui as uimod,
25 )
25 )
26 from mercurial.i18n import (
26 from mercurial.i18n import (
27 gettext,
27 gettext,
28 _,
28 _,
29 )
29 )
30
30
31 table = commands.table
31 table = commands.table
32 globalopts = commands.globalopts
32 globalopts = commands.globalopts
33 helptable = help.helptable
33 helptable = help.helptable
34 loaddoc = help.loaddoc
34 loaddoc = help.loaddoc
35
35
36 def get_desc(docstr):
36 def get_desc(docstr):
37 if not docstr:
37 if not docstr:
38 return "", ""
38 return "", ""
39 # sanitize
39 # sanitize
40 docstr = docstr.strip("\n")
40 docstr = docstr.strip("\n")
41 docstr = docstr.rstrip()
41 docstr = docstr.rstrip()
42 shortdesc = docstr.splitlines()[0].strip()
42 shortdesc = docstr.splitlines()[0].strip()
43
43
44 i = docstr.find("\n")
44 i = docstr.find("\n")
45 if i != -1:
45 if i != -1:
46 desc = docstr[i + 2:]
46 desc = docstr[i + 2:]
47 else:
47 else:
48 desc = shortdesc
48 desc = shortdesc
49
49
50 desc = textwrap.dedent(desc)
50 desc = textwrap.dedent(desc)
51
51
52 return (shortdesc, desc)
52 return (shortdesc, desc)
53
53
54 def get_opts(opts):
54 def get_opts(opts):
55 for opt in opts:
55 for opt in opts:
56 if len(opt) == 5:
56 if len(opt) == 5:
57 shortopt, longopt, default, desc, optlabel = opt
57 shortopt, longopt, default, desc, optlabel = opt
58 else:
58 else:
59 shortopt, longopt, default, desc = opt
59 shortopt, longopt, default, desc = opt
60 optlabel = _("VALUE")
60 optlabel = _("VALUE")
61 allopts = []
61 allopts = []
62 if shortopt:
62 if shortopt:
63 allopts.append("-%s" % shortopt)
63 allopts.append("-%s" % shortopt)
64 if longopt:
64 if longopt:
65 allopts.append("--%s" % longopt)
65 allopts.append("--%s" % longopt)
66 if isinstance(default, list):
66 if isinstance(default, list):
67 allopts[-1] += " <%s[+]>" % optlabel
67 allopts[-1] += " <%s[+]>" % optlabel
68 elif (default is not None) and not isinstance(default, bool):
68 elif (default is not None) and not isinstance(default, bool):
69 allopts[-1] += " <%s>" % optlabel
69 allopts[-1] += " <%s>" % optlabel
70 if '\n' in desc:
70 if '\n' in desc:
71 # only remove line breaks and indentation
71 # only remove line breaks and indentation
72 desc = ' '.join(l.lstrip() for l in desc.split('\n'))
72 desc = ' '.join(l.lstrip() for l in desc.split('\n'))
73 desc += default and _(" (default: %s)") % default or ""
73 desc += default and _(" (default: %s)") % default or ""
74 yield (", ".join(allopts), desc)
74 yield (", ".join(allopts), desc)
75
75
76 def get_cmd(cmd, cmdtable):
76 def get_cmd(cmd, cmdtable):
77 d = {}
77 d = {}
78 attr = cmdtable[cmd]
78 attr = cmdtable[cmd]
79 cmds = cmd.lstrip("^").split("|")
79 cmds = cmd.lstrip("^").split("|")
80
80
81 d['cmd'] = cmds[0]
81 d['cmd'] = cmds[0]
82 d['aliases'] = cmd.split("|")[1:]
82 d['aliases'] = cmd.split("|")[1:]
83 d['desc'] = get_desc(gettext(attr[0].__doc__))
83 d['desc'] = get_desc(gettext(attr[0].__doc__))
84 d['opts'] = list(get_opts(attr[1]))
84 d['opts'] = list(get_opts(attr[1]))
85
85
86 s = 'hg ' + cmds[0]
86 s = 'hg ' + cmds[0]
87 if len(attr) > 2:
87 if len(attr) > 2:
88 if not attr[2].startswith('hg'):
88 if not attr[2].startswith('hg'):
89 s += ' ' + attr[2]
89 s += ' ' + attr[2]
90 else:
90 else:
91 s = attr[2]
91 s = attr[2]
92 d['synopsis'] = s.strip()
92 d['synopsis'] = s.strip()
93
93
94 return d
94 return d
95
95
96 def showdoc(ui):
96 def showdoc(ui):
97 # print options
97 # print options
98 ui.write(minirst.section(_("Options")))
98 ui.write(minirst.section(_("Options")))
99 multioccur = False
99 multioccur = False
100 for optstr, desc in get_opts(globalopts):
100 for optstr, desc in get_opts(globalopts):
101 ui.write("%s\n %s\n\n" % (optstr, desc))
101 ui.write("%s\n %s\n\n" % (optstr, desc))
102 if optstr.endswith("[+]>"):
102 if optstr.endswith("[+]>"):
103 multioccur = True
103 multioccur = True
104 if multioccur:
104 if multioccur:
105 ui.write(_("\n[+] marked option can be specified multiple times\n"))
105 ui.write(_("\n[+] marked option can be specified multiple times\n"))
106 ui.write("\n")
106 ui.write("\n")
107
107
108 # print cmds
108 # print cmds
109 ui.write(minirst.section(_("Commands")))
109 ui.write(minirst.section(_("Commands")))
110 commandprinter(ui, table, minirst.subsection)
110 commandprinter(ui, table, minirst.subsection)
111
111
112 # print help topics
112 # print help topics
113 # The config help topic is included in the hgrc.5 man page.
113 # The config help topic is included in the hgrc.5 man page.
114 helpprinter(ui, helptable, minirst.section, exclude=['config'])
114 helpprinter(ui, helptable, minirst.section, exclude=['config'])
115
115
116 ui.write(minirst.section(_("Extensions")))
116 ui.write(minirst.section(_("Extensions")))
117 ui.write(_("This section contains help for extensions that are "
117 ui.write(_("This section contains help for extensions that are "
118 "distributed together with Mercurial. Help for other "
118 "distributed together with Mercurial. Help for other "
119 "extensions is available in the help system."))
119 "extensions is available in the help system."))
120 ui.write("\n\n"
120 ui.write(("\n\n"
121 ".. contents::\n"
121 ".. contents::\n"
122 " :class: htmlonly\n"
122 " :class: htmlonly\n"
123 " :local:\n"
123 " :local:\n"
124 " :depth: 1\n\n")
124 " :depth: 1\n\n"))
125
125
126 for extensionname in sorted(allextensionnames()):
126 for extensionname in sorted(allextensionnames()):
127 mod = extensions.load(ui, extensionname, None)
127 mod = extensions.load(ui, extensionname, None)
128 ui.write(minirst.subsection(extensionname))
128 ui.write(minirst.subsection(extensionname))
129 ui.write("%s\n\n" % gettext(mod.__doc__))
129 ui.write("%s\n\n" % gettext(mod.__doc__))
130 cmdtable = getattr(mod, 'cmdtable', None)
130 cmdtable = getattr(mod, 'cmdtable', None)
131 if cmdtable:
131 if cmdtable:
132 ui.write(minirst.subsubsection(_('Commands')))
132 ui.write(minirst.subsubsection(_('Commands')))
133 commandprinter(ui, cmdtable, minirst.subsubsubsection)
133 commandprinter(ui, cmdtable, minirst.subsubsubsection)
134
134
135 def showtopic(ui, topic):
135 def showtopic(ui, topic):
136 extrahelptable = [
136 extrahelptable = [
137 (["common"], '', loaddoc('common')),
137 (["common"], '', loaddoc('common')),
138 (["hg.1"], '', loaddoc('hg.1')),
138 (["hg.1"], '', loaddoc('hg.1')),
139 (["hg-ssh.8"], '', loaddoc('hg-ssh.8')),
139 (["hg-ssh.8"], '', loaddoc('hg-ssh.8')),
140 (["hgignore.5"], '', loaddoc('hgignore.5')),
140 (["hgignore.5"], '', loaddoc('hgignore.5')),
141 (["hgrc.5"], '', loaddoc('hgrc.5')),
141 (["hgrc.5"], '', loaddoc('hgrc.5')),
142 (["hgignore.5.gendoc"], '', loaddoc('hgignore')),
142 (["hgignore.5.gendoc"], '', loaddoc('hgignore')),
143 (["hgrc.5.gendoc"], '', loaddoc('config')),
143 (["hgrc.5.gendoc"], '', loaddoc('config')),
144 ]
144 ]
145 helpprinter(ui, helptable + extrahelptable, None, include=[topic])
145 helpprinter(ui, helptable + extrahelptable, None, include=[topic])
146
146
147 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
147 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
148 for names, sec, doc in helptable:
148 for names, sec, doc in helptable:
149 if exclude and names[0] in exclude:
149 if exclude and names[0] in exclude:
150 continue
150 continue
151 if include and names[0] not in include:
151 if include and names[0] not in include:
152 continue
152 continue
153 for name in names:
153 for name in names:
154 ui.write(".. _%s:\n" % name)
154 ui.write(".. _%s:\n" % name)
155 ui.write("\n")
155 ui.write("\n")
156 if sectionfunc:
156 if sectionfunc:
157 ui.write(sectionfunc(sec))
157 ui.write(sectionfunc(sec))
158 if callable(doc):
158 if callable(doc):
159 doc = doc(ui)
159 doc = doc(ui)
160 ui.write(doc)
160 ui.write(doc)
161 ui.write("\n")
161 ui.write("\n")
162
162
163 def commandprinter(ui, cmdtable, sectionfunc):
163 def commandprinter(ui, cmdtable, sectionfunc):
164 h = {}
164 h = {}
165 for c, attr in cmdtable.items():
165 for c, attr in cmdtable.items():
166 f = c.split("|")[0]
166 f = c.split("|")[0]
167 f = f.lstrip("^")
167 f = f.lstrip("^")
168 h[f] = c
168 h[f] = c
169 cmds = h.keys()
169 cmds = h.keys()
170 cmds.sort()
170 cmds.sort()
171
171
172 for f in cmds:
172 for f in cmds:
173 if f.startswith("debug"):
173 if f.startswith("debug"):
174 continue
174 continue
175 d = get_cmd(h[f], cmdtable)
175 d = get_cmd(h[f], cmdtable)
176 ui.write(sectionfunc(d['cmd']))
176 ui.write(sectionfunc(d['cmd']))
177 # short description
177 # short description
178 ui.write(d['desc'][0])
178 ui.write(d['desc'][0])
179 # synopsis
179 # synopsis
180 ui.write("::\n\n")
180 ui.write("::\n\n")
181 synopsislines = d['synopsis'].splitlines()
181 synopsislines = d['synopsis'].splitlines()
182 for line in synopsislines:
182 for line in synopsislines:
183 # some commands (such as rebase) have a multi-line
183 # some commands (such as rebase) have a multi-line
184 # synopsis
184 # synopsis
185 ui.write(" %s\n" % line)
185 ui.write(" %s\n" % line)
186 ui.write('\n')
186 ui.write('\n')
187 # description
187 # description
188 ui.write("%s\n\n" % d['desc'][1])
188 ui.write("%s\n\n" % d['desc'][1])
189 # options
189 # options
190 opt_output = list(d['opts'])
190 opt_output = list(d['opts'])
191 if opt_output:
191 if opt_output:
192 opts_len = max([len(line[0]) for line in opt_output])
192 opts_len = max([len(line[0]) for line in opt_output])
193 ui.write(_("Options:\n\n"))
193 ui.write(_("Options:\n\n"))
194 multioccur = False
194 multioccur = False
195 for optstr, desc in opt_output:
195 for optstr, desc in opt_output:
196 if desc:
196 if desc:
197 s = "%-*s %s" % (opts_len, optstr, desc)
197 s = "%-*s %s" % (opts_len, optstr, desc)
198 else:
198 else:
199 s = optstr
199 s = optstr
200 ui.write("%s\n" % s)
200 ui.write("%s\n" % s)
201 if optstr.endswith("[+]>"):
201 if optstr.endswith("[+]>"):
202 multioccur = True
202 multioccur = True
203 if multioccur:
203 if multioccur:
204 ui.write(_("\n[+] marked option can be specified"
204 ui.write(_("\n[+] marked option can be specified"
205 " multiple times\n"))
205 " multiple times\n"))
206 ui.write("\n")
206 ui.write("\n")
207 # aliases
207 # aliases
208 if d['aliases']:
208 if d['aliases']:
209 ui.write(_(" aliases: %s\n\n") % " ".join(d['aliases']))
209 ui.write(_(" aliases: %s\n\n") % " ".join(d['aliases']))
210
210
211
211
212 def allextensionnames():
212 def allextensionnames():
213 return extensions.enabled().keys() + extensions.disabled().keys()
213 return extensions.enabled().keys() + extensions.disabled().keys()
214
214
215 if __name__ == "__main__":
215 if __name__ == "__main__":
216 doc = 'hg.1.gendoc'
216 doc = 'hg.1.gendoc'
217 if len(sys.argv) > 1:
217 if len(sys.argv) > 1:
218 doc = sys.argv[1]
218 doc = sys.argv[1]
219
219
220 ui = uimod.ui()
220 ui = uimod.ui()
221 if doc == 'hg.1.gendoc':
221 if doc == 'hg.1.gendoc':
222 showdoc(ui)
222 showdoc(ui)
223 else:
223 else:
224 showtopic(ui, sys.argv[1])
224 showtopic(ui, sys.argv[1])
@@ -1,1115 +1,1115
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
60 import inspect
61
61
62 FIELD_LIST_INDENT = 7
62 FIELD_LIST_INDENT = 7
63 DEFINITION_LIST_INDENT = 7
63 DEFINITION_LIST_INDENT = 7
64 OPTION_LIST_INDENT = 7
64 OPTION_LIST_INDENT = 7
65 BLOCKQOUTE_INDENT = 3.5
65 BLOCKQOUTE_INDENT = 3.5
66
66
67 # Define two macros so man/roff can calculate the
67 # Define two macros so man/roff can calculate the
68 # indent/unindent margins by itself
68 # indent/unindent margins by itself
69 MACRO_DEF = (r""".
69 MACRO_DEF = (r""".
70 .nr rst2man-indent-level 0
70 .nr rst2man-indent-level 0
71 .
71 .
72 .de1 rstReportMargin
72 .de1 rstReportMargin
73 \\$1 \\n[an-margin]
73 \\$1 \\n[an-margin]
74 level \\n[rst2man-indent-level]
74 level \\n[rst2man-indent-level]
75 level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
75 level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
76 -
76 -
77 \\n[rst2man-indent0]
77 \\n[rst2man-indent0]
78 \\n[rst2man-indent1]
78 \\n[rst2man-indent1]
79 \\n[rst2man-indent2]
79 \\n[rst2man-indent2]
80 ..
80 ..
81 .de1 INDENT
81 .de1 INDENT
82 .\" .rstReportMargin pre:
82 .\" .rstReportMargin pre:
83 . RS \\$1
83 . RS \\$1
84 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
84 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
85 . nr rst2man-indent-level +1
85 . nr rst2man-indent-level +1
86 .\" .rstReportMargin post:
86 .\" .rstReportMargin post:
87 ..
87 ..
88 .de UNINDENT
88 .de UNINDENT
89 . RE
89 . RE
90 .\" indent \\n[an-margin]
90 .\" indent \\n[an-margin]
91 .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
91 .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
92 .nr rst2man-indent-level -1
92 .nr rst2man-indent-level -1
93 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
93 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
94 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
94 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
95 ..
95 ..
96 """)
96 """)
97
97
98 class Writer(writers.Writer):
98 class Writer(writers.Writer):
99
99
100 supported = ('manpage')
100 supported = ('manpage')
101 """Formats this writer supports."""
101 """Formats this writer supports."""
102
102
103 output = None
103 output = None
104 """Final translated form of `document`."""
104 """Final translated form of `document`."""
105
105
106 def __init__(self):
106 def __init__(self):
107 writers.Writer.__init__(self)
107 writers.Writer.__init__(self)
108 self.translator_class = Translator
108 self.translator_class = Translator
109
109
110 def translate(self):
110 def translate(self):
111 visitor = self.translator_class(self.document)
111 visitor = self.translator_class(self.document)
112 self.document.walkabout(visitor)
112 self.document.walkabout(visitor)
113 self.output = visitor.astext()
113 self.output = visitor.astext()
114
114
115
115
116 class Table(object):
116 class Table(object):
117 def __init__(self):
117 def __init__(self):
118 self._rows = []
118 self._rows = []
119 self._options = ['center']
119 self._options = ['center']
120 self._tab_char = '\t'
120 self._tab_char = '\t'
121 self._coldefs = []
121 self._coldefs = []
122 def new_row(self):
122 def new_row(self):
123 self._rows.append([])
123 self._rows.append([])
124 def append_separator(self, separator):
124 def append_separator(self, separator):
125 """Append the separator for table head."""
125 """Append the separator for table head."""
126 self._rows.append([separator])
126 self._rows.append([separator])
127 def append_cell(self, cell_lines):
127 def append_cell(self, cell_lines):
128 """cell_lines is an array of lines"""
128 """cell_lines is an array of lines"""
129 start = 0
129 start = 0
130 if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
130 if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
131 start = 1
131 start = 1
132 self._rows[-1].append(cell_lines[start:])
132 self._rows[-1].append(cell_lines[start:])
133 if len(self._coldefs) < len(self._rows[-1]):
133 if len(self._coldefs) < len(self._rows[-1]):
134 self._coldefs.append('l')
134 self._coldefs.append('l')
135 def _minimize_cell(self, cell_lines):
135 def _minimize_cell(self, cell_lines):
136 """Remove leading and trailing blank and ``.sp`` lines"""
136 """Remove leading and trailing blank and ``.sp`` lines"""
137 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
137 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
138 del cell_lines[0]
138 del cell_lines[0]
139 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
139 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
140 del cell_lines[-1]
140 del cell_lines[-1]
141 def as_list(self):
141 def as_list(self):
142 text = ['.TS\n']
142 text = ['.TS\n']
143 text.append(' '.join(self._options) + ';\n')
143 text.append(' '.join(self._options) + ';\n')
144 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
144 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
145 for row in self._rows:
145 for row in self._rows:
146 # row = array of cells. cell = array of lines.
146 # row = array of cells. cell = array of lines.
147 text.append('_\n') # line above
147 text.append('_\n') # line above
148 text.append('T{\n')
148 text.append('T{\n')
149 for i in range(len(row)):
149 for i in range(len(row)):
150 cell = row[i]
150 cell = row[i]
151 self._minimize_cell(cell)
151 self._minimize_cell(cell)
152 text.extend(cell)
152 text.extend(cell)
153 if not text[-1].endswith('\n'):
153 if not text[-1].endswith('\n'):
154 text[-1] += '\n'
154 text[-1] += '\n'
155 if i < len(row) - 1:
155 if i < len(row) - 1:
156 text.append('T}'+self._tab_char+'T{\n')
156 text.append('T}'+self._tab_char+'T{\n')
157 else:
157 else:
158 text.append('T}\n')
158 text.append('T}\n')
159 text.append('_\n')
159 text.append('_\n')
160 text.append('.TE\n')
160 text.append('.TE\n')
161 return text
161 return text
162
162
163 class Translator(nodes.NodeVisitor):
163 class Translator(nodes.NodeVisitor):
164 """"""
164 """"""
165
165
166 words_and_spaces = re.compile(r'\S+| +|\n')
166 words_and_spaces = re.compile(r'\S+| +|\n')
167 document_start = """Man page generated from reStructuredText."""
167 document_start = """Man page generated from reStructuredText."""
168
168
169 def __init__(self, document):
169 def __init__(self, document):
170 nodes.NodeVisitor.__init__(self, document)
170 nodes.NodeVisitor.__init__(self, document)
171 self.settings = settings = document.settings
171 self.settings = settings = document.settings
172 lcode = settings.language_code
172 lcode = settings.language_code
173 arglen = len(inspect.getargspec(languages.get_language)[0])
173 arglen = len(inspect.getargspec(languages.get_language)[0])
174 if arglen == 2:
174 if arglen == 2:
175 self.language = languages.get_language(lcode,
175 self.language = languages.get_language(lcode,
176 self.document.reporter)
176 self.document.reporter)
177 else:
177 else:
178 self.language = languages.get_language(lcode)
178 self.language = languages.get_language(lcode)
179 self.head = []
179 self.head = []
180 self.body = []
180 self.body = []
181 self.foot = []
181 self.foot = []
182 self.section_level = 0
182 self.section_level = 0
183 self.context = []
183 self.context = []
184 self.topic_class = ''
184 self.topic_class = ''
185 self.colspecs = []
185 self.colspecs = []
186 self.compact_p = 1
186 self.compact_p = 1
187 self.compact_simple = None
187 self.compact_simple = None
188 # the list style "*" bullet or "#" numbered
188 # the list style "*" bullet or "#" numbered
189 self._list_char = []
189 self._list_char = []
190 # writing the header .TH and .SH NAME is postboned after
190 # writing the header .TH and .SH NAME is postboned after
191 # docinfo.
191 # docinfo.
192 self._docinfo = {
192 self._docinfo = {
193 "title" : "", "title_upper": "",
193 "title" : "", "title_upper": "",
194 "subtitle" : "",
194 "subtitle" : "",
195 "manual_section" : "", "manual_group" : "",
195 "manual_section" : "", "manual_group" : "",
196 "author" : [],
196 "author" : [],
197 "date" : "",
197 "date" : "",
198 "copyright" : "",
198 "copyright" : "",
199 "version" : "",
199 "version" : "",
200 }
200 }
201 self._docinfo_keys = [] # a list to keep the sequence as in source.
201 self._docinfo_keys = [] # a list to keep the sequence as in source.
202 self._docinfo_names = {} # to get name from text not normalized.
202 self._docinfo_names = {} # to get name from text not normalized.
203 self._in_docinfo = None
203 self._in_docinfo = None
204 self._active_table = None
204 self._active_table = None
205 self._in_literal = False
205 self._in_literal = False
206 self.header_written = 0
206 self.header_written = 0
207 self._line_block = 0
207 self._line_block = 0
208 self.authors = []
208 self.authors = []
209 self.section_level = 0
209 self.section_level = 0
210 self._indent = [0]
210 self._indent = [0]
211 # central definition of simple processing rules
211 # central definition of simple processing rules
212 # what to output on : visit, depart
212 # what to output on : visit, depart
213 # Do not use paragraph requests ``.PP`` because these set indentation.
213 # Do not use paragraph requests ``.PP`` because these set indentation.
214 # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
214 # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
215 #
215 #
216 # Fonts are put on a stack, the top one is used.
216 # Fonts are put on a stack, the top one is used.
217 # ``.ft P`` or ``\\fP`` pop from stack.
217 # ``.ft P`` or ``\\fP`` pop from stack.
218 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
218 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
219 # Hopefully ``C`` courier too.
219 # Hopefully ``C`` courier too.
220 self.defs = {
220 self.defs = {
221 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
221 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
222 'definition_list_item' : ('.TP', ''),
222 'definition_list_item' : ('.TP', ''),
223 'field_name' : ('.TP\n.B ', '\n'),
223 'field_name' : ('.TP\n.B ', '\n'),
224 'literal' : ('\\fB', '\\fP'),
224 'literal' : ('\\fB', '\\fP'),
225 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
225 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
226
226
227 'option_list_item' : ('.TP\n', ''),
227 'option_list_item' : ('.TP\n', ''),
228
228
229 'reference' : (r'\%', r'\:'),
229 'reference' : (r'\%', r'\:'),
230 'emphasis': ('\\fI', '\\fP'),
230 'emphasis': ('\\fI', '\\fP'),
231 'strong' : ('\\fB', '\\fP'),
231 'strong' : ('\\fB', '\\fP'),
232 'term' : ('\n.B ', '\n'),
232 'term' : ('\n.B ', '\n'),
233 'title_reference' : ('\\fI', '\\fP'),
233 'title_reference' : ('\\fI', '\\fP'),
234
234
235 'topic-title' : ('.SS ',),
235 'topic-title' : ('.SS ',),
236 'sidebar-title' : ('.SS ',),
236 'sidebar-title' : ('.SS ',),
237
237
238 'problematic' : ('\n.nf\n', '\n.fi\n'),
238 'problematic' : ('\n.nf\n', '\n.fi\n'),
239 }
239 }
240 # NOTE don't specify the newline before a dot-command, but ensure
240 # NOTE don't specify the newline before a dot-command, but ensure
241 # it is there.
241 # it is there.
242
242
243 def comment_begin(self, text):
243 def comment_begin(self, text):
244 """Return commented version of the passed text WITHOUT end of
244 """Return commented version of the passed text WITHOUT end of
245 line/comment."""
245 line/comment."""
246 prefix = '.\\" '
246 prefix = '.\\" '
247 out_text = ''.join(
247 out_text = ''.join(
248 [(prefix + in_line + '\n')
248 [(prefix + in_line + '\n')
249 for in_line in text.split('\n')])
249 for in_line in text.split('\n')])
250 return out_text
250 return out_text
251
251
252 def comment(self, text):
252 def comment(self, text):
253 """Return commented version of the passed text."""
253 """Return commented version of the passed text."""
254 return self.comment_begin(text)+'.\n'
254 return self.comment_begin(text)+'.\n'
255
255
256 def ensure_eol(self):
256 def ensure_eol(self):
257 """Ensure the last line in body is terminated by new line."""
257 """Ensure the last line in body is terminated by new line."""
258 if self.body[-1][-1] != '\n':
258 if self.body[-1][-1] != '\n':
259 self.body.append('\n')
259 self.body.append('\n')
260
260
261 def astext(self):
261 def astext(self):
262 """Return the final formatted document as a string."""
262 """Return the final formatted document as a string."""
263 if not self.header_written:
263 if not self.header_written:
264 # ensure we get a ".TH" as viewers require it.
264 # ensure we get a ".TH" as viewers require it.
265 self.head.append(self.header())
265 self.head.append(self.header())
266 # filter body
266 # filter body
267 for i in xrange(len(self.body) - 1, 0, -1):
267 for i in xrange(len(self.body) - 1, 0, -1):
268 # remove superfluous vertical gaps.
268 # remove superfluous vertical gaps.
269 if self.body[i] == '.sp\n':
269 if self.body[i] == '.sp\n':
270 if self.body[i - 1][:4] in ('.BI ','.IP '):
270 if self.body[i - 1][:4] in ('.BI ','.IP '):
271 self.body[i] = '.\n'
271 self.body[i] = '.\n'
272 elif (self.body[i - 1][:3] == '.B ' and
272 elif (self.body[i - 1][:3] == '.B ' and
273 self.body[i - 2][:4] == '.TP\n'):
273 self.body[i - 2][:4] == '.TP\n'):
274 self.body[i] = '.\n'
274 self.body[i] = '.\n'
275 elif (self.body[i - 1] == '\n' and
275 elif (self.body[i - 1] == '\n' and
276 self.body[i - 2][0] != '.' and
276 self.body[i - 2][0] != '.' and
277 (self.body[i - 3][:7] == '.TP\n.B '
277 (self.body[i - 3][:7] == '.TP\n.B '
278 or self.body[i - 3][:4] == '\n.B ')
278 or self.body[i - 3][:4] == '\n.B ')
279 ):
279 ):
280 self.body[i] = '.\n'
280 self.body[i] = '.\n'
281 return ''.join(self.head + self.body + self.foot)
281 return ''.join(self.head + self.body + self.foot)
282
282
283 def deunicode(self, text):
283 def deunicode(self, text):
284 text = text.replace(u'\xa0', '\\ ')
284 text = text.replace(u'\xa0', '\\ ')
285 text = text.replace(u'\u2020', '\\(dg')
285 text = text.replace(u'\u2020', '\\(dg')
286 return text
286 return text
287
287
288 def visit_Text(self, node):
288 def visit_Text(self, node):
289 text = node.astext()
289 text = node.astext()
290 text = text.replace('\\','\\e')
290 text = text.replace('\\','\\e')
291 replace_pairs = [
291 replace_pairs = [
292 (u'-', ur'\-'),
292 (u'-', ur'\-'),
293 (u'\'', ur'\(aq'),
293 (u'\'', ur'\(aq'),
294 (u'´', ur'\''),
294 (u'´', ur'\''),
295 (u'`', ur'\(ga'),
295 (u'`', ur'\(ga'),
296 ]
296 ]
297 for (in_char, out_markup) in replace_pairs:
297 for (in_char, out_markup) in replace_pairs:
298 text = text.replace(in_char, out_markup)
298 text = text.replace(in_char, out_markup)
299 # unicode
299 # unicode
300 text = self.deunicode(text)
300 text = self.deunicode(text)
301 if self._in_literal:
301 if self._in_literal:
302 # prevent interpretation of "." at line start
302 # prevent interpretation of "." at line start
303 if text[0] == '.':
303 if text[0] == '.':
304 text = '\\&' + text
304 text = '\\&' + text
305 text = text.replace('\n.', '\n\\&.')
305 text = text.replace('\n.', '\n\\&.')
306 self.body.append(text)
306 self.body.append(text)
307
307
308 def depart_Text(self, node):
308 def depart_Text(self, node):
309 pass
309 pass
310
310
311 def list_start(self, node):
311 def list_start(self, node):
312 class enum_char(object):
312 class enum_char(object):
313 enum_style = {
313 enum_style = {
314 'bullet' : '\\(bu',
314 'bullet' : '\\(bu',
315 'emdash' : '\\(em',
315 'emdash' : '\\(em',
316 }
316 }
317
317
318 def __init__(self, style):
318 def __init__(self, style):
319 self._style = style
319 self._style = style
320 if 'start' in node:
320 if 'start' in node:
321 self._cnt = node['start'] - 1
321 self._cnt = node['start'] - 1
322 else:
322 else:
323 self._cnt = 0
323 self._cnt = 0
324 self._indent = 2
324 self._indent = 2
325 if style == 'arabic':
325 if style == 'arabic':
326 # indentation depends on number of children
326 # indentation depends on number of children
327 # and start value.
327 # and start value.
328 self._indent = len(str(len(node.children)))
328 self._indent = len(str(len(node.children)))
329 self._indent += len(str(self._cnt)) + 1
329 self._indent += len(str(self._cnt)) + 1
330 elif style == 'loweralpha':
330 elif style == 'loweralpha':
331 self._cnt += ord('a') - 1
331 self._cnt += ord('a') - 1
332 self._indent = 3
332 self._indent = 3
333 elif style == 'upperalpha':
333 elif style == 'upperalpha':
334 self._cnt += ord('A') - 1
334 self._cnt += ord('A') - 1
335 self._indent = 3
335 self._indent = 3
336 elif style.endswith('roman'):
336 elif style.endswith('roman'):
337 self._indent = 5
337 self._indent = 5
338
338
339 def next(self):
339 def next(self):
340 if self._style == 'bullet':
340 if self._style == 'bullet':
341 return self.enum_style[self._style]
341 return self.enum_style[self._style]
342 elif self._style == 'emdash':
342 elif self._style == 'emdash':
343 return self.enum_style[self._style]
343 return self.enum_style[self._style]
344 self._cnt += 1
344 self._cnt += 1
345 # TODO add prefix postfix
345 # TODO add prefix postfix
346 if self._style == 'arabic':
346 if self._style == 'arabic':
347 return "%d." % self._cnt
347 return "%d." % self._cnt
348 elif self._style in ('loweralpha', 'upperalpha'):
348 elif self._style in ('loweralpha', 'upperalpha'):
349 return "%c." % self._cnt
349 return "%c." % self._cnt
350 elif self._style.endswith('roman'):
350 elif self._style.endswith('roman'):
351 res = roman.toRoman(self._cnt) + '.'
351 res = roman.toRoman(self._cnt) + '.'
352 if self._style.startswith('upper'):
352 if self._style.startswith('upper'):
353 return res.upper()
353 return res.upper()
354 return res.lower()
354 return res.lower()
355 else:
355 else:
356 return "%d." % self._cnt
356 return "%d." % self._cnt
357 def get_width(self):
357 def get_width(self):
358 return self._indent
358 return self._indent
359 def __repr__(self):
359 def __repr__(self):
360 return 'enum_style-%s' % list(self._style)
360 return 'enum_style-%s' % list(self._style)
361
361
362 if 'enumtype' in node:
362 if 'enumtype' in node:
363 self._list_char.append(enum_char(node['enumtype']))
363 self._list_char.append(enum_char(node['enumtype']))
364 else:
364 else:
365 self._list_char.append(enum_char('bullet'))
365 self._list_char.append(enum_char('bullet'))
366 if len(self._list_char) > 1:
366 if len(self._list_char) > 1:
367 # indent nested lists
367 # indent nested lists
368 self.indent(self._list_char[-2].get_width())
368 self.indent(self._list_char[-2].get_width())
369 else:
369 else:
370 self.indent(self._list_char[-1].get_width())
370 self.indent(self._list_char[-1].get_width())
371
371
372 def list_end(self):
372 def list_end(self):
373 self.dedent()
373 self.dedent()
374 self._list_char.pop()
374 self._list_char.pop()
375
375
376 def header(self):
376 def header(self):
377 tmpl = (".TH %(title_upper)s %(manual_section)s"
377 tmpl = (".TH %(title_upper)s %(manual_section)s"
378 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
378 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
379 ".SH NAME\n"
379 ".SH NAME\n"
380 "%(title)s \- %(subtitle)s\n")
380 "%(title)s \- %(subtitle)s\n")
381 return tmpl % self._docinfo
381 return tmpl % self._docinfo
382
382
383 def append_header(self):
383 def append_header(self):
384 """append header with .TH and .SH NAME"""
384 """append header with .TH and .SH NAME"""
385 # NOTE before everything
385 # NOTE before everything
386 # .TH title_upper section date source manual
386 # .TH title_upper section date source manual
387 if self.header_written:
387 if self.header_written:
388 return
388 return
389 self.body.append(self.header())
389 self.body.append(self.header())
390 self.body.append(MACRO_DEF)
390 self.body.append(MACRO_DEF)
391 self.header_written = 1
391 self.header_written = 1
392
392
393 def visit_address(self, node):
393 def visit_address(self, node):
394 self.visit_docinfo_item(node, 'address')
394 self.visit_docinfo_item(node, 'address')
395
395
396 def depart_address(self, node):
396 def depart_address(self, node):
397 pass
397 pass
398
398
399 def visit_admonition(self, node, name=None):
399 def visit_admonition(self, node, name=None):
400 if name:
400 if name:
401 self.body.append('.IP %s\n' %
401 self.body.append('.IP %s\n' %
402 self.language.labels.get(name, name))
402 self.language.labels.get(name, name))
403
403
404 def depart_admonition(self, node):
404 def depart_admonition(self, node):
405 self.body.append('.RE\n')
405 self.body.append('.RE\n')
406
406
407 def visit_attention(self, node):
407 def visit_attention(self, node):
408 self.visit_admonition(node, 'attention')
408 self.visit_admonition(node, 'attention')
409
409
410 depart_attention = depart_admonition
410 depart_attention = depart_admonition
411
411
412 def visit_docinfo_item(self, node, name):
412 def visit_docinfo_item(self, node, name):
413 if name == 'author':
413 if name == 'author':
414 self._docinfo[name].append(node.astext())
414 self._docinfo[name].append(node.astext())
415 else:
415 else:
416 self._docinfo[name] = node.astext()
416 self._docinfo[name] = node.astext()
417 self._docinfo_keys.append(name)
417 self._docinfo_keys.append(name)
418 raise nodes.SkipNode
418 raise nodes.SkipNode()
419
419
420 def depart_docinfo_item(self, node):
420 def depart_docinfo_item(self, node):
421 pass
421 pass
422
422
423 def visit_author(self, node):
423 def visit_author(self, node):
424 self.visit_docinfo_item(node, 'author')
424 self.visit_docinfo_item(node, 'author')
425
425
426 depart_author = depart_docinfo_item
426 depart_author = depart_docinfo_item
427
427
428 def visit_authors(self, node):
428 def visit_authors(self, node):
429 # _author is called anyway.
429 # _author is called anyway.
430 pass
430 pass
431
431
432 def depart_authors(self, node):
432 def depart_authors(self, node):
433 pass
433 pass
434
434
435 def visit_block_quote(self, node):
435 def visit_block_quote(self, node):
436 # BUG/HACK: indent always uses the _last_ indentation,
436 # BUG/HACK: indent always uses the _last_ indentation,
437 # thus we need two of them.
437 # thus we need two of them.
438 self.indent(BLOCKQOUTE_INDENT)
438 self.indent(BLOCKQOUTE_INDENT)
439 self.indent(0)
439 self.indent(0)
440
440
441 def depart_block_quote(self, node):
441 def depart_block_quote(self, node):
442 self.dedent()
442 self.dedent()
443 self.dedent()
443 self.dedent()
444
444
445 def visit_bullet_list(self, node):
445 def visit_bullet_list(self, node):
446 self.list_start(node)
446 self.list_start(node)
447
447
448 def depart_bullet_list(self, node):
448 def depart_bullet_list(self, node):
449 self.list_end()
449 self.list_end()
450
450
451 def visit_caption(self, node):
451 def visit_caption(self, node):
452 pass
452 pass
453
453
454 def depart_caption(self, node):
454 def depart_caption(self, node):
455 pass
455 pass
456
456
457 def visit_caution(self, node):
457 def visit_caution(self, node):
458 self.visit_admonition(node, 'caution')
458 self.visit_admonition(node, 'caution')
459
459
460 depart_caution = depart_admonition
460 depart_caution = depart_admonition
461
461
462 def visit_citation(self, node):
462 def visit_citation(self, node):
463 num, text = node.astext().split(None, 1)
463 num, text = node.astext().split(None, 1)
464 num = num.strip()
464 num = num.strip()
465 self.body.append('.IP [%s] 5\n' % num)
465 self.body.append('.IP [%s] 5\n' % num)
466
466
467 def depart_citation(self, node):
467 def depart_citation(self, node):
468 pass
468 pass
469
469
470 def visit_citation_reference(self, node):
470 def visit_citation_reference(self, node):
471 self.body.append('['+node.astext()+']')
471 self.body.append('['+node.astext()+']')
472 raise nodes.SkipNode
472 raise nodes.SkipNode()
473
473
474 def visit_classifier(self, node):
474 def visit_classifier(self, node):
475 pass
475 pass
476
476
477 def depart_classifier(self, node):
477 def depart_classifier(self, node):
478 pass
478 pass
479
479
480 def visit_colspec(self, node):
480 def visit_colspec(self, node):
481 self.colspecs.append(node)
481 self.colspecs.append(node)
482
482
483 def depart_colspec(self, node):
483 def depart_colspec(self, node):
484 pass
484 pass
485
485
486 def write_colspecs(self):
486 def write_colspecs(self):
487 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
487 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
488
488
489 def visit_comment(self, node,
489 def visit_comment(self, node,
490 sub=re.compile('-(?=-)').sub):
490 sub=re.compile('-(?=-)').sub):
491 self.body.append(self.comment(node.astext()))
491 self.body.append(self.comment(node.astext()))
492 raise nodes.SkipNode
492 raise nodes.SkipNode()
493
493
494 def visit_contact(self, node):
494 def visit_contact(self, node):
495 self.visit_docinfo_item(node, 'contact')
495 self.visit_docinfo_item(node, 'contact')
496
496
497 depart_contact = depart_docinfo_item
497 depart_contact = depart_docinfo_item
498
498
499 def visit_container(self, node):
499 def visit_container(self, node):
500 pass
500 pass
501
501
502 def depart_container(self, node):
502 def depart_container(self, node):
503 pass
503 pass
504
504
505 def visit_compound(self, node):
505 def visit_compound(self, node):
506 pass
506 pass
507
507
508 def depart_compound(self, node):
508 def depart_compound(self, node):
509 pass
509 pass
510
510
511 def visit_copyright(self, node):
511 def visit_copyright(self, node):
512 self.visit_docinfo_item(node, 'copyright')
512 self.visit_docinfo_item(node, 'copyright')
513
513
514 def visit_danger(self, node):
514 def visit_danger(self, node):
515 self.visit_admonition(node, 'danger')
515 self.visit_admonition(node, 'danger')
516
516
517 depart_danger = depart_admonition
517 depart_danger = depart_admonition
518
518
519 def visit_date(self, node):
519 def visit_date(self, node):
520 self.visit_docinfo_item(node, 'date')
520 self.visit_docinfo_item(node, 'date')
521
521
522 def visit_decoration(self, node):
522 def visit_decoration(self, node):
523 pass
523 pass
524
524
525 def depart_decoration(self, node):
525 def depart_decoration(self, node):
526 pass
526 pass
527
527
528 def visit_definition(self, node):
528 def visit_definition(self, node):
529 pass
529 pass
530
530
531 def depart_definition(self, node):
531 def depart_definition(self, node):
532 pass
532 pass
533
533
534 def visit_definition_list(self, node):
534 def visit_definition_list(self, node):
535 self.indent(DEFINITION_LIST_INDENT)
535 self.indent(DEFINITION_LIST_INDENT)
536
536
537 def depart_definition_list(self, node):
537 def depart_definition_list(self, node):
538 self.dedent()
538 self.dedent()
539
539
540 def visit_definition_list_item(self, node):
540 def visit_definition_list_item(self, node):
541 self.body.append(self.defs['definition_list_item'][0])
541 self.body.append(self.defs['definition_list_item'][0])
542
542
543 def depart_definition_list_item(self, node):
543 def depart_definition_list_item(self, node):
544 self.body.append(self.defs['definition_list_item'][1])
544 self.body.append(self.defs['definition_list_item'][1])
545
545
546 def visit_description(self, node):
546 def visit_description(self, node):
547 pass
547 pass
548
548
549 def depart_description(self, node):
549 def depart_description(self, node):
550 pass
550 pass
551
551
552 def visit_docinfo(self, node):
552 def visit_docinfo(self, node):
553 self._in_docinfo = 1
553 self._in_docinfo = 1
554
554
555 def depart_docinfo(self, node):
555 def depart_docinfo(self, node):
556 self._in_docinfo = None
556 self._in_docinfo = None
557 # NOTE nothing should be written before this
557 # NOTE nothing should be written before this
558 self.append_header()
558 self.append_header()
559
559
560 def visit_doctest_block(self, node):
560 def visit_doctest_block(self, node):
561 self.body.append(self.defs['literal_block'][0])
561 self.body.append(self.defs['literal_block'][0])
562 self._in_literal = True
562 self._in_literal = True
563
563
564 def depart_doctest_block(self, node):
564 def depart_doctest_block(self, node):
565 self._in_literal = False
565 self._in_literal = False
566 self.body.append(self.defs['literal_block'][1])
566 self.body.append(self.defs['literal_block'][1])
567
567
568 def visit_document(self, node):
568 def visit_document(self, node):
569 # no blank line between comment and header.
569 # no blank line between comment and header.
570 self.body.append(self.comment(self.document_start).rstrip()+'\n')
570 self.body.append(self.comment(self.document_start).rstrip()+'\n')
571 # writing header is postboned
571 # writing header is postboned
572 self.header_written = 0
572 self.header_written = 0
573
573
574 def depart_document(self, node):
574 def depart_document(self, node):
575 if self._docinfo['author']:
575 if self._docinfo['author']:
576 self.body.append('.SH AUTHOR\n%s\n'
576 self.body.append('.SH AUTHOR\n%s\n'
577 % ', '.join(self._docinfo['author']))
577 % ', '.join(self._docinfo['author']))
578 skip = ('author', 'copyright', 'date',
578 skip = ('author', 'copyright', 'date',
579 'manual_group', 'manual_section',
579 'manual_group', 'manual_section',
580 'subtitle',
580 'subtitle',
581 'title', 'title_upper', 'version')
581 'title', 'title_upper', 'version')
582 for name in self._docinfo_keys:
582 for name in self._docinfo_keys:
583 if name == 'address':
583 if name == 'address':
584 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
584 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
585 self.language.labels.get(name, name),
585 self.language.labels.get(name, name),
586 self.defs['indent'][0] % 0,
586 self.defs['indent'][0] % 0,
587 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
587 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
588 self._docinfo[name],
588 self._docinfo[name],
589 self.defs['indent'][1],
589 self.defs['indent'][1],
590 self.defs['indent'][1]))
590 self.defs['indent'][1]))
591 elif name not in skip:
591 elif name not in skip:
592 if name in self._docinfo_names:
592 if name in self._docinfo_names:
593 label = self._docinfo_names[name]
593 label = self._docinfo_names[name]
594 else:
594 else:
595 label = self.language.labels.get(name, name)
595 label = self.language.labels.get(name, name)
596 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
596 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
597 if self._docinfo['copyright']:
597 if self._docinfo['copyright']:
598 self.body.append('.SH COPYRIGHT\n%s\n'
598 self.body.append('.SH COPYRIGHT\n%s\n'
599 % self._docinfo['copyright'])
599 % self._docinfo['copyright'])
600 self.body.append(self.comment(
600 self.body.append(self.comment(
601 'Generated by docutils manpage writer.\n'))
601 'Generated by docutils manpage writer.\n'))
602
602
603 def visit_emphasis(self, node):
603 def visit_emphasis(self, node):
604 self.body.append(self.defs['emphasis'][0])
604 self.body.append(self.defs['emphasis'][0])
605
605
606 def depart_emphasis(self, node):
606 def depart_emphasis(self, node):
607 self.body.append(self.defs['emphasis'][1])
607 self.body.append(self.defs['emphasis'][1])
608
608
609 def visit_entry(self, node):
609 def visit_entry(self, node):
610 # a cell in a table row
610 # a cell in a table row
611 if 'morerows' in node:
611 if 'morerows' in node:
612 self.document.reporter.warning('"table row spanning" not supported',
612 self.document.reporter.warning('"table row spanning" not supported',
613 base_node=node)
613 base_node=node)
614 if 'morecols' in node:
614 if 'morecols' in node:
615 self.document.reporter.warning(
615 self.document.reporter.warning(
616 '"table cell spanning" not supported', base_node=node)
616 '"table cell spanning" not supported', base_node=node)
617 self.context.append(len(self.body))
617 self.context.append(len(self.body))
618
618
619 def depart_entry(self, node):
619 def depart_entry(self, node):
620 start = self.context.pop()
620 start = self.context.pop()
621 self._active_table.append_cell(self.body[start:])
621 self._active_table.append_cell(self.body[start:])
622 del self.body[start:]
622 del self.body[start:]
623
623
624 def visit_enumerated_list(self, node):
624 def visit_enumerated_list(self, node):
625 self.list_start(node)
625 self.list_start(node)
626
626
627 def depart_enumerated_list(self, node):
627 def depart_enumerated_list(self, node):
628 self.list_end()
628 self.list_end()
629
629
630 def visit_error(self, node):
630 def visit_error(self, node):
631 self.visit_admonition(node, 'error')
631 self.visit_admonition(node, 'error')
632
632
633 depart_error = depart_admonition
633 depart_error = depart_admonition
634
634
635 def visit_field(self, node):
635 def visit_field(self, node):
636 pass
636 pass
637
637
638 def depart_field(self, node):
638 def depart_field(self, node):
639 pass
639 pass
640
640
641 def visit_field_body(self, node):
641 def visit_field_body(self, node):
642 if self._in_docinfo:
642 if self._in_docinfo:
643 name_normalized = self._field_name.lower().replace(" ","_")
643 name_normalized = self._field_name.lower().replace(" ","_")
644 self._docinfo_names[name_normalized] = self._field_name
644 self._docinfo_names[name_normalized] = self._field_name
645 self.visit_docinfo_item(node, name_normalized)
645 self.visit_docinfo_item(node, name_normalized)
646 raise nodes.SkipNode
646 raise nodes.SkipNode()
647
647
648 def depart_field_body(self, node):
648 def depart_field_body(self, node):
649 pass
649 pass
650
650
651 def visit_field_list(self, node):
651 def visit_field_list(self, node):
652 self.indent(FIELD_LIST_INDENT)
652 self.indent(FIELD_LIST_INDENT)
653
653
654 def depart_field_list(self, node):
654 def depart_field_list(self, node):
655 self.dedent()
655 self.dedent()
656
656
657 def visit_field_name(self, node):
657 def visit_field_name(self, node):
658 if self._in_docinfo:
658 if self._in_docinfo:
659 self._field_name = node.astext()
659 self._field_name = node.astext()
660 raise nodes.SkipNode
660 raise nodes.SkipNode()
661 else:
661 else:
662 self.body.append(self.defs['field_name'][0])
662 self.body.append(self.defs['field_name'][0])
663
663
664 def depart_field_name(self, node):
664 def depart_field_name(self, node):
665 self.body.append(self.defs['field_name'][1])
665 self.body.append(self.defs['field_name'][1])
666
666
667 def visit_figure(self, node):
667 def visit_figure(self, node):
668 self.indent(2.5)
668 self.indent(2.5)
669 self.indent(0)
669 self.indent(0)
670
670
671 def depart_figure(self, node):
671 def depart_figure(self, node):
672 self.dedent()
672 self.dedent()
673 self.dedent()
673 self.dedent()
674
674
675 def visit_footer(self, node):
675 def visit_footer(self, node):
676 self.document.reporter.warning('"footer" not supported',
676 self.document.reporter.warning('"footer" not supported',
677 base_node=node)
677 base_node=node)
678
678
679 def depart_footer(self, node):
679 def depart_footer(self, node):
680 pass
680 pass
681
681
682 def visit_footnote(self, node):
682 def visit_footnote(self, node):
683 num, text = node.astext().split(None, 1)
683 num, text = node.astext().split(None, 1)
684 num = num.strip()
684 num = num.strip()
685 self.body.append('.IP [%s] 5\n' % self.deunicode(num))
685 self.body.append('.IP [%s] 5\n' % self.deunicode(num))
686
686
687 def depart_footnote(self, node):
687 def depart_footnote(self, node):
688 pass
688 pass
689
689
690 def footnote_backrefs(self, node):
690 def footnote_backrefs(self, node):
691 self.document.reporter.warning('"footnote_backrefs" not supported',
691 self.document.reporter.warning('"footnote_backrefs" not supported',
692 base_node=node)
692 base_node=node)
693
693
694 def visit_footnote_reference(self, node):
694 def visit_footnote_reference(self, node):
695 self.body.append('['+self.deunicode(node.astext())+']')
695 self.body.append('['+self.deunicode(node.astext())+']')
696 raise nodes.SkipNode
696 raise nodes.SkipNode()
697
697
698 def depart_footnote_reference(self, node):
698 def depart_footnote_reference(self, node):
699 pass
699 pass
700
700
701 def visit_generated(self, node):
701 def visit_generated(self, node):
702 pass
702 pass
703
703
704 def depart_generated(self, node):
704 def depart_generated(self, node):
705 pass
705 pass
706
706
707 def visit_header(self, node):
707 def visit_header(self, node):
708 raise NotImplementedError, node.astext()
708 raise NotImplementedError(node.astext())
709
709
710 def depart_header(self, node):
710 def depart_header(self, node):
711 pass
711 pass
712
712
713 def visit_hint(self, node):
713 def visit_hint(self, node):
714 self.visit_admonition(node, 'hint')
714 self.visit_admonition(node, 'hint')
715
715
716 depart_hint = depart_admonition
716 depart_hint = depart_admonition
717
717
718 def visit_subscript(self, node):
718 def visit_subscript(self, node):
719 self.body.append('\\s-2\\d')
719 self.body.append('\\s-2\\d')
720
720
721 def depart_subscript(self, node):
721 def depart_subscript(self, node):
722 self.body.append('\\u\\s0')
722 self.body.append('\\u\\s0')
723
723
724 def visit_superscript(self, node):
724 def visit_superscript(self, node):
725 self.body.append('\\s-2\\u')
725 self.body.append('\\s-2\\u')
726
726
727 def depart_superscript(self, node):
727 def depart_superscript(self, node):
728 self.body.append('\\d\\s0')
728 self.body.append('\\d\\s0')
729
729
730 def visit_attribution(self, node):
730 def visit_attribution(self, node):
731 self.body.append('\\(em ')
731 self.body.append('\\(em ')
732
732
733 def depart_attribution(self, node):
733 def depart_attribution(self, node):
734 self.body.append('\n')
734 self.body.append('\n')
735
735
736 def visit_image(self, node):
736 def visit_image(self, node):
737 self.document.reporter.warning('"image" not supported',
737 self.document.reporter.warning('"image" not supported',
738 base_node=node)
738 base_node=node)
739 text = []
739 text = []
740 if 'alt' in node.attributes:
740 if 'alt' in node.attributes:
741 text.append(node.attributes['alt'])
741 text.append(node.attributes['alt'])
742 if 'uri' in node.attributes:
742 if 'uri' in node.attributes:
743 text.append(node.attributes['uri'])
743 text.append(node.attributes['uri'])
744 self.body.append('[image: %s]\n' % ('/'.join(text)))
744 self.body.append('[image: %s]\n' % ('/'.join(text)))
745 raise nodes.SkipNode
745 raise nodes.SkipNode()
746
746
747 def visit_important(self, node):
747 def visit_important(self, node):
748 self.visit_admonition(node, 'important')
748 self.visit_admonition(node, 'important')
749
749
750 depart_important = depart_admonition
750 depart_important = depart_admonition
751
751
752 def visit_label(self, node):
752 def visit_label(self, node):
753 # footnote and citation
753 # footnote and citation
754 if (isinstance(node.parent, nodes.footnote)
754 if (isinstance(node.parent, nodes.footnote)
755 or isinstance(node.parent, nodes.citation)):
755 or isinstance(node.parent, nodes.citation)):
756 raise nodes.SkipNode
756 raise nodes.SkipNode()
757 self.document.reporter.warning('"unsupported "label"',
757 self.document.reporter.warning('"unsupported "label"',
758 base_node=node)
758 base_node=node)
759 self.body.append('[')
759 self.body.append('[')
760
760
761 def depart_label(self, node):
761 def depart_label(self, node):
762 self.body.append(']\n')
762 self.body.append(']\n')
763
763
764 def visit_legend(self, node):
764 def visit_legend(self, node):
765 pass
765 pass
766
766
767 def depart_legend(self, node):
767 def depart_legend(self, node):
768 pass
768 pass
769
769
770 # WHAT should we use .INDENT, .UNINDENT ?
770 # WHAT should we use .INDENT, .UNINDENT ?
771 def visit_line_block(self, node):
771 def visit_line_block(self, node):
772 self._line_block += 1
772 self._line_block += 1
773 if self._line_block == 1:
773 if self._line_block == 1:
774 self.body.append('.sp\n')
774 self.body.append('.sp\n')
775 self.body.append('.nf\n')
775 self.body.append('.nf\n')
776 else:
776 else:
777 self.body.append('.in +2\n')
777 self.body.append('.in +2\n')
778
778
779 def depart_line_block(self, node):
779 def depart_line_block(self, node):
780 self._line_block -= 1
780 self._line_block -= 1
781 if self._line_block == 0:
781 if self._line_block == 0:
782 self.body.append('.fi\n')
782 self.body.append('.fi\n')
783 self.body.append('.sp\n')
783 self.body.append('.sp\n')
784 else:
784 else:
785 self.body.append('.in -2\n')
785 self.body.append('.in -2\n')
786
786
787 def visit_line(self, node):
787 def visit_line(self, node):
788 pass
788 pass
789
789
790 def depart_line(self, node):
790 def depart_line(self, node):
791 self.body.append('\n')
791 self.body.append('\n')
792
792
793 def visit_list_item(self, node):
793 def visit_list_item(self, node):
794 # man 7 man argues to use ".IP" instead of ".TP"
794 # man 7 man argues to use ".IP" instead of ".TP"
795 self.body.append('.IP %s %d\n' % (
795 self.body.append('.IP %s %d\n' % (
796 self._list_char[-1].next(),
796 next(self._list_char[-1]),
797 self._list_char[-1].get_width(),))
797 self._list_char[-1].get_width(),))
798
798
799 def depart_list_item(self, node):
799 def depart_list_item(self, node):
800 pass
800 pass
801
801
802 def visit_literal(self, node):
802 def visit_literal(self, node):
803 self.body.append(self.defs['literal'][0])
803 self.body.append(self.defs['literal'][0])
804
804
805 def depart_literal(self, node):
805 def depart_literal(self, node):
806 self.body.append(self.defs['literal'][1])
806 self.body.append(self.defs['literal'][1])
807
807
808 def visit_literal_block(self, node):
808 def visit_literal_block(self, node):
809 self.body.append(self.defs['literal_block'][0])
809 self.body.append(self.defs['literal_block'][0])
810 self._in_literal = True
810 self._in_literal = True
811
811
812 def depart_literal_block(self, node):
812 def depart_literal_block(self, node):
813 self._in_literal = False
813 self._in_literal = False
814 self.body.append(self.defs['literal_block'][1])
814 self.body.append(self.defs['literal_block'][1])
815
815
816 def visit_meta(self, node):
816 def visit_meta(self, node):
817 raise NotImplementedError, node.astext()
817 raise NotImplementedError(node.astext())
818
818
819 def depart_meta(self, node):
819 def depart_meta(self, node):
820 pass
820 pass
821
821
822 def visit_note(self, node):
822 def visit_note(self, node):
823 self.visit_admonition(node, 'note')
823 self.visit_admonition(node, 'note')
824
824
825 depart_note = depart_admonition
825 depart_note = depart_admonition
826
826
827 def indent(self, by=0.5):
827 def indent(self, by=0.5):
828 # if we are in a section ".SH" there already is a .RS
828 # if we are in a section ".SH" there already is a .RS
829 step = self._indent[-1]
829 step = self._indent[-1]
830 self._indent.append(by)
830 self._indent.append(by)
831 self.body.append(self.defs['indent'][0] % step)
831 self.body.append(self.defs['indent'][0] % step)
832
832
833 def dedent(self):
833 def dedent(self):
834 self._indent.pop()
834 self._indent.pop()
835 self.body.append(self.defs['indent'][1])
835 self.body.append(self.defs['indent'][1])
836
836
837 def visit_option_list(self, node):
837 def visit_option_list(self, node):
838 self.indent(OPTION_LIST_INDENT)
838 self.indent(OPTION_LIST_INDENT)
839
839
840 def depart_option_list(self, node):
840 def depart_option_list(self, node):
841 self.dedent()
841 self.dedent()
842
842
843 def visit_option_list_item(self, node):
843 def visit_option_list_item(self, node):
844 # one item of the list
844 # one item of the list
845 self.body.append(self.defs['option_list_item'][0])
845 self.body.append(self.defs['option_list_item'][0])
846
846
847 def depart_option_list_item(self, node):
847 def depart_option_list_item(self, node):
848 self.body.append(self.defs['option_list_item'][1])
848 self.body.append(self.defs['option_list_item'][1])
849
849
850 def visit_option_group(self, node):
850 def visit_option_group(self, node):
851 # as one option could have several forms it is a group
851 # as one option could have several forms it is a group
852 # options without parameter bold only, .B, -v
852 # options without parameter bold only, .B, -v
853 # options with parameter bold italic, .BI, -f file
853 # options with parameter bold italic, .BI, -f file
854 #
854 #
855 # we do not know if .B or .BI
855 # we do not know if .B or .BI
856 self.context.append('.B') # blind guess
856 self.context.append('.B') # blind guess
857 self.context.append(len(self.body)) # to be able to insert later
857 self.context.append(len(self.body)) # to be able to insert later
858 self.context.append(0) # option counter
858 self.context.append(0) # option counter
859
859
860 def depart_option_group(self, node):
860 def depart_option_group(self, node):
861 self.context.pop() # the counter
861 self.context.pop() # the counter
862 start_position = self.context.pop()
862 start_position = self.context.pop()
863 text = self.body[start_position:]
863 text = self.body[start_position:]
864 del self.body[start_position:]
864 del self.body[start_position:]
865 self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
865 self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
866
866
867 def visit_option(self, node):
867 def visit_option(self, node):
868 # each form of the option will be presented separately
868 # each form of the option will be presented separately
869 if self.context[-1] > 0:
869 if self.context[-1] > 0:
870 self.body.append(', ')
870 self.body.append(', ')
871 if self.context[-3] == '.BI':
871 if self.context[-3] == '.BI':
872 self.body.append('\\')
872 self.body.append('\\')
873 self.body.append(' ')
873 self.body.append(' ')
874
874
875 def depart_option(self, node):
875 def depart_option(self, node):
876 self.context[-1] += 1
876 self.context[-1] += 1
877
877
878 def visit_option_string(self, node):
878 def visit_option_string(self, node):
879 # do not know if .B or .BI
879 # do not know if .B or .BI
880 pass
880 pass
881
881
882 def depart_option_string(self, node):
882 def depart_option_string(self, node):
883 pass
883 pass
884
884
885 def visit_option_argument(self, node):
885 def visit_option_argument(self, node):
886 self.context[-3] = '.BI' # bold/italic alternate
886 self.context[-3] = '.BI' # bold/italic alternate
887 if node['delimiter'] != ' ':
887 if node['delimiter'] != ' ':
888 self.body.append('\\fB%s ' % node['delimiter'])
888 self.body.append('\\fB%s ' % node['delimiter'])
889 elif self.body[len(self.body) - 1].endswith('='):
889 elif self.body[len(self.body) - 1].endswith('='):
890 # a blank only means no blank in output, just changing font
890 # a blank only means no blank in output, just changing font
891 self.body.append(' ')
891 self.body.append(' ')
892 else:
892 else:
893 # blank backslash blank, switch font then a blank
893 # blank backslash blank, switch font then a blank
894 self.body.append(' \\ ')
894 self.body.append(' \\ ')
895
895
896 def depart_option_argument(self, node):
896 def depart_option_argument(self, node):
897 pass
897 pass
898
898
899 def visit_organization(self, node):
899 def visit_organization(self, node):
900 self.visit_docinfo_item(node, 'organization')
900 self.visit_docinfo_item(node, 'organization')
901
901
902 def depart_organization(self, node):
902 def depart_organization(self, node):
903 pass
903 pass
904
904
905 def visit_paragraph(self, node):
905 def visit_paragraph(self, node):
906 # ``.PP`` : Start standard indented paragraph.
906 # ``.PP`` : Start standard indented paragraph.
907 # ``.LP`` : Start block paragraph, all except the first.
907 # ``.LP`` : Start block paragraph, all except the first.
908 # ``.P [type]`` : Start paragraph type.
908 # ``.P [type]`` : Start paragraph type.
909 # NOTE don't use paragraph starts because they reset indentation.
909 # NOTE don't use paragraph starts because they reset indentation.
910 # ``.sp`` is only vertical space
910 # ``.sp`` is only vertical space
911 self.ensure_eol()
911 self.ensure_eol()
912 self.body.append('.sp\n')
912 self.body.append('.sp\n')
913
913
914 def depart_paragraph(self, node):
914 def depart_paragraph(self, node):
915 self.body.append('\n')
915 self.body.append('\n')
916
916
917 def visit_problematic(self, node):
917 def visit_problematic(self, node):
918 self.body.append(self.defs['problematic'][0])
918 self.body.append(self.defs['problematic'][0])
919
919
920 def depart_problematic(self, node):
920 def depart_problematic(self, node):
921 self.body.append(self.defs['problematic'][1])
921 self.body.append(self.defs['problematic'][1])
922
922
923 def visit_raw(self, node):
923 def visit_raw(self, node):
924 if node.get('format') == 'manpage':
924 if node.get('format') == 'manpage':
925 self.body.append(node.astext() + "\n")
925 self.body.append(node.astext() + "\n")
926 # Keep non-manpage raw text out of output:
926 # Keep non-manpage raw text out of output:
927 raise nodes.SkipNode
927 raise nodes.SkipNode()
928
928
929 def visit_reference(self, node):
929 def visit_reference(self, node):
930 """E.g. link or email address."""
930 """E.g. link or email address."""
931 self.body.append(self.defs['reference'][0])
931 self.body.append(self.defs['reference'][0])
932
932
933 def depart_reference(self, node):
933 def depart_reference(self, node):
934 self.body.append(self.defs['reference'][1])
934 self.body.append(self.defs['reference'][1])
935
935
936 def visit_revision(self, node):
936 def visit_revision(self, node):
937 self.visit_docinfo_item(node, 'revision')
937 self.visit_docinfo_item(node, 'revision')
938
938
939 depart_revision = depart_docinfo_item
939 depart_revision = depart_docinfo_item
940
940
941 def visit_row(self, node):
941 def visit_row(self, node):
942 self._active_table.new_row()
942 self._active_table.new_row()
943
943
944 def depart_row(self, node):
944 def depart_row(self, node):
945 pass
945 pass
946
946
947 def visit_section(self, node):
947 def visit_section(self, node):
948 self.section_level += 1
948 self.section_level += 1
949
949
950 def depart_section(self, node):
950 def depart_section(self, node):
951 self.section_level -= 1
951 self.section_level -= 1
952
952
953 def visit_status(self, node):
953 def visit_status(self, node):
954 self.visit_docinfo_item(node, 'status')
954 self.visit_docinfo_item(node, 'status')
955
955
956 depart_status = depart_docinfo_item
956 depart_status = depart_docinfo_item
957
957
958 def visit_strong(self, node):
958 def visit_strong(self, node):
959 self.body.append(self.defs['strong'][0])
959 self.body.append(self.defs['strong'][0])
960
960
961 def depart_strong(self, node):
961 def depart_strong(self, node):
962 self.body.append(self.defs['strong'][1])
962 self.body.append(self.defs['strong'][1])
963
963
964 def visit_substitution_definition(self, node):
964 def visit_substitution_definition(self, node):
965 """Internal only."""
965 """Internal only."""
966 raise nodes.SkipNode
966 raise nodes.SkipNode()
967
967
968 def visit_substitution_reference(self, node):
968 def visit_substitution_reference(self, node):
969 self.document.reporter.warning('"substitution_reference" not supported',
969 self.document.reporter.warning('"substitution_reference" not supported',
970 base_node=node)
970 base_node=node)
971
971
972 def visit_subtitle(self, node):
972 def visit_subtitle(self, node):
973 if isinstance(node.parent, nodes.sidebar):
973 if isinstance(node.parent, nodes.sidebar):
974 self.body.append(self.defs['strong'][0])
974 self.body.append(self.defs['strong'][0])
975 elif isinstance(node.parent, nodes.document):
975 elif isinstance(node.parent, nodes.document):
976 self.visit_docinfo_item(node, 'subtitle')
976 self.visit_docinfo_item(node, 'subtitle')
977 elif isinstance(node.parent, nodes.section):
977 elif isinstance(node.parent, nodes.section):
978 self.body.append(self.defs['strong'][0])
978 self.body.append(self.defs['strong'][0])
979
979
980 def depart_subtitle(self, node):
980 def depart_subtitle(self, node):
981 # document subtitle calls SkipNode
981 # document subtitle calls SkipNode
982 self.body.append(self.defs['strong'][1]+'\n.PP\n')
982 self.body.append(self.defs['strong'][1]+'\n.PP\n')
983
983
984 def visit_system_message(self, node):
984 def visit_system_message(self, node):
985 # TODO add report_level
985 # TODO add report_level
986 #if node['level'] < self.document.reporter['writer'].report_level:
986 #if node['level'] < self.document.reporter['writer'].report_level:
987 # Level is too low to display:
987 # Level is too low to display:
988 # raise nodes.SkipNode
988 # raise nodes.SkipNode
989 attr = {}
989 attr = {}
990 if node.hasattr('id'):
990 if node.hasattr('id'):
991 attr['name'] = node['id']
991 attr['name'] = node['id']
992 if node.hasattr('line'):
992 if node.hasattr('line'):
993 line = ', line %s' % node['line']
993 line = ', line %s' % node['line']
994 else:
994 else:
995 line = ''
995 line = ''
996 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
996 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
997 % (node['type'], node['level'], node['source'], line))
997 % (node['type'], node['level'], node['source'], line))
998
998
999 def depart_system_message(self, node):
999 def depart_system_message(self, node):
1000 pass
1000 pass
1001
1001
1002 def visit_table(self, node):
1002 def visit_table(self, node):
1003 self._active_table = Table()
1003 self._active_table = Table()
1004
1004
1005 def depart_table(self, node):
1005 def depart_table(self, node):
1006 self.ensure_eol()
1006 self.ensure_eol()
1007 self.body.extend(self._active_table.as_list())
1007 self.body.extend(self._active_table.as_list())
1008 self._active_table = None
1008 self._active_table = None
1009
1009
1010 def visit_target(self, node):
1010 def visit_target(self, node):
1011 # targets are in-document hyper targets, without any use for man-pages.
1011 # targets are in-document hyper targets, without any use for man-pages.
1012 raise nodes.SkipNode
1012 raise nodes.SkipNode()
1013
1013
1014 def visit_tbody(self, node):
1014 def visit_tbody(self, node):
1015 pass
1015 pass
1016
1016
1017 def depart_tbody(self, node):
1017 def depart_tbody(self, node):
1018 pass
1018 pass
1019
1019
1020 def visit_term(self, node):
1020 def visit_term(self, node):
1021 self.body.append(self.defs['term'][0])
1021 self.body.append(self.defs['term'][0])
1022
1022
1023 def depart_term(self, node):
1023 def depart_term(self, node):
1024 self.body.append(self.defs['term'][1])
1024 self.body.append(self.defs['term'][1])
1025
1025
1026 def visit_tgroup(self, node):
1026 def visit_tgroup(self, node):
1027 pass
1027 pass
1028
1028
1029 def depart_tgroup(self, node):
1029 def depart_tgroup(self, node):
1030 pass
1030 pass
1031
1031
1032 def visit_thead(self, node):
1032 def visit_thead(self, node):
1033 # MAYBE double line '='
1033 # MAYBE double line '='
1034 pass
1034 pass
1035
1035
1036 def depart_thead(self, node):
1036 def depart_thead(self, node):
1037 # MAYBE double line '='
1037 # MAYBE double line '='
1038 pass
1038 pass
1039
1039
1040 def visit_tip(self, node):
1040 def visit_tip(self, node):
1041 self.visit_admonition(node, 'tip')
1041 self.visit_admonition(node, 'tip')
1042
1042
1043 depart_tip = depart_admonition
1043 depart_tip = depart_admonition
1044
1044
1045 def visit_title(self, node):
1045 def visit_title(self, node):
1046 if isinstance(node.parent, nodes.topic):
1046 if isinstance(node.parent, nodes.topic):
1047 self.body.append(self.defs['topic-title'][0])
1047 self.body.append(self.defs['topic-title'][0])
1048 elif isinstance(node.parent, nodes.sidebar):
1048 elif isinstance(node.parent, nodes.sidebar):
1049 self.body.append(self.defs['sidebar-title'][0])
1049 self.body.append(self.defs['sidebar-title'][0])
1050 elif isinstance(node.parent, nodes.admonition):
1050 elif isinstance(node.parent, nodes.admonition):
1051 self.body.append('.IP "')
1051 self.body.append('.IP "')
1052 elif self.section_level == 0:
1052 elif self.section_level == 0:
1053 self._docinfo['title'] = node.astext()
1053 self._docinfo['title'] = node.astext()
1054 # document title for .TH
1054 # document title for .TH
1055 self._docinfo['title_upper'] = node.astext().upper()
1055 self._docinfo['title_upper'] = node.astext().upper()
1056 raise nodes.SkipNode
1056 raise nodes.SkipNode()
1057 elif self.section_level == 1:
1057 elif self.section_level == 1:
1058 self.body.append('.SH ')
1058 self.body.append('.SH ')
1059 for n in node.traverse(nodes.Text):
1059 for n in node.traverse(nodes.Text):
1060 n.parent.replace(n, nodes.Text(n.astext().upper()))
1060 n.parent.replace(n, nodes.Text(n.astext().upper()))
1061 else:
1061 else:
1062 self.body.append('.SS ')
1062 self.body.append('.SS ')
1063
1063
1064 def depart_title(self, node):
1064 def depart_title(self, node):
1065 if isinstance(node.parent, nodes.admonition):
1065 if isinstance(node.parent, nodes.admonition):
1066 self.body.append('"')
1066 self.body.append('"')
1067 self.body.append('\n')
1067 self.body.append('\n')
1068
1068
1069 def visit_title_reference(self, node):
1069 def visit_title_reference(self, node):
1070 """inline citation reference"""
1070 """inline citation reference"""
1071 self.body.append(self.defs['title_reference'][0])
1071 self.body.append(self.defs['title_reference'][0])
1072
1072
1073 def depart_title_reference(self, node):
1073 def depart_title_reference(self, node):
1074 self.body.append(self.defs['title_reference'][1])
1074 self.body.append(self.defs['title_reference'][1])
1075
1075
1076 def visit_topic(self, node):
1076 def visit_topic(self, node):
1077 pass
1077 pass
1078
1078
1079 def depart_topic(self, node):
1079 def depart_topic(self, node):
1080 pass
1080 pass
1081
1081
1082 def visit_sidebar(self, node):
1082 def visit_sidebar(self, node):
1083 pass
1083 pass
1084
1084
1085 def depart_sidebar(self, node):
1085 def depart_sidebar(self, node):
1086 pass
1086 pass
1087
1087
1088 def visit_rubric(self, node):
1088 def visit_rubric(self, node):
1089 pass
1089 pass
1090
1090
1091 def depart_rubric(self, node):
1091 def depart_rubric(self, node):
1092 pass
1092 pass
1093
1093
1094 def visit_transition(self, node):
1094 def visit_transition(self, node):
1095 # .PP Begin a new paragraph and reset prevailing indent.
1095 # .PP Begin a new paragraph and reset prevailing indent.
1096 # .sp N leaves N lines of blank space.
1096 # .sp N leaves N lines of blank space.
1097 # .ce centers the next line
1097 # .ce centers the next line
1098 self.body.append('\n.sp\n.ce\n----\n')
1098 self.body.append('\n.sp\n.ce\n----\n')
1099
1099
1100 def depart_transition(self, node):
1100 def depart_transition(self, node):
1101 self.body.append('\n.ce 0\n.sp\n')
1101 self.body.append('\n.ce 0\n.sp\n')
1102
1102
1103 def visit_version(self, node):
1103 def visit_version(self, node):
1104 self.visit_docinfo_item(node, 'version')
1104 self.visit_docinfo_item(node, 'version')
1105
1105
1106 def visit_warning(self, node):
1106 def visit_warning(self, node):
1107 self.visit_admonition(node, 'warning')
1107 self.visit_admonition(node, 'warning')
1108
1108
1109 depart_warning = depart_admonition
1109 depart_warning = depart_admonition
1110
1110
1111 def unimplemented_visit(self, node):
1111 def unimplemented_visit(self, node):
1112 raise NotImplementedError('visiting unimplemented node type: %s'
1112 raise NotImplementedError('visiting unimplemented node type: %s'
1113 % node.__class__.__name__)
1113 % node.__class__.__name__)
1114
1114
1115 # vim: set fileencoding=utf-8 et ts=4 ai :
1115 # vim: set fileencoding=utf-8 et ts=4 ai :
@@ -1,43 +1,45
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # mercurial - scalable distributed SCM
3 # mercurial - scalable distributed SCM
4 #
4 #
5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2005-2007 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 import os
10 import os
11 import sys
11 import sys
12
12
13 if os.environ.get('HGUNICODEPEDANTRY', False):
13 if os.environ.get('HGUNICODEPEDANTRY', False):
14 reload(sys)
14 try:
15 sys.setdefaultencoding("undefined")
15 reload(sys)
16
16 sys.setdefaultencoding("undefined")
17 except NameError:
18 pass
17
19
18 libdir = '@LIBDIR@'
20 libdir = '@LIBDIR@'
19
21
20 if libdir != '@' 'LIBDIR' '@':
22 if libdir != '@' 'LIBDIR' '@':
21 if not os.path.isabs(libdir):
23 if not os.path.isabs(libdir):
22 libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
24 libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
23 libdir)
25 libdir)
24 libdir = os.path.abspath(libdir)
26 libdir = os.path.abspath(libdir)
25 sys.path.insert(0, libdir)
27 sys.path.insert(0, libdir)
26
28
27 # enable importing on demand to reduce startup time
29 # enable importing on demand to reduce startup time
28 try:
30 try:
29 from mercurial import demandimport; demandimport.enable()
31 if sys.version_info[0] < 3:
32 from mercurial import demandimport; demandimport.enable()
30 except ImportError:
33 except ImportError:
31 import sys
32 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
34 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
33 ' '.join(sys.path))
35 ' '.join(sys.path))
34 sys.stderr.write("(check your install and PYTHONPATH)\n")
36 sys.stderr.write("(check your install and PYTHONPATH)\n")
35 sys.exit(-1)
37 sys.exit(-1)
36
38
37 import mercurial.util
39 import mercurial.util
38 import mercurial.dispatch
40 import mercurial.dispatch
39
41
40 for fp in (sys.stdin, sys.stdout, sys.stderr):
42 for fp in (sys.stdin, sys.stdout, sys.stderr):
41 mercurial.util.setbinary(fp)
43 mercurial.util.setbinary(fp)
42
44
43 mercurial.dispatch.run()
45 mercurial.dispatch.run()
@@ -1,100 +1,100
1 # automv.py
1 # automv.py
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 """Check for unrecorded moves at commit time (EXPERIMENTAL)
7 """Check for unrecorded moves at commit time (EXPERIMENTAL)
8
8
9 This extension checks at commit/amend time if any of the committed files
9 This extension checks at commit/amend time if any of the committed files
10 comes from an unrecorded mv.
10 comes from an unrecorded mv.
11
11
12 The threshold at which a file is considered a move can be set with the
12 The threshold at which a file is considered a move can be set with the
13 ``automv.similarity`` config option. This option takes a percentage between 0
13 ``automv.similarity`` config option. This option takes a percentage between 0
14 (disabled) and 100 (files must be identical), the default is 95.
14 (disabled) and 100 (files must be identical), the default is 95.
15
15
16 """
16 """
17
17
18 # Using 95 as a default similarity is based on an analysis of the mercurial
18 # Using 95 as a default similarity is based on an analysis of the mercurial
19 # repositories of the cpython, mozilla-central & mercurial repositories, as
19 # repositories of the cpython, mozilla-central & mercurial repositories, as
20 # well as 2 very large facebook repositories. At 95 50% of all potential
20 # well as 2 very large facebook repositories. At 95 50% of all potential
21 # missed moves would be caught, as well as correspond with 87% of all
21 # missed moves would be caught, as well as correspond with 87% of all
22 # explicitly marked moves. Together, 80% of moved files are 95% similar or
22 # explicitly marked moves. Together, 80% of moved files are 95% similar or
23 # more.
23 # more.
24 #
24 #
25 # See http://markmail.org/thread/5pxnljesvufvom57 for context.
25 # See http://markmail.org/thread/5pxnljesvufvom57 for context.
26
26
27 from __future__ import absolute_import
27 from __future__ import absolute_import
28
28
29 from mercurial.i18n import _
29 from mercurial import (
30 from mercurial import (
30 commands,
31 commands,
31 copies,
32 copies,
32 error,
33 error,
33 extensions,
34 extensions,
34 scmutil,
35 scmutil,
35 similar
36 similar
36 )
37 )
37 from mercurial.i18n import _
38
38
39 def extsetup(ui):
39 def extsetup(ui):
40 entry = extensions.wrapcommand(
40 entry = extensions.wrapcommand(
41 commands.table, 'commit', mvcheck)
41 commands.table, 'commit', mvcheck)
42 entry[1].append(
42 entry[1].append(
43 ('', 'no-automv', None,
43 ('', 'no-automv', None,
44 _('disable automatic file move detection')))
44 _('disable automatic file move detection')))
45
45
46 def mvcheck(orig, ui, repo, *pats, **opts):
46 def mvcheck(orig, ui, repo, *pats, **opts):
47 """Hook to check for moves at commit time"""
47 """Hook to check for moves at commit time"""
48 renames = None
48 renames = None
49 disabled = opts.pop('no_automv', False)
49 disabled = opts.pop('no_automv', False)
50 if not disabled:
50 if not disabled:
51 threshold = ui.configint('automv', 'similarity', 95)
51 threshold = ui.configint('automv', 'similarity', 95)
52 if not 0 <= threshold <= 100:
52 if not 0 <= threshold <= 100:
53 raise error.Abort(_('automv.similarity must be between 0 and 100'))
53 raise error.Abort(_('automv.similarity must be between 0 and 100'))
54 if threshold > 0:
54 if threshold > 0:
55 match = scmutil.match(repo[None], pats, opts)
55 match = scmutil.match(repo[None], pats, opts)
56 added, removed = _interestingfiles(repo, match)
56 added, removed = _interestingfiles(repo, match)
57 renames = _findrenames(repo, match, added, removed,
57 renames = _findrenames(repo, match, added, removed,
58 threshold / 100.0)
58 threshold / 100.0)
59
59
60 with repo.wlock():
60 with repo.wlock():
61 if renames is not None:
61 if renames is not None:
62 scmutil._markchanges(repo, (), (), renames)
62 scmutil._markchanges(repo, (), (), renames)
63 return orig(ui, repo, *pats, **opts)
63 return orig(ui, repo, *pats, **opts)
64
64
65 def _interestingfiles(repo, matcher):
65 def _interestingfiles(repo, matcher):
66 """Find what files were added or removed in this commit.
66 """Find what files were added or removed in this commit.
67
67
68 Returns a tuple of two lists: (added, removed). Only files not *already*
68 Returns a tuple of two lists: (added, removed). Only files not *already*
69 marked as moved are included in the added list.
69 marked as moved are included in the added list.
70
70
71 """
71 """
72 stat = repo.status(match=matcher)
72 stat = repo.status(match=matcher)
73 added = stat[1]
73 added = stat[1]
74 removed = stat[2]
74 removed = stat[2]
75
75
76 copy = copies._forwardcopies(repo['.'], repo[None], matcher)
76 copy = copies._forwardcopies(repo['.'], repo[None], matcher)
77 # remove the copy files for which we already have copy info
77 # remove the copy files for which we already have copy info
78 added = [f for f in added if f not in copy]
78 added = [f for f in added if f not in copy]
79
79
80 return added, removed
80 return added, removed
81
81
82 def _findrenames(repo, matcher, added, removed, similarity):
82 def _findrenames(repo, matcher, added, removed, similarity):
83 """Find what files in added are really moved files.
83 """Find what files in added are really moved files.
84
84
85 Any file named in removed that is at least similarity% similar to a file
85 Any file named in removed that is at least similarity% similar to a file
86 in added is seen as a rename.
86 in added is seen as a rename.
87
87
88 """
88 """
89 renames = {}
89 renames = {}
90 if similarity > 0:
90 if similarity > 0:
91 for src, dst, score in similar.findrenames(
91 for src, dst, score in similar.findrenames(
92 repo, added, removed, similarity):
92 repo, added, removed, similarity):
93 if repo.ui.verbose:
93 if repo.ui.verbose:
94 repo.ui.status(
94 repo.ui.status(
95 _('detected move of %s as %s (%d%% similar)\n') % (
95 _('detected move of %s as %s (%d%% similar)\n') % (
96 matcher.rel(src), matcher.rel(dst), score * 100))
96 matcher.rel(src), matcher.rel(dst), score * 100))
97 renames[dst] = src
97 renames[dst] = src
98 if renames:
98 if renames:
99 repo.ui.status(_('detected move of %d files\n') % len(renames))
99 repo.ui.status(_('detected move of %d files\n') % len(renames))
100 return renames
100 return renames
@@ -1,927 +1,928
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 import urlparse
285 import xmlrpclib
286
284
287 from mercurial.i18n import _
285 from mercurial.i18n import _
288 from mercurial.node import short
286 from mercurial.node import short
289 from mercurial import (
287 from mercurial import (
290 cmdutil,
288 cmdutil,
291 error,
289 error,
292 mail,
290 mail,
293 util,
291 util,
294 )
292 )
295
293
294 urlparse = util.urlparse
295 xmlrpclib = util.xmlrpclib
296
296 # Note for extension authors: ONLY specify testedwith = 'internal' for
297 # Note for extension authors: ONLY specify testedwith = 'internal' for
297 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
298 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
298 # 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
299 # leave the attribute unspecified.
300 # leave the attribute unspecified.
300 testedwith = 'internal'
301 testedwith = 'internal'
301
302
302 class bzaccess(object):
303 class bzaccess(object):
303 '''Base class for access to Bugzilla.'''
304 '''Base class for access to Bugzilla.'''
304
305
305 def __init__(self, ui):
306 def __init__(self, ui):
306 self.ui = ui
307 self.ui = ui
307 usermap = self.ui.config('bugzilla', 'usermap')
308 usermap = self.ui.config('bugzilla', 'usermap')
308 if usermap:
309 if usermap:
309 self.ui.readconfig(usermap, sections=['usermap'])
310 self.ui.readconfig(usermap, sections=['usermap'])
310
311
311 def map_committer(self, user):
312 def map_committer(self, user):
312 '''map name of committer to Bugzilla user name.'''
313 '''map name of committer to Bugzilla user name.'''
313 for committer, bzuser in self.ui.configitems('usermap'):
314 for committer, bzuser in self.ui.configitems('usermap'):
314 if committer.lower() == user.lower():
315 if committer.lower() == user.lower():
315 return bzuser
316 return bzuser
316 return user
317 return user
317
318
318 # Methods to be implemented by access classes.
319 # Methods to be implemented by access classes.
319 #
320 #
320 # '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
321 # updates to bug state. Recognized dict keys are:
322 # updates to bug state. Recognized dict keys are:
322 #
323 #
323 # 'hours': Value, float containing work hours to be updated.
324 # 'hours': Value, float containing work hours to be updated.
324 # '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.
325
326
326 def filter_real_bug_ids(self, bugs):
327 def filter_real_bug_ids(self, bugs):
327 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
328 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
328 pass
329 pass
329
330
330 def filter_cset_known_bug_ids(self, node, bugs):
331 def filter_cset_known_bug_ids(self, node, bugs):
331 '''remove bug IDs where node occurs in comment text from bugs.'''
332 '''remove bug IDs where node occurs in comment text from bugs.'''
332 pass
333 pass
333
334
334 def updatebug(self, bugid, newstate, text, committer):
335 def updatebug(self, bugid, newstate, text, committer):
335 '''update the specified bug. Add comment text and set new states.
336 '''update the specified bug. Add comment text and set new states.
336
337
337 If possible add the comment as being from the committer of
338 If possible add the comment as being from the committer of
338 the changeset. Otherwise use the default Bugzilla user.
339 the changeset. Otherwise use the default Bugzilla user.
339 '''
340 '''
340 pass
341 pass
341
342
342 def notify(self, bugs, committer):
343 def notify(self, bugs, committer):
343 '''Force sending of Bugzilla notification emails.
344 '''Force sending of Bugzilla notification emails.
344
345
345 Only required if the access method does not trigger notification
346 Only required if the access method does not trigger notification
346 emails automatically.
347 emails automatically.
347 '''
348 '''
348 pass
349 pass
349
350
350 # Bugzilla via direct access to MySQL database.
351 # Bugzilla via direct access to MySQL database.
351 class bzmysql(bzaccess):
352 class bzmysql(bzaccess):
352 '''Support for direct MySQL access to Bugzilla.
353 '''Support for direct MySQL access to Bugzilla.
353
354
354 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.
355
356
356 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
357 recommended to use the XMLRPC access method instead.
358 recommended to use the XMLRPC access method instead.
358 '''
359 '''
359
360
360 @staticmethod
361 @staticmethod
361 def sql_buglist(ids):
362 def sql_buglist(ids):
362 '''return SQL-friendly list of bug ids'''
363 '''return SQL-friendly list of bug ids'''
363 return '(' + ','.join(map(str, ids)) + ')'
364 return '(' + ','.join(map(str, ids)) + ')'
364
365
365 _MySQLdb = None
366 _MySQLdb = None
366
367
367 def __init__(self, ui):
368 def __init__(self, ui):
368 try:
369 try:
369 import MySQLdb as mysql
370 import MySQLdb as mysql
370 bzmysql._MySQLdb = mysql
371 bzmysql._MySQLdb = mysql
371 except ImportError as err:
372 except ImportError as err:
372 raise error.Abort(_('python mysql support not available: %s') % err)
373 raise error.Abort(_('python mysql support not available: %s') % err)
373
374
374 bzaccess.__init__(self, ui)
375 bzaccess.__init__(self, ui)
375
376
376 host = self.ui.config('bugzilla', 'host', 'localhost')
377 host = self.ui.config('bugzilla', 'host', 'localhost')
377 user = self.ui.config('bugzilla', 'user', 'bugs')
378 user = self.ui.config('bugzilla', 'user', 'bugs')
378 passwd = self.ui.config('bugzilla', 'password')
379 passwd = self.ui.config('bugzilla', 'password')
379 db = self.ui.config('bugzilla', 'db', 'bugs')
380 db = self.ui.config('bugzilla', 'db', 'bugs')
380 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
381 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
381 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') %
382 (host, db, user, '*' * len(passwd)))
383 (host, db, user, '*' * len(passwd)))
383 self.conn = bzmysql._MySQLdb.connect(host=host,
384 self.conn = bzmysql._MySQLdb.connect(host=host,
384 user=user, passwd=passwd,
385 user=user, passwd=passwd,
385 db=db,
386 db=db,
386 connect_timeout=timeout)
387 connect_timeout=timeout)
387 self.cursor = self.conn.cursor()
388 self.cursor = self.conn.cursor()
388 self.longdesc_id = self.get_longdesc_id()
389 self.longdesc_id = self.get_longdesc_id()
389 self.user_ids = {}
390 self.user_ids = {}
390 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
391 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
391
392
392 def run(self, *args, **kwargs):
393 def run(self, *args, **kwargs):
393 '''run a query.'''
394 '''run a query.'''
394 self.ui.note(_('query: %s %s\n') % (args, kwargs))
395 self.ui.note(_('query: %s %s\n') % (args, kwargs))
395 try:
396 try:
396 self.cursor.execute(*args, **kwargs)
397 self.cursor.execute(*args, **kwargs)
397 except bzmysql._MySQLdb.MySQLError:
398 except bzmysql._MySQLdb.MySQLError:
398 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
399 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
399 raise
400 raise
400
401
401 def get_longdesc_id(self):
402 def get_longdesc_id(self):
402 '''get identity of longdesc field'''
403 '''get identity of longdesc field'''
403 self.run('select fieldid from fielddefs where name = "longdesc"')
404 self.run('select fieldid from fielddefs where name = "longdesc"')
404 ids = self.cursor.fetchall()
405 ids = self.cursor.fetchall()
405 if len(ids) != 1:
406 if len(ids) != 1:
406 raise error.Abort(_('unknown database schema'))
407 raise error.Abort(_('unknown database schema'))
407 return ids[0][0]
408 return ids[0][0]
408
409
409 def filter_real_bug_ids(self, bugs):
410 def filter_real_bug_ids(self, bugs):
410 '''filter not-existing bugs from set.'''
411 '''filter not-existing bugs from set.'''
411 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' %
412 bzmysql.sql_buglist(bugs.keys()))
413 bzmysql.sql_buglist(bugs.keys()))
413 existing = [id for (id,) in self.cursor.fetchall()]
414 existing = [id for (id,) in self.cursor.fetchall()]
414 for id in bugs.keys():
415 for id in bugs.keys():
415 if id not in existing:
416 if id not in existing:
416 self.ui.status(_('bug %d does not exist\n') % id)
417 self.ui.status(_('bug %d does not exist\n') % id)
417 del bugs[id]
418 del bugs[id]
418
419
419 def filter_cset_known_bug_ids(self, node, bugs):
420 def filter_cset_known_bug_ids(self, node, bugs):
420 '''filter bug ids that already refer to this changeset from set.'''
421 '''filter bug ids that already refer to this changeset from set.'''
421 self.run('''select bug_id from longdescs where
422 self.run('''select bug_id from longdescs where
422 bug_id in %s and thetext like "%%%s%%"''' %
423 bug_id in %s and thetext like "%%%s%%"''' %
423 (bzmysql.sql_buglist(bugs.keys()), short(node)))
424 (bzmysql.sql_buglist(bugs.keys()), short(node)))
424 for (id,) in self.cursor.fetchall():
425 for (id,) in self.cursor.fetchall():
425 self.ui.status(_('bug %d already knows about changeset %s\n') %
426 self.ui.status(_('bug %d already knows about changeset %s\n') %
426 (id, short(node)))
427 (id, short(node)))
427 del bugs[id]
428 del bugs[id]
428
429
429 def notify(self, bugs, committer):
430 def notify(self, bugs, committer):
430 '''tell bugzilla to send mail.'''
431 '''tell bugzilla to send mail.'''
431 self.ui.status(_('telling bugzilla to send mail:\n'))
432 self.ui.status(_('telling bugzilla to send mail:\n'))
432 (user, userid) = self.get_bugzilla_user(committer)
433 (user, userid) = self.get_bugzilla_user(committer)
433 for id in bugs.keys():
434 for id in bugs.keys():
434 self.ui.status(_(' bug %s\n') % id)
435 self.ui.status(_(' bug %s\n') % id)
435 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
436 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
436 bzdir = self.ui.config('bugzilla', 'bzdir',
437 bzdir = self.ui.config('bugzilla', 'bzdir',
437 '/var/www/html/bugzilla')
438 '/var/www/html/bugzilla')
438 try:
439 try:
439 # Backwards-compatible with old notify string, which
440 # Backwards-compatible with old notify string, which
440 # took one string. This will throw with a new format
441 # took one string. This will throw with a new format
441 # string.
442 # string.
442 cmd = cmdfmt % id
443 cmd = cmdfmt % id
443 except TypeError:
444 except TypeError:
444 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
445 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
445 self.ui.note(_('running notify command %s\n') % cmd)
446 self.ui.note(_('running notify command %s\n') % cmd)
446 fp = util.popen('(%s) 2>&1' % cmd)
447 fp = util.popen('(%s) 2>&1' % cmd)
447 out = fp.read()
448 out = fp.read()
448 ret = fp.close()
449 ret = fp.close()
449 if ret:
450 if ret:
450 self.ui.warn(out)
451 self.ui.warn(out)
451 raise error.Abort(_('bugzilla notify command %s') %
452 raise error.Abort(_('bugzilla notify command %s') %
452 util.explainexit(ret)[0])
453 util.explainexit(ret)[0])
453 self.ui.status(_('done\n'))
454 self.ui.status(_('done\n'))
454
455
455 def get_user_id(self, user):
456 def get_user_id(self, user):
456 '''look up numeric bugzilla user id.'''
457 '''look up numeric bugzilla user id.'''
457 try:
458 try:
458 return self.user_ids[user]
459 return self.user_ids[user]
459 except KeyError:
460 except KeyError:
460 try:
461 try:
461 userid = int(user)
462 userid = int(user)
462 except ValueError:
463 except ValueError:
463 self.ui.note(_('looking up user %s\n') % user)
464 self.ui.note(_('looking up user %s\n') % user)
464 self.run('''select userid from profiles
465 self.run('''select userid from profiles
465 where login_name like %s''', user)
466 where login_name like %s''', user)
466 all = self.cursor.fetchall()
467 all = self.cursor.fetchall()
467 if len(all) != 1:
468 if len(all) != 1:
468 raise KeyError(user)
469 raise KeyError(user)
469 userid = int(all[0][0])
470 userid = int(all[0][0])
470 self.user_ids[user] = userid
471 self.user_ids[user] = userid
471 return userid
472 return userid
472
473
473 def get_bugzilla_user(self, committer):
474 def get_bugzilla_user(self, committer):
474 '''See if committer is a registered bugzilla user. Return
475 '''See if committer is a registered bugzilla user. Return
475 bugzilla username and userid if so. If not, return default
476 bugzilla username and userid if so. If not, return default
476 bugzilla username and userid.'''
477 bugzilla username and userid.'''
477 user = self.map_committer(committer)
478 user = self.map_committer(committer)
478 try:
479 try:
479 userid = self.get_user_id(user)
480 userid = self.get_user_id(user)
480 except KeyError:
481 except KeyError:
481 try:
482 try:
482 defaultuser = self.ui.config('bugzilla', 'bzuser')
483 defaultuser = self.ui.config('bugzilla', 'bzuser')
483 if not defaultuser:
484 if not defaultuser:
484 raise error.Abort(_('cannot find bugzilla user id for %s') %
485 raise error.Abort(_('cannot find bugzilla user id for %s') %
485 user)
486 user)
486 userid = self.get_user_id(defaultuser)
487 userid = self.get_user_id(defaultuser)
487 user = defaultuser
488 user = defaultuser
488 except KeyError:
489 except KeyError:
489 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')
490 % (user, defaultuser))
491 % (user, defaultuser))
491 return (user, userid)
492 return (user, userid)
492
493
493 def updatebug(self, bugid, newstate, text, committer):
494 def updatebug(self, bugid, newstate, text, committer):
494 '''update bug state with comment text.
495 '''update bug state with comment text.
495
496
496 Try adding comment as committer of changeset, otherwise as
497 Try adding comment as committer of changeset, otherwise as
497 default bugzilla user.'''
498 default bugzilla user.'''
498 if len(newstate) > 0:
499 if len(newstate) > 0:
499 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
500 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
500
501
501 (user, userid) = self.get_bugzilla_user(committer)
502 (user, userid) = self.get_bugzilla_user(committer)
502 now = time.strftime('%Y-%m-%d %H:%M:%S')
503 now = time.strftime('%Y-%m-%d %H:%M:%S')
503 self.run('''insert into longdescs
504 self.run('''insert into longdescs
504 (bug_id, who, bug_when, thetext)
505 (bug_id, who, bug_when, thetext)
505 values (%s, %s, %s, %s)''',
506 values (%s, %s, %s, %s)''',
506 (bugid, userid, now, text))
507 (bugid, userid, now, text))
507 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)
508 values (%s, %s, %s, %s)''',
509 values (%s, %s, %s, %s)''',
509 (bugid, userid, now, self.longdesc_id))
510 (bugid, userid, now, self.longdesc_id))
510 self.conn.commit()
511 self.conn.commit()
511
512
512 class bzmysql_2_18(bzmysql):
513 class bzmysql_2_18(bzmysql):
513 '''support for bugzilla 2.18 series.'''
514 '''support for bugzilla 2.18 series.'''
514
515
515 def __init__(self, ui):
516 def __init__(self, ui):
516 bzmysql.__init__(self, ui)
517 bzmysql.__init__(self, ui)
517 self.default_notify = \
518 self.default_notify = \
518 "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"
519
520
520 class bzmysql_3_0(bzmysql_2_18):
521 class bzmysql_3_0(bzmysql_2_18):
521 '''support for bugzilla 3.0 series.'''
522 '''support for bugzilla 3.0 series.'''
522
523
523 def __init__(self, ui):
524 def __init__(self, ui):
524 bzmysql_2_18.__init__(self, ui)
525 bzmysql_2_18.__init__(self, ui)
525
526
526 def get_longdesc_id(self):
527 def get_longdesc_id(self):
527 '''get identity of longdesc field'''
528 '''get identity of longdesc field'''
528 self.run('select id from fielddefs where name = "longdesc"')
529 self.run('select id from fielddefs where name = "longdesc"')
529 ids = self.cursor.fetchall()
530 ids = self.cursor.fetchall()
530 if len(ids) != 1:
531 if len(ids) != 1:
531 raise error.Abort(_('unknown database schema'))
532 raise error.Abort(_('unknown database schema'))
532 return ids[0][0]
533 return ids[0][0]
533
534
534 # Bugzilla via XMLRPC interface.
535 # Bugzilla via XMLRPC interface.
535
536
536 class cookietransportrequest(object):
537 class cookietransportrequest(object):
537 """A Transport request method that retains cookies over its lifetime.
538 """A Transport request method that retains cookies over its lifetime.
538
539
539 The regular xmlrpclib transports ignore cookies. Which causes
540 The regular xmlrpclib transports ignore cookies. Which causes
540 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
541 the Bugzilla XMLRPC interface prior to 4.4.3.
542 the Bugzilla XMLRPC interface prior to 4.4.3.
542
543
543 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
544 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
545 requests.
546 requests.
546 """
547 """
547
548
548 # Inspiration drawn from
549 # Inspiration drawn from
549 # 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
550 # 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/
551
552
552 cookies = []
553 cookies = []
553 def send_cookies(self, connection):
554 def send_cookies(self, connection):
554 if self.cookies:
555 if self.cookies:
555 for cookie in self.cookies:
556 for cookie in self.cookies:
556 connection.putheader("Cookie", cookie)
557 connection.putheader("Cookie", cookie)
557
558
558 def request(self, host, handler, request_body, verbose=0):
559 def request(self, host, handler, request_body, verbose=0):
559 self.verbose = verbose
560 self.verbose = verbose
560 self.accept_gzip_encoding = False
561 self.accept_gzip_encoding = False
561
562
562 # issue XML-RPC request
563 # issue XML-RPC request
563 h = self.make_connection(host)
564 h = self.make_connection(host)
564 if verbose:
565 if verbose:
565 h.set_debuglevel(1)
566 h.set_debuglevel(1)
566
567
567 self.send_request(h, handler, request_body)
568 self.send_request(h, handler, request_body)
568 self.send_host(h, host)
569 self.send_host(h, host)
569 self.send_cookies(h)
570 self.send_cookies(h)
570 self.send_user_agent(h)
571 self.send_user_agent(h)
571 self.send_content(h, request_body)
572 self.send_content(h, request_body)
572
573
573 # 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.
574 # 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
575 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
576 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
576 # HTTP(S) has an underlying HTTP(S)Connection, so extract
577 # HTTP(S) has an underlying HTTP(S)Connection, so extract
577 # that and use it.
578 # that and use it.
578 try:
579 try:
579 response = h.getresponse()
580 response = h.getresponse()
580 except AttributeError:
581 except AttributeError:
581 response = h._conn.getresponse()
582 response = h._conn.getresponse()
582
583
583 # Add any cookie definitions to our list.
584 # Add any cookie definitions to our list.
584 for header in response.msg.getallmatchingheaders("Set-Cookie"):
585 for header in response.msg.getallmatchingheaders("Set-Cookie"):
585 val = header.split(": ", 1)[1]
586 val = header.split(": ", 1)[1]
586 cookie = val.split(";", 1)[0]
587 cookie = val.split(";", 1)[0]
587 self.cookies.append(cookie)
588 self.cookies.append(cookie)
588
589
589 if response.status != 200:
590 if response.status != 200:
590 raise xmlrpclib.ProtocolError(host + handler, response.status,
591 raise xmlrpclib.ProtocolError(host + handler, response.status,
591 response.reason, response.msg.headers)
592 response.reason, response.msg.headers)
592
593
593 payload = response.read()
594 payload = response.read()
594 parser, unmarshaller = self.getparser()
595 parser, unmarshaller = self.getparser()
595 parser.feed(payload)
596 parser.feed(payload)
596 parser.close()
597 parser.close()
597
598
598 return unmarshaller.close()
599 return unmarshaller.close()
599
600
600 # The explicit calls to the underlying xmlrpclib __init__() methods are
601 # The explicit calls to the underlying xmlrpclib __init__() methods are
601 # necessary. The xmlrpclib.Transport classes are old-style classes, and
602 # necessary. The xmlrpclib.Transport classes are old-style classes, and
602 # 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
603 # inheritance with a new-style class.
604 # inheritance with a new-style class.
604 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
605 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
605 def __init__(self, use_datetime=0):
606 def __init__(self, use_datetime=0):
606 if util.safehasattr(xmlrpclib.Transport, "__init__"):
607 if util.safehasattr(xmlrpclib.Transport, "__init__"):
607 xmlrpclib.Transport.__init__(self, use_datetime)
608 xmlrpclib.Transport.__init__(self, use_datetime)
608
609
609 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
610 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
610 def __init__(self, use_datetime=0):
611 def __init__(self, use_datetime=0):
611 if util.safehasattr(xmlrpclib.Transport, "__init__"):
612 if util.safehasattr(xmlrpclib.Transport, "__init__"):
612 xmlrpclib.SafeTransport.__init__(self, use_datetime)
613 xmlrpclib.SafeTransport.__init__(self, use_datetime)
613
614
614 class bzxmlrpc(bzaccess):
615 class bzxmlrpc(bzaccess):
615 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
616 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
616
617
617 Requires a minimum Bugzilla version 3.4.
618 Requires a minimum Bugzilla version 3.4.
618 """
619 """
619
620
620 def __init__(self, ui):
621 def __init__(self, ui):
621 bzaccess.__init__(self, ui)
622 bzaccess.__init__(self, ui)
622
623
623 bzweb = self.ui.config('bugzilla', 'bzurl',
624 bzweb = self.ui.config('bugzilla', 'bzurl',
624 'http://localhost/bugzilla/')
625 'http://localhost/bugzilla/')
625 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
626 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
626
627
627 user = self.ui.config('bugzilla', 'user', 'bugs')
628 user = self.ui.config('bugzilla', 'user', 'bugs')
628 passwd = self.ui.config('bugzilla', 'password')
629 passwd = self.ui.config('bugzilla', 'password')
629
630
630 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
631 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
631 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
632 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
632 'FIXED')
633 'FIXED')
633
634
634 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
635 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
635 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
636 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
636 self.bzvermajor = int(ver[0])
637 self.bzvermajor = int(ver[0])
637 self.bzverminor = int(ver[1])
638 self.bzverminor = int(ver[1])
638 login = self.bzproxy.User.login({'login': user, 'password': passwd,
639 login = self.bzproxy.User.login({'login': user, 'password': passwd,
639 'restrict_login': True})
640 'restrict_login': True})
640 self.bztoken = login.get('token', '')
641 self.bztoken = login.get('token', '')
641
642
642 def transport(self, uri):
643 def transport(self, uri):
643 if urlparse.urlparse(uri, "http")[0] == "https":
644 if urlparse.urlparse(uri, "http")[0] == "https":
644 return cookiesafetransport()
645 return cookiesafetransport()
645 else:
646 else:
646 return cookietransport()
647 return cookietransport()
647
648
648 def get_bug_comments(self, id):
649 def get_bug_comments(self, id):
649 """Return a string with all comment text for a bug."""
650 """Return a string with all comment text for a bug."""
650 c = self.bzproxy.Bug.comments({'ids': [id],
651 c = self.bzproxy.Bug.comments({'ids': [id],
651 'include_fields': ['text'],
652 'include_fields': ['text'],
652 'token': self.bztoken})
653 'token': self.bztoken})
653 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']])
654
655
655 def filter_real_bug_ids(self, bugs):
656 def filter_real_bug_ids(self, bugs):
656 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
657 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
657 'include_fields': [],
658 'include_fields': [],
658 'permissive': True,
659 'permissive': True,
659 'token': self.bztoken,
660 'token': self.bztoken,
660 })
661 })
661 for badbug in probe['faults']:
662 for badbug in probe['faults']:
662 id = badbug['id']
663 id = badbug['id']
663 self.ui.status(_('bug %d does not exist\n') % id)
664 self.ui.status(_('bug %d does not exist\n') % id)
664 del bugs[id]
665 del bugs[id]
665
666
666 def filter_cset_known_bug_ids(self, node, bugs):
667 def filter_cset_known_bug_ids(self, node, bugs):
667 for id in sorted(bugs.keys()):
668 for id in sorted(bugs.keys()):
668 if self.get_bug_comments(id).find(short(node)) != -1:
669 if self.get_bug_comments(id).find(short(node)) != -1:
669 self.ui.status(_('bug %d already knows about changeset %s\n') %
670 self.ui.status(_('bug %d already knows about changeset %s\n') %
670 (id, short(node)))
671 (id, short(node)))
671 del bugs[id]
672 del bugs[id]
672
673
673 def updatebug(self, bugid, newstate, text, committer):
674 def updatebug(self, bugid, newstate, text, committer):
674 args = {}
675 args = {}
675 if 'hours' in newstate:
676 if 'hours' in newstate:
676 args['work_time'] = newstate['hours']
677 args['work_time'] = newstate['hours']
677
678
678 if self.bzvermajor >= 4:
679 if self.bzvermajor >= 4:
679 args['ids'] = [bugid]
680 args['ids'] = [bugid]
680 args['comment'] = {'body' : text}
681 args['comment'] = {'body' : text}
681 if 'fix' in newstate:
682 if 'fix' in newstate:
682 args['status'] = self.fixstatus
683 args['status'] = self.fixstatus
683 args['resolution'] = self.fixresolution
684 args['resolution'] = self.fixresolution
684 args['token'] = self.bztoken
685 args['token'] = self.bztoken
685 self.bzproxy.Bug.update(args)
686 self.bzproxy.Bug.update(args)
686 else:
687 else:
687 if 'fix' in newstate:
688 if 'fix' in newstate:
688 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
689 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
689 "to mark bugs fixed\n"))
690 "to mark bugs fixed\n"))
690 args['id'] = bugid
691 args['id'] = bugid
691 args['comment'] = text
692 args['comment'] = text
692 self.bzproxy.Bug.add_comment(args)
693 self.bzproxy.Bug.add_comment(args)
693
694
694 class bzxmlrpcemail(bzxmlrpc):
695 class bzxmlrpcemail(bzxmlrpc):
695 """Read data from Bugzilla via XMLRPC, send updates via email.
696 """Read data from Bugzilla via XMLRPC, send updates via email.
696
697
697 Advantages of sending updates via email:
698 Advantages of sending updates via email:
698 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.
699 2. Bug statuses or other fields not accessible via XMLRPC can
700 2. Bug statuses or other fields not accessible via XMLRPC can
700 potentially be updated.
701 potentially be updated.
701
702
702 There is no XMLRPC function to change bug status before Bugzilla
703 There is no XMLRPC function to change bug status before Bugzilla
703 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.
704 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.
705 """
706 """
706
707
707 # 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,
708 # in-email fields are specified as '@<fieldname> = <value>'. In
709 # in-email fields are specified as '@<fieldname> = <value>'. In
709 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
710 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
710 # 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
711 # 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
712 # 4.0 onwards.
713 # 4.0 onwards.
713
714
714 def __init__(self, ui):
715 def __init__(self, ui):
715 bzxmlrpc.__init__(self, ui)
716 bzxmlrpc.__init__(self, ui)
716
717
717 self.bzemail = self.ui.config('bugzilla', 'bzemail')
718 self.bzemail = self.ui.config('bugzilla', 'bzemail')
718 if not self.bzemail:
719 if not self.bzemail:
719 raise error.Abort(_("configuration 'bzemail' missing"))
720 raise error.Abort(_("configuration 'bzemail' missing"))
720 mail.validateconfig(self.ui)
721 mail.validateconfig(self.ui)
721
722
722 def makecommandline(self, fieldname, value):
723 def makecommandline(self, fieldname, value):
723 if self.bzvermajor >= 4:
724 if self.bzvermajor >= 4:
724 return "@%s %s" % (fieldname, str(value))
725 return "@%s %s" % (fieldname, str(value))
725 else:
726 else:
726 if fieldname == "id":
727 if fieldname == "id":
727 fieldname = "bug_id"
728 fieldname = "bug_id"
728 return "@%s = %s" % (fieldname, str(value))
729 return "@%s = %s" % (fieldname, str(value))
729
730
730 def send_bug_modify_email(self, bugid, commands, comment, committer):
731 def send_bug_modify_email(self, bugid, commands, comment, committer):
731 '''send modification message to Bugzilla bug via email.
732 '''send modification message to Bugzilla bug via email.
732
733
733 The message format is documented in the Bugzilla email_in.pl
734 The message format is documented in the Bugzilla email_in.pl
734 specification. commands is a list of command lines, comment is the
735 specification. commands is a list of command lines, comment is the
735 comment text.
736 comment text.
736
737
737 To stop users from crafting commit comments with
738 To stop users from crafting commit comments with
738 Bugzilla commands, specify the bug ID via the message body, rather
739 Bugzilla commands, specify the bug ID via the message body, rather
739 than the subject line, and leave a blank line after it.
740 than the subject line, and leave a blank line after it.
740 '''
741 '''
741 user = self.map_committer(committer)
742 user = self.map_committer(committer)
742 matches = self.bzproxy.User.get({'match': [user],
743 matches = self.bzproxy.User.get({'match': [user],
743 'token': self.bztoken})
744 'token': self.bztoken})
744 if not matches['users']:
745 if not matches['users']:
745 user = self.ui.config('bugzilla', 'user', 'bugs')
746 user = self.ui.config('bugzilla', 'user', 'bugs')
746 matches = self.bzproxy.User.get({'match': [user],
747 matches = self.bzproxy.User.get({'match': [user],
747 'token': self.bztoken})
748 'token': self.bztoken})
748 if not matches['users']:
749 if not matches['users']:
749 raise error.Abort(_("default bugzilla user %s email not found")
750 raise error.Abort(_("default bugzilla user %s email not found")
750 % user)
751 % user)
751 user = matches['users'][0]['email']
752 user = matches['users'][0]['email']
752 commands.append(self.makecommandline("id", bugid))
753 commands.append(self.makecommandline("id", bugid))
753
754
754 text = "\n".join(commands) + "\n\n" + comment
755 text = "\n".join(commands) + "\n\n" + comment
755
756
756 _charsets = mail._charsets(self.ui)
757 _charsets = mail._charsets(self.ui)
757 user = mail.addressencode(self.ui, user, _charsets)
758 user = mail.addressencode(self.ui, user, _charsets)
758 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
759 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
759 msg = mail.mimeencode(self.ui, text, _charsets)
760 msg = mail.mimeencode(self.ui, text, _charsets)
760 msg['From'] = user
761 msg['From'] = user
761 msg['To'] = bzemail
762 msg['To'] = bzemail
762 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
763 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
763 sendmail = mail.connect(self.ui)
764 sendmail = mail.connect(self.ui)
764 sendmail(user, bzemail, msg.as_string())
765 sendmail(user, bzemail, msg.as_string())
765
766
766 def updatebug(self, bugid, newstate, text, committer):
767 def updatebug(self, bugid, newstate, text, committer):
767 cmds = []
768 cmds = []
768 if 'hours' in newstate:
769 if 'hours' in newstate:
769 cmds.append(self.makecommandline("work_time", newstate['hours']))
770 cmds.append(self.makecommandline("work_time", newstate['hours']))
770 if 'fix' in newstate:
771 if 'fix' in newstate:
771 cmds.append(self.makecommandline("bug_status", self.fixstatus))
772 cmds.append(self.makecommandline("bug_status", self.fixstatus))
772 cmds.append(self.makecommandline("resolution", self.fixresolution))
773 cmds.append(self.makecommandline("resolution", self.fixresolution))
773 self.send_bug_modify_email(bugid, cmds, text, committer)
774 self.send_bug_modify_email(bugid, cmds, text, committer)
774
775
775 class bugzilla(object):
776 class bugzilla(object):
776 # supported versions of bugzilla. different versions have
777 # supported versions of bugzilla. different versions have
777 # different schemas.
778 # different schemas.
778 _versions = {
779 _versions = {
779 '2.16': bzmysql,
780 '2.16': bzmysql,
780 '2.18': bzmysql_2_18,
781 '2.18': bzmysql_2_18,
781 '3.0': bzmysql_3_0,
782 '3.0': bzmysql_3_0,
782 'xmlrpc': bzxmlrpc,
783 'xmlrpc': bzxmlrpc,
783 'xmlrpc+email': bzxmlrpcemail
784 'xmlrpc+email': bzxmlrpcemail
784 }
785 }
785
786
786 _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*'
787 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
788 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
788 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
789 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
789
790
790 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
791 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
791 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
792 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
792 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
793 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
793 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
794 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
794
795
795 def __init__(self, ui, repo):
796 def __init__(self, ui, repo):
796 self.ui = ui
797 self.ui = ui
797 self.repo = repo
798 self.repo = repo
798
799
799 bzversion = self.ui.config('bugzilla', 'version')
800 bzversion = self.ui.config('bugzilla', 'version')
800 try:
801 try:
801 bzclass = bugzilla._versions[bzversion]
802 bzclass = bugzilla._versions[bzversion]
802 except KeyError:
803 except KeyError:
803 raise error.Abort(_('bugzilla version %s not supported') %
804 raise error.Abort(_('bugzilla version %s not supported') %
804 bzversion)
805 bzversion)
805 self.bzdriver = bzclass(self.ui)
806 self.bzdriver = bzclass(self.ui)
806
807
807 self.bug_re = re.compile(
808 self.bug_re = re.compile(
808 self.ui.config('bugzilla', 'regexp',
809 self.ui.config('bugzilla', 'regexp',
809 bugzilla._default_bug_re), re.IGNORECASE)
810 bugzilla._default_bug_re), re.IGNORECASE)
810 self.fix_re = re.compile(
811 self.fix_re = re.compile(
811 self.ui.config('bugzilla', 'fixregexp',
812 self.ui.config('bugzilla', 'fixregexp',
812 bugzilla._default_fix_re), re.IGNORECASE)
813 bugzilla._default_fix_re), re.IGNORECASE)
813 self.split_re = re.compile(r'\D+')
814 self.split_re = re.compile(r'\D+')
814
815
815 def find_bugs(self, ctx):
816 def find_bugs(self, ctx):
816 '''return bugs dictionary created from commit comment.
817 '''return bugs dictionary created from commit comment.
817
818
818 Extract bug info from changeset comments. Filter out any that are
819 Extract bug info from changeset comments. Filter out any that are
819 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
820 the given changeset in their comments.
821 the given changeset in their comments.
821 '''
822 '''
822 start = 0
823 start = 0
823 hours = 0.0
824 hours = 0.0
824 bugs = {}
825 bugs = {}
825 bugmatch = self.bug_re.search(ctx.description(), start)
826 bugmatch = self.bug_re.search(ctx.description(), start)
826 fixmatch = self.fix_re.search(ctx.description(), start)
827 fixmatch = self.fix_re.search(ctx.description(), start)
827 while True:
828 while True:
828 bugattribs = {}
829 bugattribs = {}
829 if not bugmatch and not fixmatch:
830 if not bugmatch and not fixmatch:
830 break
831 break
831 if not bugmatch:
832 if not bugmatch:
832 m = fixmatch
833 m = fixmatch
833 elif not fixmatch:
834 elif not fixmatch:
834 m = bugmatch
835 m = bugmatch
835 else:
836 else:
836 if bugmatch.start() < fixmatch.start():
837 if bugmatch.start() < fixmatch.start():
837 m = bugmatch
838 m = bugmatch
838 else:
839 else:
839 m = fixmatch
840 m = fixmatch
840 start = m.end()
841 start = m.end()
841 if m is bugmatch:
842 if m is bugmatch:
842 bugmatch = self.bug_re.search(ctx.description(), start)
843 bugmatch = self.bug_re.search(ctx.description(), start)
843 if 'fix' in bugattribs:
844 if 'fix' in bugattribs:
844 del bugattribs['fix']
845 del bugattribs['fix']
845 else:
846 else:
846 fixmatch = self.fix_re.search(ctx.description(), start)
847 fixmatch = self.fix_re.search(ctx.description(), start)
847 bugattribs['fix'] = None
848 bugattribs['fix'] = None
848
849
849 try:
850 try:
850 ids = m.group('ids')
851 ids = m.group('ids')
851 except IndexError:
852 except IndexError:
852 ids = m.group(1)
853 ids = m.group(1)
853 try:
854 try:
854 hours = float(m.group('hours'))
855 hours = float(m.group('hours'))
855 bugattribs['hours'] = hours
856 bugattribs['hours'] = hours
856 except IndexError:
857 except IndexError:
857 pass
858 pass
858 except TypeError:
859 except TypeError:
859 pass
860 pass
860 except ValueError:
861 except ValueError:
861 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
862 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
862
863
863 for id in self.split_re.split(ids):
864 for id in self.split_re.split(ids):
864 if not id:
865 if not id:
865 continue
866 continue
866 bugs[int(id)] = bugattribs
867 bugs[int(id)] = bugattribs
867 if bugs:
868 if bugs:
868 self.bzdriver.filter_real_bug_ids(bugs)
869 self.bzdriver.filter_real_bug_ids(bugs)
869 if bugs:
870 if bugs:
870 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
871 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
871 return bugs
872 return bugs
872
873
873 def update(self, bugid, newstate, ctx):
874 def update(self, bugid, newstate, ctx):
874 '''update bugzilla bug with reference to changeset.'''
875 '''update bugzilla bug with reference to changeset.'''
875
876
876 def webroot(root):
877 def webroot(root):
877 '''strip leading prefix of repo root and turn into
878 '''strip leading prefix of repo root and turn into
878 url-safe path.'''
879 url-safe path.'''
879 count = int(self.ui.config('bugzilla', 'strip', 0))
880 count = int(self.ui.config('bugzilla', 'strip', 0))
880 root = util.pconvert(root)
881 root = util.pconvert(root)
881 while count > 0:
882 while count > 0:
882 c = root.find('/')
883 c = root.find('/')
883 if c == -1:
884 if c == -1:
884 break
885 break
885 root = root[c + 1:]
886 root = root[c + 1:]
886 count -= 1
887 count -= 1
887 return root
888 return root
888
889
889 mapfile = None
890 mapfile = None
890 tmpl = self.ui.config('bugzilla', 'template')
891 tmpl = self.ui.config('bugzilla', 'template')
891 if not tmpl:
892 if not tmpl:
892 mapfile = self.ui.config('bugzilla', 'style')
893 mapfile = self.ui.config('bugzilla', 'style')
893 if not mapfile and not tmpl:
894 if not mapfile and not tmpl:
894 tmpl = _('changeset {node|short} in repo {root} refers '
895 tmpl = _('changeset {node|short} in repo {root} refers '
895 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
896 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
896 t = cmdutil.changeset_templater(self.ui, self.repo,
897 t = cmdutil.changeset_templater(self.ui, self.repo,
897 False, None, tmpl, mapfile, False)
898 False, None, tmpl, mapfile, False)
898 self.ui.pushbuffer()
899 self.ui.pushbuffer()
899 t.show(ctx, changes=ctx.changeset(),
900 t.show(ctx, changes=ctx.changeset(),
900 bug=str(bugid),
901 bug=str(bugid),
901 hgweb=self.ui.config('web', 'baseurl'),
902 hgweb=self.ui.config('web', 'baseurl'),
902 root=self.repo.root,
903 root=self.repo.root,
903 webroot=webroot(self.repo.root))
904 webroot=webroot(self.repo.root))
904 data = self.ui.popbuffer()
905 data = self.ui.popbuffer()
905 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
906 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
906
907
907 def notify(self, bugs, committer):
908 def notify(self, bugs, committer):
908 '''ensure Bugzilla users are notified of bug change.'''
909 '''ensure Bugzilla users are notified of bug change.'''
909 self.bzdriver.notify(bugs, committer)
910 self.bzdriver.notify(bugs, committer)
910
911
911 def hook(ui, repo, hooktype, node=None, **kwargs):
912 def hook(ui, repo, hooktype, node=None, **kwargs):
912 '''add comment to bugzilla for each changeset that refers to a
913 '''add comment to bugzilla for each changeset that refers to a
913 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
914 seen multiple times does not fill bug with duplicate data.'''
915 seen multiple times does not fill bug with duplicate data.'''
915 if node is None:
916 if node is None:
916 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') %
917 hooktype)
918 hooktype)
918 try:
919 try:
919 bz = bugzilla(ui, repo)
920 bz = bugzilla(ui, repo)
920 ctx = repo[node]
921 ctx = repo[node]
921 bugs = bz.find_bugs(ctx)
922 bugs = bz.find_bugs(ctx)
922 if bugs:
923 if bugs:
923 for bug in bugs:
924 for bug in bugs:
924 bz.update(bug, bugs[bug], ctx)
925 bz.update(bug, bugs[bug], ctx)
925 bz.notify(bugs, util.email(ctx.user()))
926 bz.notify(bugs, util.email(ctx.user()))
926 except Exception as e:
927 except Exception as e:
927 raise error.Abort(_('Bugzilla error: %s') % e)
928 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,712 +1,643
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 SocketServer
44 import errno
43 import errno
45 import gc
44 import hashlib
46 import inspect
45 import inspect
47 import os
46 import os
48 import random
49 import re
47 import re
48 import signal
50 import struct
49 import struct
51 import sys
50 import sys
52 import threading
53 import time
51 import time
54 import traceback
55
52
56 from mercurial.i18n import _
53 from mercurial.i18n import _
57
54
58 from mercurial import (
55 from mercurial import (
59 cmdutil,
56 cmdutil,
60 commands,
57 commands,
61 commandserver,
58 commandserver,
62 dispatch,
59 dispatch,
63 error,
60 error,
64 extensions,
61 extensions,
65 osutil,
62 osutil,
66 util,
63 util,
67 )
64 )
68
65
69 # Note for extension authors: ONLY specify testedwith = 'internal' for
66 # Note for extension authors: ONLY specify testedwith = 'internal' for
70 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
67 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
71 # 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
72 # leave the attribute unspecified.
69 # leave the attribute unspecified.
73 testedwith = 'internal'
70 testedwith = 'internal'
74
71
75 _log = commandserver.log
72 _log = commandserver.log
76
73
77 def _hashlist(items):
74 def _hashlist(items):
78 """return sha1 hexdigest for a list"""
75 """return sha1 hexdigest for a list"""
79 return util.sha1(str(items)).hexdigest()
76 return hashlib.sha1(str(items)).hexdigest()
80
77
81 # sensitive config sections affecting confighash
78 # sensitive config sections affecting confighash
82 _configsections = [
79 _configsections = [
80 'alias', # affects global state commands.table
83 'extdiff', # uisetup will register new commands
81 'extdiff', # uisetup will register new commands
84 'extensions',
82 'extensions',
85 ]
83 ]
86
84
87 # sensitive environment variables affecting confighash
85 # sensitive environment variables affecting confighash
88 _envre = re.compile(r'''\A(?:
86 _envre = re.compile(r'''\A(?:
89 CHGHG
87 CHGHG
90 |HG.*
88 |HG.*
91 |LANG(?:UAGE)?
89 |LANG(?:UAGE)?
92 |LC_.*
90 |LC_.*
93 |LD_.*
91 |LD_.*
94 |PATH
92 |PATH
95 |PYTHON.*
93 |PYTHON.*
96 |TERM(?:INFO)?
94 |TERM(?:INFO)?
97 |TZ
95 |TZ
98 )\Z''', re.X)
96 )\Z''', re.X)
99
97
100 def _confighash(ui):
98 def _confighash(ui):
101 """return a quick hash for detecting config/env changes
99 """return a quick hash for detecting config/env changes
102
100
103 confighash is the hash of sensitive config items and environment variables.
101 confighash is the hash of sensitive config items and environment variables.
104
102
105 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
106 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
107 server. different from mtimehash, confighash change will not mark the
105 server. different from mtimehash, confighash change will not mark the
108 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
109 same time.
107 same time.
110 """
108 """
111 sectionitems = []
109 sectionitems = []
112 for section in _configsections:
110 for section in _configsections:
113 sectionitems.append(ui.configitems(section))
111 sectionitems.append(ui.configitems(section))
114 sectionhash = _hashlist(sectionitems)
112 sectionhash = _hashlist(sectionitems)
115 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)]
116 envhash = _hashlist(sorted(envitems))
114 envhash = _hashlist(sorted(envitems))
117 return sectionhash[:6] + envhash[:6]
115 return sectionhash[:6] + envhash[:6]
118
116
119 def _getmtimepaths(ui):
117 def _getmtimepaths(ui):
120 """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
121
119
122 The list will include:
120 The list will include:
123 - extensions (will not cover all files for complex extensions)
121 - extensions (will not cover all files for complex extensions)
124 - mercurial/__version__.py
122 - mercurial/__version__.py
125 - python binary
123 - python binary
126 """
124 """
127 modules = [m for n, m in extensions.extensions(ui)]
125 modules = [m for n, m in extensions.extensions(ui)]
128 try:
126 try:
129 from mercurial import __version__
127 from mercurial import __version__
130 modules.append(__version__)
128 modules.append(__version__)
131 except ImportError:
129 except ImportError:
132 pass
130 pass
133 files = [sys.executable]
131 files = [sys.executable]
134 for m in modules:
132 for m in modules:
135 try:
133 try:
136 files.append(inspect.getabsfile(m))
134 files.append(inspect.getabsfile(m))
137 except TypeError:
135 except TypeError:
138 pass
136 pass
139 return sorted(set(files))
137 return sorted(set(files))
140
138
141 def _mtimehash(paths):
139 def _mtimehash(paths):
142 """return a quick hash for detecting file changes
140 """return a quick hash for detecting file changes
143
141
144 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
145 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
146 expensive. therefore it's not 100% reliable for detecting content changes.
144 expensive. therefore it's not 100% reliable for detecting content changes.
147 it's possible to return different hashes for same file contents.
145 it's possible to return different hashes for same file contents.
148 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
149 some carefully crafted situation.
147 some carefully crafted situation.
150
148
151 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
152 considered outdated immediately and should no longer provide service.
150 considered outdated immediately and should no longer provide service.
151
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
154 race conditions). We need to calculate confighash without importing.
153 """
155 """
154 def trystat(path):
156 def trystat(path):
155 try:
157 try:
156 st = os.stat(path)
158 st = os.stat(path)
157 return (st.st_mtime, st.st_size)
159 return (st.st_mtime, st.st_size)
158 except OSError:
160 except OSError:
159 # could be ENOENT, EPERM etc. not fatal in any case
161 # could be ENOENT, EPERM etc. not fatal in any case
160 pass
162 pass
161 return _hashlist(map(trystat, paths))[:12]
163 return _hashlist(map(trystat, paths))[:12]
162
164
163 class hashstate(object):
165 class hashstate(object):
164 """a structure storing confighash, mtimehash, paths used for mtimehash"""
166 """a structure storing confighash, mtimehash, paths used for mtimehash"""
165 def __init__(self, confighash, mtimehash, mtimepaths):
167 def __init__(self, confighash, mtimehash, mtimepaths):
166 self.confighash = confighash
168 self.confighash = confighash
167 self.mtimehash = mtimehash
169 self.mtimehash = mtimehash
168 self.mtimepaths = mtimepaths
170 self.mtimepaths = mtimepaths
169
171
170 @staticmethod
172 @staticmethod
171 def fromui(ui, mtimepaths=None):
173 def fromui(ui, mtimepaths=None):
172 if mtimepaths is None:
174 if mtimepaths is None:
173 mtimepaths = _getmtimepaths(ui)
175 mtimepaths = _getmtimepaths(ui)
174 confighash = _confighash(ui)
176 confighash = _confighash(ui)
175 mtimehash = _mtimehash(mtimepaths)
177 mtimehash = _mtimehash(mtimepaths)
176 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
178 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
177 return hashstate(confighash, mtimehash, mtimepaths)
179 return hashstate(confighash, mtimehash, mtimepaths)
178
180
179 # copied from hgext/pager.py:uisetup()
181 # copied from hgext/pager.py:uisetup()
180 def _setuppagercmd(ui, options, cmd):
182 def _setuppagercmd(ui, options, cmd):
181 if not ui.formatted():
183 if not ui.formatted():
182 return
184 return
183
185
184 p = ui.config("pager", "pager", os.environ.get("PAGER"))
186 p = ui.config("pager", "pager", os.environ.get("PAGER"))
185 usepager = False
187 usepager = False
186 always = util.parsebool(options['pager'])
188 always = util.parsebool(options['pager'])
187 auto = options['pager'] == 'auto'
189 auto = options['pager'] == 'auto'
188
190
189 if not p:
191 if not p:
190 pass
192 pass
191 elif always:
193 elif always:
192 usepager = True
194 usepager = True
193 elif not auto:
195 elif not auto:
194 usepager = False
196 usepager = False
195 else:
197 else:
196 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
198 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
197 attend = ui.configlist('pager', 'attend', attended)
199 attend = ui.configlist('pager', 'attend', attended)
198 ignore = ui.configlist('pager', 'ignore')
200 ignore = ui.configlist('pager', 'ignore')
199 cmds, _ = cmdutil.findcmd(cmd, commands.table)
201 cmds, _ = cmdutil.findcmd(cmd, commands.table)
200
202
201 for cmd in cmds:
203 for cmd in cmds:
202 var = 'attend-%s' % cmd
204 var = 'attend-%s' % cmd
203 if ui.config('pager', var):
205 if ui.config('pager', var):
204 usepager = ui.configbool('pager', var)
206 usepager = ui.configbool('pager', var)
205 break
207 break
206 if (cmd in attend or
208 if (cmd in attend or
207 (cmd not in ignore and not attend)):
209 (cmd not in ignore and not attend)):
208 usepager = True
210 usepager = True
209 break
211 break
210
212
211 if usepager:
213 if usepager:
212 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
214 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
213 ui.setconfig('ui', 'interactive', False, 'pager')
215 ui.setconfig('ui', 'interactive', False, 'pager')
214 return p
216 return p
215
217
216 _envvarre = re.compile(r'\$[a-zA-Z_]+')
217
218 def _clearenvaliases(cmdtable):
219 """Remove stale command aliases referencing env vars; variable expansion
220 is done at dispatch.addaliases()"""
221 for name, tab in cmdtable.items():
222 cmddef = tab[0]
223 if (isinstance(cmddef, dispatch.cmdalias) and
224 not cmddef.definition.startswith('!') and # shell alias
225 _envvarre.search(cmddef.definition)):
226 del cmdtable[name]
227
228 def _newchgui(srcui, csystem):
218 def _newchgui(srcui, csystem):
229 class chgui(srcui.__class__):
219 class chgui(srcui.__class__):
230 def __init__(self, src=None):
220 def __init__(self, src=None):
231 super(chgui, self).__init__(src)
221 super(chgui, self).__init__(src)
232 if src:
222 if src:
233 self._csystem = getattr(src, '_csystem', csystem)
223 self._csystem = getattr(src, '_csystem', csystem)
234 else:
224 else:
235 self._csystem = csystem
225 self._csystem = csystem
236
226
237 def system(self, cmd, environ=None, cwd=None, onerr=None,
227 def system(self, cmd, environ=None, cwd=None, onerr=None,
238 errprefix=None):
228 errprefix=None):
239 # 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
240 # captured (to self._buffers), or the output stream is not stdout
230 # captured (to self._buffers), or the output stream is not stdout
241 # (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
242 # these situations and will behave differently (write to stdout).
232 # these situations and will behave differently (write to stdout).
243 if (any(s[1] for s in self._bufferstates)
233 if (any(s[1] for s in self._bufferstates)
244 or not util.safehasattr(self.fout, 'fileno')
234 or not util.safehasattr(self.fout, 'fileno')
245 or self.fout.fileno() != sys.stdout.fileno()):
235 or self.fout.fileno() != sys.stdout.fileno()):
246 return super(chgui, self).system(cmd, environ, cwd, onerr,
236 return super(chgui, self).system(cmd, environ, cwd, onerr,
247 errprefix)
237 errprefix)
248 # copied from mercurial/util.py:system()
238 # copied from mercurial/util.py:system()
249 self.flush()
239 self.flush()
250 def py2shell(val):
240 def py2shell(val):
251 if val is None or val is False:
241 if val is None or val is False:
252 return '0'
242 return '0'
253 if val is True:
243 if val is True:
254 return '1'
244 return '1'
255 return str(val)
245 return str(val)
256 env = os.environ.copy()
246 env = os.environ.copy()
257 if environ:
247 if environ:
258 env.update((k, py2shell(v)) for k, v in environ.iteritems())
248 env.update((k, py2shell(v)) for k, v in environ.iteritems())
259 env['HG'] = util.hgexecutable()
249 env['HG'] = util.hgexecutable()
260 rc = self._csystem(cmd, env, cwd)
250 rc = self._csystem(cmd, env, cwd)
261 if rc and onerr:
251 if rc and onerr:
262 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
252 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
263 util.explainexit(rc)[0])
253 util.explainexit(rc)[0])
264 if errprefix:
254 if errprefix:
265 errmsg = '%s: %s' % (errprefix, errmsg)
255 errmsg = '%s: %s' % (errprefix, errmsg)
266 raise onerr(errmsg)
256 raise onerr(errmsg)
267 return rc
257 return rc
268
258
269 return chgui(srcui)
259 return chgui(srcui)
270
260
271 def _loadnewui(srcui, args):
261 def _loadnewui(srcui, args):
272 newui = srcui.__class__()
262 newui = srcui.__class__()
273 for a in ['fin', 'fout', 'ferr', 'environ']:
263 for a in ['fin', 'fout', 'ferr', 'environ']:
274 setattr(newui, a, getattr(srcui, a))
264 setattr(newui, a, getattr(srcui, a))
275 if util.safehasattr(srcui, '_csystem'):
265 if util.safehasattr(srcui, '_csystem'):
276 newui._csystem = srcui._csystem
266 newui._csystem = srcui._csystem
277
267
278 # internal config: extensions.chgserver
268 # internal config: extensions.chgserver
279 newui.setconfig('extensions', 'chgserver',
269 newui.setconfig('extensions', 'chgserver',
280 srcui.config('extensions', 'chgserver'), '--config')
270 srcui.config('extensions', 'chgserver'), '--config')
281
271
282 # command line args
272 # command line args
283 args = args[:]
273 args = args[:]
284 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
274 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
285
275
286 # stolen from tortoisehg.util.copydynamicconfig()
276 # stolen from tortoisehg.util.copydynamicconfig()
287 for section, name, value in srcui.walkconfig():
277 for section, name, value in srcui.walkconfig():
288 source = srcui.configsource(section, name)
278 source = srcui.configsource(section, name)
289 if ':' in source or source == '--config':
279 if ':' in source or source == '--config':
290 # path:line or command line
280 # path:line or command line
291 continue
281 continue
292 if source == 'none':
282 if source == 'none':
293 # ui.configsource returns 'none' by default
283 # ui.configsource returns 'none' by default
294 source = ''
284 source = ''
295 newui.setconfig(section, name, value, source)
285 newui.setconfig(section, name, value, source)
296
286
297 # load wd and repo config, copied from dispatch.py
287 # load wd and repo config, copied from dispatch.py
298 cwds = dispatch._earlygetopt(['--cwd'], args)
288 cwds = dispatch._earlygetopt(['--cwd'], args)
299 cwd = cwds and os.path.realpath(cwds[-1]) or None
289 cwd = cwds and os.path.realpath(cwds[-1]) or None
300 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
290 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
301 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
291 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
302
292
303 return (newui, newlui)
293 return (newui, newlui)
304
294
305 class channeledsystem(object):
295 class channeledsystem(object):
306 """Propagate ui.system() request in the following format:
296 """Propagate ui.system() request in the following format:
307
297
308 payload length (unsigned int),
298 payload length (unsigned int),
309 cmd, '\0',
299 cmd, '\0',
310 cwd, '\0',
300 cwd, '\0',
311 envkey, '=', val, '\0',
301 envkey, '=', val, '\0',
312 ...
302 ...
313 envkey, '=', val
303 envkey, '=', val
314
304
315 and waits:
305 and waits:
316
306
317 exitcode length (unsigned int),
307 exitcode length (unsigned int),
318 exitcode (int)
308 exitcode (int)
319 """
309 """
320 def __init__(self, in_, out, channel):
310 def __init__(self, in_, out, channel):
321 self.in_ = in_
311 self.in_ = in_
322 self.out = out
312 self.out = out
323 self.channel = channel
313 self.channel = channel
324
314
325 def __call__(self, cmd, environ, cwd):
315 def __call__(self, cmd, environ, cwd):
326 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
316 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
327 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())
328 data = '\0'.join(args)
318 data = '\0'.join(args)
329 self.out.write(struct.pack('>cI', self.channel, len(data)))
319 self.out.write(struct.pack('>cI', self.channel, len(data)))
330 self.out.write(data)
320 self.out.write(data)
331 self.out.flush()
321 self.out.flush()
332
322
333 length = self.in_.read(4)
323 length = self.in_.read(4)
334 length, = struct.unpack('>I', length)
324 length, = struct.unpack('>I', length)
335 if length != 4:
325 if length != 4:
336 raise error.Abort(_('invalid response'))
326 raise error.Abort(_('invalid response'))
337 rc, = struct.unpack('>i', self.in_.read(4))
327 rc, = struct.unpack('>i', self.in_.read(4))
338 return rc
328 return rc
339
329
340 _iochannels = [
330 _iochannels = [
341 # server.ch, ui.fp, mode
331 # server.ch, ui.fp, mode
342 ('cin', 'fin', 'rb'),
332 ('cin', 'fin', 'rb'),
343 ('cout', 'fout', 'wb'),
333 ('cout', 'fout', 'wb'),
344 ('cerr', 'ferr', 'wb'),
334 ('cerr', 'ferr', 'wb'),
345 ]
335 ]
346
336
347 class chgcmdserver(commandserver.server):
337 class chgcmdserver(commandserver.server):
348 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
338 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
349 super(chgcmdserver, self).__init__(
339 super(chgcmdserver, self).__init__(
350 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
340 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
351 self.clientsock = sock
341 self.clientsock = sock
352 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
342 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
353 self.hashstate = hashstate
343 self.hashstate = hashstate
354 self.baseaddress = baseaddress
344 self.baseaddress = baseaddress
355 if hashstate is not None:
345 if hashstate is not None:
356 self.capabilities = self.capabilities.copy()
346 self.capabilities = self.capabilities.copy()
357 self.capabilities['validate'] = chgcmdserver.validate
347 self.capabilities['validate'] = chgcmdserver.validate
358
348
359 def cleanup(self):
349 def cleanup(self):
350 super(chgcmdserver, self).cleanup()
360 # dispatch._runcatch() does not flush outputs if exception is not
351 # dispatch._runcatch() does not flush outputs if exception is not
361 # handled by dispatch._dispatch()
352 # handled by dispatch._dispatch()
362 self.ui.flush()
353 self.ui.flush()
363 self._restoreio()
354 self._restoreio()
364
355
365 def attachio(self):
356 def attachio(self):
366 """Attach to client's stdio passed via unix domain socket; all
357 """Attach to client's stdio passed via unix domain socket; all
367 channels except cresult will no longer be used
358 channels except cresult will no longer be used
368 """
359 """
369 # tell client to sendmsg() with 1-byte payload, which makes it
360 # tell client to sendmsg() with 1-byte payload, which makes it
370 # distinctive from "attachio\n" command consumed by client.read()
361 # distinctive from "attachio\n" command consumed by client.read()
371 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
362 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
372 clientfds = osutil.recvfds(self.clientsock.fileno())
363 clientfds = osutil.recvfds(self.clientsock.fileno())
373 _log('received fds: %r\n' % clientfds)
364 _log('received fds: %r\n' % clientfds)
374
365
375 ui = self.ui
366 ui = self.ui
376 ui.flush()
367 ui.flush()
377 first = self._saveio()
368 first = self._saveio()
378 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
369 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
379 assert fd > 0
370 assert fd > 0
380 fp = getattr(ui, fn)
371 fp = getattr(ui, fn)
381 os.dup2(fd, fp.fileno())
372 os.dup2(fd, fp.fileno())
382 os.close(fd)
373 os.close(fd)
383 if not first:
374 if not first:
384 continue
375 continue
385 # reset buffering mode when client is first attached. as we want
376 # reset buffering mode when client is first attached. as we want
386 # to see output immediately on pager, the mode stays unchanged
377 # to see output immediately on pager, the mode stays unchanged
387 # when client re-attached. ferr is unchanged because it should
378 # when client re-attached. ferr is unchanged because it should
388 # be unbuffered no matter if it is a tty or not.
379 # be unbuffered no matter if it is a tty or not.
389 if fn == 'ferr':
380 if fn == 'ferr':
390 newfp = fp
381 newfp = fp
391 else:
382 else:
392 # make it line buffered explicitly because the default is
383 # make it line buffered explicitly because the default is
393 # decided on first write(), where fout could be a pager.
384 # decided on first write(), where fout could be a pager.
394 if fp.isatty():
385 if fp.isatty():
395 bufsize = 1 # line buffered
386 bufsize = 1 # line buffered
396 else:
387 else:
397 bufsize = -1 # system default
388 bufsize = -1 # system default
398 newfp = os.fdopen(fp.fileno(), mode, bufsize)
389 newfp = os.fdopen(fp.fileno(), mode, bufsize)
399 setattr(ui, fn, newfp)
390 setattr(ui, fn, newfp)
400 setattr(self, cn, newfp)
391 setattr(self, cn, newfp)
401
392
402 self.cresult.write(struct.pack('>i', len(clientfds)))
393 self.cresult.write(struct.pack('>i', len(clientfds)))
403
394
404 def _saveio(self):
395 def _saveio(self):
405 if self._oldios:
396 if self._oldios:
406 return False
397 return False
407 ui = self.ui
398 ui = self.ui
408 for cn, fn, _mode in _iochannels:
399 for cn, fn, _mode in _iochannels:
409 ch = getattr(self, cn)
400 ch = getattr(self, cn)
410 fp = getattr(ui, fn)
401 fp = getattr(ui, fn)
411 fd = os.dup(fp.fileno())
402 fd = os.dup(fp.fileno())
412 self._oldios.append((ch, fp, fd))
403 self._oldios.append((ch, fp, fd))
413 return True
404 return True
414
405
415 def _restoreio(self):
406 def _restoreio(self):
416 ui = self.ui
407 ui = self.ui
417 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):
418 newfp = getattr(ui, fn)
409 newfp = getattr(ui, fn)
419 # close newfp while it's associated with client; otherwise it
410 # close newfp while it's associated with client; otherwise it
420 # would be closed when newfp is deleted
411 # would be closed when newfp is deleted
421 if newfp is not fp:
412 if newfp is not fp:
422 newfp.close()
413 newfp.close()
423 # restore original fd: fp is open again
414 # restore original fd: fp is open again
424 os.dup2(fd, fp.fileno())
415 os.dup2(fd, fp.fileno())
425 os.close(fd)
416 os.close(fd)
426 setattr(self, cn, ch)
417 setattr(self, cn, ch)
427 setattr(ui, fn, fp)
418 setattr(ui, fn, fp)
428 del self._oldios[:]
419 del self._oldios[:]
429
420
430 def validate(self):
421 def validate(self):
431 """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
432
423
433 Read a list of '\0' separated arguments.
424 Read a list of '\0' separated arguments.
434 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'
435 if the list is empty.
426 if the list is empty.
436 An instruction string could be either:
427 An instruction string could be either:
437 - "unlink $path", the client should unlink the path to stop the
428 - "unlink $path", the client should unlink the path to stop the
438 outdated server.
429 outdated server.
439 - "redirect $path", the client should attempt to connect to $path
430 - "redirect $path", the client should attempt to connect to $path
440 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
441 "reconnect".
432 "reconnect".
442 - "exit $n", the client should exit directly with code n.
433 - "exit $n", the client should exit directly with code n.
443 This may happen if we cannot parse the config.
434 This may happen if we cannot parse the config.
444 - "reconnect", the client should close the connection and
435 - "reconnect", the client should close the connection and
445 reconnect.
436 reconnect.
446 If neither "reconnect" nor "redirect" is included in the instruction
437 If neither "reconnect" nor "redirect" is included in the instruction
447 list, the client can continue with this server after completing all
438 list, the client can continue with this server after completing all
448 the instructions.
439 the instructions.
449 """
440 """
450 args = self._readlist()
441 args = self._readlist()
451 try:
442 try:
452 self.ui, lui = _loadnewui(self.ui, args)
443 self.ui, lui = _loadnewui(self.ui, args)
453 except error.ParseError as inst:
444 except error.ParseError as inst:
454 dispatch._formatparse(self.ui.warn, inst)
445 dispatch._formatparse(self.ui.warn, inst)
455 self.ui.flush()
446 self.ui.flush()
456 self.cresult.write('exit 255')
447 self.cresult.write('exit 255')
457 return
448 return
458 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
449 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
459 insts = []
450 insts = []
460 if newhash.mtimehash != self.hashstate.mtimehash:
451 if newhash.mtimehash != self.hashstate.mtimehash:
461 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
452 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
462 insts.append('unlink %s' % addr)
453 insts.append('unlink %s' % addr)
463 # mtimehash is empty if one or more extensions fail to load.
454 # mtimehash is empty if one or more extensions fail to load.
464 # to be compatible with hg, still serve the client this time.
455 # to be compatible with hg, still serve the client this time.
465 if self.hashstate.mtimehash:
456 if self.hashstate.mtimehash:
466 insts.append('reconnect')
457 insts.append('reconnect')
467 if newhash.confighash != self.hashstate.confighash:
458 if newhash.confighash != self.hashstate.confighash:
468 addr = _hashaddress(self.baseaddress, newhash.confighash)
459 addr = _hashaddress(self.baseaddress, newhash.confighash)
469 insts.append('redirect %s' % addr)
460 insts.append('redirect %s' % addr)
470 _log('validate: %s\n' % insts)
461 _log('validate: %s\n' % insts)
471 self.cresult.write('\0'.join(insts) or '\0')
462 self.cresult.write('\0'.join(insts) or '\0')
472
463
473 def chdir(self):
464 def chdir(self):
474 """Change current directory
465 """Change current directory
475
466
476 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.
477 It does not affect --config parameter.
468 It does not affect --config parameter.
478 """
469 """
479 path = self._readstr()
470 path = self._readstr()
480 if not path:
471 if not path:
481 return
472 return
482 _log('chdir to %r\n' % path)
473 _log('chdir to %r\n' % path)
483 os.chdir(path)
474 os.chdir(path)
484
475
485 def setumask(self):
476 def setumask(self):
486 """Change umask"""
477 """Change umask"""
487 mask = struct.unpack('>I', self._read(4))[0]
478 mask = struct.unpack('>I', self._read(4))[0]
488 _log('setumask %r\n' % mask)
479 _log('setumask %r\n' % mask)
489 os.umask(mask)
480 os.umask(mask)
490
481
491 def getpager(self):
482 def getpager(self):
492 """Read cmdargs and write pager command to r-channel if enabled
483 """Read cmdargs and write pager command to r-channel if enabled
493
484
494 If pager isn't enabled, this writes '\0' because channeledoutput
485 If pager isn't enabled, this writes '\0' because channeledoutput
495 does not allow to write empty data.
486 does not allow to write empty data.
496 """
487 """
497 args = self._readlist()
488 args = self._readlist()
498 try:
489 try:
499 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
490 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
500 args)
491 args)
501 except (error.Abort, error.AmbiguousCommand, error.CommandError,
492 except (error.Abort, error.AmbiguousCommand, error.CommandError,
502 error.UnknownCommand):
493 error.UnknownCommand):
503 cmd = None
494 cmd = None
504 options = {}
495 options = {}
505 if not cmd or 'pager' not in options:
496 if not cmd or 'pager' not in options:
506 self.cresult.write('\0')
497 self.cresult.write('\0')
507 return
498 return
508
499
509 pagercmd = _setuppagercmd(self.ui, options, cmd)
500 pagercmd = _setuppagercmd(self.ui, options, cmd)
510 if pagercmd:
501 if pagercmd:
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
504 if util.safehasattr(signal, 'SIGPIPE') and \
505 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
506 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
511 self.cresult.write(pagercmd)
507 self.cresult.write(pagercmd)
512 else:
508 else:
513 self.cresult.write('\0')
509 self.cresult.write('\0')
514
510
515 def setenv(self):
511 def setenv(self):
516 """Clear and update os.environ
512 """Clear and update os.environ
517
513
518 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.
519 """
515 """
520 l = self._readlist()
516 l = self._readlist()
521 try:
517 try:
522 newenv = dict(s.split('=', 1) for s in l)
518 newenv = dict(s.split('=', 1) for s in l)
523 except ValueError:
519 except ValueError:
524 raise ValueError('unexpected value in setenv request')
520 raise ValueError('unexpected value in setenv request')
525 _log('setenv: %r\n' % sorted(newenv.keys()))
521 _log('setenv: %r\n' % sorted(newenv.keys()))
526 os.environ.clear()
522 os.environ.clear()
527 os.environ.update(newenv)
523 os.environ.update(newenv)
528 _clearenvaliases(commands.table)
529
524
530 capabilities = commandserver.server.capabilities.copy()
525 capabilities = commandserver.server.capabilities.copy()
531 capabilities.update({'attachio': attachio,
526 capabilities.update({'attachio': attachio,
532 'chdir': chdir,
527 'chdir': chdir,
533 'getpager': getpager,
528 'getpager': getpager,
534 'setenv': setenv,
529 'setenv': setenv,
535 'setumask': setumask})
530 'setumask': setumask})
536
531
537 # copied from mercurial/commandserver.py
538 class _requesthandler(SocketServer.StreamRequestHandler):
539 def handle(self):
540 # use a different process group from the master process, making this
541 # process pass kernel "is_current_pgrp_orphaned" check so signals like
542 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
543 os.setpgid(0, 0)
544 # change random state otherwise forked request handlers would have a
545 # same state inherited from parent.
546 random.seed()
547 ui = self.server.ui
548 repo = self.server.repo
549 sv = None
550 try:
551 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
552 self.server.hashstate, self.server.baseaddress)
553 try:
554 sv.serve()
555 # handle exceptions that may be raised by command server. most of
556 # known exceptions are caught by dispatch.
557 except error.Abort as inst:
558 ui.warn(_('abort: %s\n') % inst)
559 except IOError as inst:
560 if inst.errno != errno.EPIPE:
561 raise
562 except KeyboardInterrupt:
563 pass
564 finally:
565 sv.cleanup()
566 except: # re-raises
567 # also write traceback to error channel. otherwise client cannot
568 # see it because it is written to server's stderr by default.
569 if sv:
570 cerr = sv.cerr
571 else:
572 cerr = commandserver.channeledoutput(self.wfile, 'e')
573 traceback.print_exc(file=cerr)
574 raise
575 finally:
576 # trigger __del__ since ForkingMixIn uses os._exit
577 gc.collect()
578
579 def _tempaddress(address):
532 def _tempaddress(address):
580 return '%s.%d.tmp' % (address, os.getpid())
533 return '%s.%d.tmp' % (address, os.getpid())
581
534
582 def _hashaddress(address, hashstr):
535 def _hashaddress(address, hashstr):
583 return '%s-%s' % (address, hashstr)
536 return '%s-%s' % (address, hashstr)
584
537
585 class AutoExitMixIn: # use old-style to comply with SocketServer design
538 class chgunixservicehandler(object):
586 lastactive = time.time()
539 """Set of operations for chg services"""
587 idletimeout = 3600 # default 1 hour
540
541 pollinterval = 1 # [sec]
588
542
589 def startautoexitthread(self):
543 def __init__(self, ui):
590 # note: the auto-exit check here is cheap enough to not use a thread,
544 self.ui = ui
591 # be done in serve_forever. however SocketServer is hook-unfriendly,
545 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
592 # you simply cannot hook serve_forever without copying a lot of code.
546 self._lastactive = time.time()
593 # besides, serve_forever's docstring suggests using thread.
547
594 thread = threading.Thread(target=self._autoexitloop)
548 def bindsocket(self, sock, address):
595 thread.daemon = True
549 self._inithashstate(address)
596 thread.start()
550 self._checkextensions()
551 self._bind(sock)
552 self._createsymlink()
597
553
598 def _autoexitloop(self, interval=1):
554 def _inithashstate(self, address):
599 while True:
555 self._baseaddress = address
600 time.sleep(interval)
556 if self.ui.configbool('chgserver', 'skiphash', False):
601 if not self.issocketowner():
557 self._hashstate = None
602 _log('%s is not owned, exiting.\n' % self.server_address)
558 self._realaddress = address
603 break
559 return
604 if time.time() - self.lastactive > self.idletimeout:
560 self._hashstate = hashstate.fromui(self.ui)
605 _log('being idle too long. exiting.\n')
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
606 break
607 self.shutdown()
608
562
609 def process_request(self, request, address):
563 def _checkextensions(self):
610 self.lastactive = time.time()
564 if not self._hashstate:
611 return SocketServer.ForkingMixIn.process_request(
565 return
612 self, request, address)
566 if extensions.notloaded():
567 # one or more extensions failed to load. mtimehash becomes
568 # meaningless because we do not know the paths of those extensions.
569 # set mtimehash to an illegal hash value to invalidate the server.
570 self._hashstate.mtimehash = ''
613
571
614 def server_bind(self):
572 def _bind(self, sock):
615 # 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
616 # check later
574 # check later
617 tempaddress = _tempaddress(self.server_address)
575 tempaddress = _tempaddress(self._realaddress)
618 # use relative path instead of full path at bind() if possible, since
576 util.bindunixsocket(sock, tempaddress)
619 # AF_UNIX path has very small length limit (107 chars) on common
577 self._socketstat = os.stat(tempaddress)
620 # platforms (see sys/un.h)
621 dirname, basename = os.path.split(tempaddress)
622 bakwdfd = None
623 if dirname:
624 bakwdfd = os.open('.', os.O_DIRECTORY)
625 os.chdir(dirname)
626 self.socket.bind(basename)
627 self._socketstat = os.stat(basename)
628 # rename will replace the old socket file if exists atomically. the
578 # rename will replace the old socket file if exists atomically. the
629 # old server will detect ownership change and exit.
579 # old server will detect ownership change and exit.
630 util.rename(basename, self.server_address)
580 util.rename(tempaddress, self._realaddress)
631 if bakwdfd:
632 os.fchdir(bakwdfd)
633 os.close(bakwdfd)
634
581
635 def issocketowner(self):
582 def _createsymlink(self):
583 if self._baseaddress == self._realaddress:
584 return
585 tempaddress = _tempaddress(self._baseaddress)
586 os.symlink(os.path.basename(self._realaddress), tempaddress)
587 util.rename(tempaddress, self._baseaddress)
588
589 def _issocketowner(self):
636 try:
590 try:
637 stat = os.stat(self.server_address)
591 stat = os.stat(self._realaddress)
638 return (stat.st_ino == self._socketstat.st_ino and
592 return (stat.st_ino == self._socketstat.st_ino and
639 stat.st_mtime == self._socketstat.st_mtime)
593 stat.st_mtime == self._socketstat.st_mtime)
640 except OSError:
594 except OSError:
641 return False
595 return False
642
596
643 def unlinksocketfile(self):
597 def unlinksocket(self, address):
644 if not self.issocketowner():
598 if not self._issocketowner():
645 return
599 return
646 # 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
647 # remove another server's socket file. but that's okay
601 # remove another server's socket file. but that's okay
648 # since that server will detect and exit automatically and
602 # since that server will detect and exit automatically and
649 # the client will start a new server on demand.
603 # the client will start a new server on demand.
650 try:
604 try:
651 os.unlink(self.server_address)
605 os.unlink(self._realaddress)
652 except OSError as exc:
606 except OSError as exc:
653 if exc.errno != errno.ENOENT:
607 if exc.errno != errno.ENOENT:
654 raise
608 raise
655
609
656 class chgunixservice(commandserver.unixservice):
610 def printbanner(self, address):
657 def init(self):
611 # no "listening at" message should be printed to simulate hg behavior
658 if self.repo:
612 pass
659 # one chgserver can serve multiple repos. drop repo infomation
613
660 self.ui.setconfig('bundle', 'mainreporoot', '', 'repo')
614 def shouldexit(self):
661 self.repo = None
615 if not self._issocketowner():
662 self._inithashstate()
616 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
663 self._checkextensions()
617 return True
664 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
618 if time.time() - self._lastactive > self._idletimeout:
665 SocketServer.UnixStreamServer):
619 self.ui.debug('being idle too long. exiting.\n')
666 ui = self.ui
620 return True
667 repo = self.repo
621 return False
668 hashstate = self.hashstate
669 baseaddress = self.baseaddress
670 self.server = cls(self.address, _requesthandler)
671 self.server.idletimeout = self.ui.configint(
672 'chgserver', 'idletimeout', self.server.idletimeout)
673 self.server.startautoexitthread()
674 self._createsymlink()
675
622
676 def _inithashstate(self):
623 def newconnection(self):
677 self.baseaddress = self.address
624 self._lastactive = time.time()
678 if self.ui.configbool('chgserver', 'skiphash', False):
625
679 self.hashstate = None
626 def createcmdserver(self, repo, conn, fin, fout):
680 return
627 return chgcmdserver(self.ui, repo, fin, fout, conn,
681 self.hashstate = hashstate.fromui(self.ui)
628 self._hashstate, self._baseaddress)
682 self.address = _hashaddress(self.address, self.hashstate.confighash)
683
629
684 def _checkextensions(self):
630 def chgunixservice(ui, repo, opts):
685 if not self.hashstate:
631 if repo:
686 return
632 # one chgserver can serve multiple repos. drop repo infomation
687 if extensions.notloaded():
633 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
688 # one or more extensions failed to load. mtimehash becomes
634 h = chgunixservicehandler(ui)
689 # meaningless because we do not know the paths of those extensions.
635 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
690 # set mtimehash to an illegal hash value to invalidate the server.
691 self.hashstate.mtimehash = ''
692
693 def _createsymlink(self):
694 if self.baseaddress == self.address:
695 return
696 tempaddress = _tempaddress(self.baseaddress)
697 os.symlink(os.path.basename(self.address), tempaddress)
698 util.rename(tempaddress, self.baseaddress)
699
700 def run(self):
701 try:
702 self.server.serve_forever()
703 finally:
704 self.server.unlinksocketfile()
705
636
706 def uisetup(ui):
637 def uisetup(ui):
707 commandserver._servicemap['chgunix'] = chgunixservice
638 commandserver._servicemap['chgunix'] = chgunixservice
708
639
709 # 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
710 # start another chg. drop it to avoid possible side effects.
641 # start another chg. drop it to avoid possible side effects.
711 if 'CHGINTERNALMARK' in os.environ:
642 if 'CHGINTERNALMARK' in os.environ:
712 del os.environ['CHGINTERNALMARK']
643 del os.environ['CHGINTERNALMARK']
@@ -1,664 +1,665
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 Labels
32 Labels
33 ------
33 ------
34
34
35 Text receives color effects depending on the labels that it has. Many
35 Text receives color effects depending on the labels that it has. Many
36 default Mercurial commands emit labelled text. You can also define
36 default Mercurial commands emit labelled text. You can also define
37 your own labels in templates using the label function, see :hg:`help
37 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
38 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
39 that case, effects given to the last label will override any other
40 effects. This includes the special "none" effect, which nullifies
40 effects. This includes the special "none" effect, which nullifies
41 other effects.
41 other effects.
42
42
43 Labels are normally invisible. In order to see these labels and their
43 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
44 position in the text, use the global --color=debug option. The same
45 anchor text may be associated to multiple labels, e.g.
45 anchor text may be associated to multiple labels, e.g.
46
46
47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48
48
49 The following are the default effects for some default labels. Default
49 The following are the default effects for some default labels. Default
50 effects may be overridden from your configuration file::
50 effects may be overridden from your configuration file::
51
51
52 [color]
52 [color]
53 status.modified = blue bold underline red_background
53 status.modified = blue bold underline red_background
54 status.added = green bold
54 status.added = green bold
55 status.removed = red bold blue_background
55 status.removed = red bold blue_background
56 status.deleted = cyan bold underline
56 status.deleted = cyan bold underline
57 status.unknown = magenta bold underline
57 status.unknown = magenta bold underline
58 status.ignored = black bold
58 status.ignored = black bold
59
59
60 # 'none' turns off all effects
60 # 'none' turns off all effects
61 status.clean = none
61 status.clean = none
62 status.copied = none
62 status.copied = none
63
63
64 qseries.applied = blue bold underline
64 qseries.applied = blue bold underline
65 qseries.unapplied = black bold
65 qseries.unapplied = black bold
66 qseries.missing = red bold
66 qseries.missing = red bold
67
67
68 diff.diffline = bold
68 diff.diffline = bold
69 diff.extended = cyan bold
69 diff.extended = cyan bold
70 diff.file_a = red bold
70 diff.file_a = red bold
71 diff.file_b = green bold
71 diff.file_b = green bold
72 diff.hunk = magenta
72 diff.hunk = magenta
73 diff.deleted = red
73 diff.deleted = red
74 diff.inserted = green
74 diff.inserted = green
75 diff.changed = white
75 diff.changed = white
76 diff.tab =
76 diff.tab =
77 diff.trailingwhitespace = bold red_background
77 diff.trailingwhitespace = bold red_background
78
78
79 # Blank so it inherits the style of the surrounding label
79 # Blank so it inherits the style of the surrounding label
80 changeset.public =
80 changeset.public =
81 changeset.draft =
81 changeset.draft =
82 changeset.secret =
82 changeset.secret =
83
83
84 resolve.unresolved = red bold
84 resolve.unresolved = red bold
85 resolve.resolved = green bold
85 resolve.resolved = green bold
86
86
87 bookmarks.active = green
87 bookmarks.active = green
88
88
89 branches.active = none
89 branches.active = none
90 branches.closed = black bold
90 branches.closed = black bold
91 branches.current = green
91 branches.current = green
92 branches.inactive = none
92 branches.inactive = none
93
93
94 tags.normal = green
94 tags.normal = green
95 tags.local = black bold
95 tags.local = black bold
96
96
97 rebase.rebased = blue
97 rebase.rebased = blue
98 rebase.remaining = red bold
98 rebase.remaining = red bold
99
99
100 shelve.age = cyan
100 shelve.age = cyan
101 shelve.newest = green bold
101 shelve.newest = green bold
102 shelve.name = blue bold
102 shelve.name = blue bold
103
103
104 histedit.remaining = red bold
104 histedit.remaining = red bold
105
105
106 Custom colors
106 Custom colors
107 -------------
107 -------------
108
108
109 Because there are only eight standard colors, this module allows you
109 Because there are only eight standard colors, this module allows you
110 to define color names for other color slots which might be available
110 to define color names for other color slots which might be available
111 for your terminal type, assuming terminfo mode. For instance::
111 for your terminal type, assuming terminfo mode. For instance::
112
112
113 color.brightblue = 12
113 color.brightblue = 12
114 color.pink = 207
114 color.pink = 207
115 color.orange = 202
115 color.orange = 202
116
116
117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
117 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
118 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
119 '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,
120 defined colors may then be used as any of the pre-defined eight,
121 including appending '_background' to set the background to that color.
121 including appending '_background' to set the background to that color.
122
122
123 Modes
123 Modes
124 -----
124 -----
125
125
126 By default, the color extension will use ANSI mode (or win32 mode on
126 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
127 Windows) if it detects a terminal. To override auto mode (to enable
128 terminfo mode, for example), set the following configuration option::
128 terminfo mode, for example), set the following configuration option::
129
129
130 [color]
130 [color]
131 mode = terminfo
131 mode = terminfo
132
132
133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 disable color.
134 disable color.
135
135
136 Note that on some systems, terminfo mode may cause problems when using
136 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
137 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
138 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
139 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
140 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
141 pass through all terminal control codes, not just color control
142 codes).
142 codes).
143
143
144 On some systems (such as MSYS in Windows), the terminal may support
144 On some systems (such as MSYS in Windows), the terminal may support
145 a different color mode than the pager (activated via the "pager"
145 a different color mode than the pager (activated via the "pager"
146 extension). It is possible to define separate modes depending on whether
146 extension). It is possible to define separate modes depending on whether
147 the pager is active::
147 the pager is active::
148
148
149 [color]
149 [color]
150 mode = auto
150 mode = auto
151 pagermode = ansi
151 pagermode = ansi
152
152
153 If ``pagermode`` is not defined, the ``mode`` will be used.
153 If ``pagermode`` is not defined, the ``mode`` will be used.
154 '''
154 '''
155
155
156 from __future__ import absolute_import
156 from __future__ import absolute_import
157
157
158 import os
158 import os
159
160 from mercurial.i18n import _
159 from mercurial import (
161 from mercurial import (
160 cmdutil,
162 cmdutil,
161 commands,
163 commands,
162 dispatch,
164 dispatch,
163 extensions,
165 extensions,
164 subrepo,
166 subrepo,
165 ui as uimod,
167 ui as uimod,
166 util,
168 util,
167 )
169 )
168 from mercurial.i18n import _
169
170
170 cmdtable = {}
171 cmdtable = {}
171 command = cmdutil.command(cmdtable)
172 command = cmdutil.command(cmdtable)
172 # Note for extension authors: ONLY specify testedwith = 'internal' for
173 # Note for extension authors: ONLY specify testedwith = 'internal' for
173 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
174 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
174 # be specifying the version(s) of Mercurial they are tested with, or
175 # be specifying the version(s) of Mercurial they are tested with, or
175 # leave the attribute unspecified.
176 # leave the attribute unspecified.
176 testedwith = 'internal'
177 testedwith = 'internal'
177
178
178 # start and stop parameters for effects
179 # start and stop parameters for effects
179 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
180 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
180 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
181 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
181 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
182 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
182 'black_background': 40, 'red_background': 41,
183 'black_background': 40, 'red_background': 41,
183 'green_background': 42, 'yellow_background': 43,
184 'green_background': 42, 'yellow_background': 43,
184 'blue_background': 44, 'purple_background': 45,
185 'blue_background': 44, 'purple_background': 45,
185 'cyan_background': 46, 'white_background': 47}
186 'cyan_background': 46, 'white_background': 47}
186
187
187 def _terminfosetup(ui, mode):
188 def _terminfosetup(ui, mode):
188 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
189 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
189
190
190 global _terminfo_params
191 global _terminfo_params
191 # If we failed to load curses, we go ahead and return.
192 # If we failed to load curses, we go ahead and return.
192 if not _terminfo_params:
193 if not _terminfo_params:
193 return
194 return
194 # Otherwise, see what the config file says.
195 # Otherwise, see what the config file says.
195 if mode not in ('auto', 'terminfo'):
196 if mode not in ('auto', 'terminfo'):
196 return
197 return
197
198
198 _terminfo_params.update((key[6:], (False, int(val)))
199 _terminfo_params.update((key[6:], (False, int(val)))
199 for key, val in ui.configitems('color')
200 for key, val in ui.configitems('color')
200 if key.startswith('color.'))
201 if key.startswith('color.'))
201
202
202 try:
203 try:
203 curses.setupterm()
204 curses.setupterm()
204 except curses.error as e:
205 except curses.error as e:
205 _terminfo_params = {}
206 _terminfo_params = {}
206 return
207 return
207
208
208 for key, (b, e) in _terminfo_params.items():
209 for key, (b, e) in _terminfo_params.items():
209 if not b:
210 if not b:
210 continue
211 continue
211 if not curses.tigetstr(e):
212 if not curses.tigetstr(e):
212 # Most terminals don't support dim, invis, etc, so don't be
213 # Most terminals don't support dim, invis, etc, so don't be
213 # noisy and use ui.debug().
214 # noisy and use ui.debug().
214 ui.debug("no terminfo entry for %s\n" % e)
215 ui.debug("no terminfo entry for %s\n" % e)
215 del _terminfo_params[key]
216 del _terminfo_params[key]
216 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
217 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
217 # Only warn about missing terminfo entries if we explicitly asked for
218 # Only warn about missing terminfo entries if we explicitly asked for
218 # terminfo mode.
219 # terminfo mode.
219 if mode == "terminfo":
220 if mode == "terminfo":
220 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
221 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
221 "ECMA-48 color\n"))
222 "ECMA-48 color\n"))
222 _terminfo_params = {}
223 _terminfo_params = {}
223
224
224 def _modesetup(ui, coloropt):
225 def _modesetup(ui, coloropt):
225 global _terminfo_params
226 global _terminfo_params
226
227
227 if coloropt == 'debug':
228 if coloropt == 'debug':
228 return 'debug'
229 return 'debug'
229
230
230 auto = (coloropt == 'auto')
231 auto = (coloropt == 'auto')
231 always = not auto and util.parsebool(coloropt)
232 always = not auto and util.parsebool(coloropt)
232 if not always and not auto:
233 if not always and not auto:
233 return None
234 return None
234
235
235 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
236 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
236
237
237 mode = ui.config('color', 'mode', 'auto')
238 mode = ui.config('color', 'mode', 'auto')
238
239
239 # If pager is active, color.pagermode overrides color.mode.
240 # If pager is active, color.pagermode overrides color.mode.
240 if getattr(ui, 'pageractive', False):
241 if getattr(ui, 'pageractive', False):
241 mode = ui.config('color', 'pagermode', mode)
242 mode = ui.config('color', 'pagermode', mode)
242
243
243 realmode = mode
244 realmode = mode
244 if mode == 'auto':
245 if mode == 'auto':
245 if os.name == 'nt':
246 if os.name == 'nt':
246 term = os.environ.get('TERM')
247 term = os.environ.get('TERM')
247 # TERM won't be defined in a vanilla cmd.exe environment.
248 # TERM won't be defined in a vanilla cmd.exe environment.
248
249
249 # UNIX-like environments on Windows such as Cygwin and MSYS will
250 # UNIX-like environments on Windows such as Cygwin and MSYS will
250 # set TERM. They appear to make a best effort attempt at setting it
251 # set TERM. They appear to make a best effort attempt at setting it
251 # to something appropriate. However, not all environments with TERM
252 # to something appropriate. However, not all environments with TERM
252 # defined support ANSI. Since "ansi" could result in terminal
253 # defined support ANSI. Since "ansi" could result in terminal
253 # gibberish, we error on the side of selecting "win32". However, if
254 # gibberish, we error on the side of selecting "win32". However, if
254 # w32effects is not defined, we almost certainly don't support
255 # w32effects is not defined, we almost certainly don't support
255 # "win32", so don't even try.
256 # "win32", so don't even try.
256 if (term and 'xterm' in term) or not w32effects:
257 if (term and 'xterm' in term) or not w32effects:
257 realmode = 'ansi'
258 realmode = 'ansi'
258 else:
259 else:
259 realmode = 'win32'
260 realmode = 'win32'
260 else:
261 else:
261 realmode = 'ansi'
262 realmode = 'ansi'
262
263
263 def modewarn():
264 def modewarn():
264 # only warn if color.mode was explicitly set and we're in
265 # only warn if color.mode was explicitly set and we're in
265 # an interactive terminal
266 # an interactive terminal
266 if mode == realmode and ui.interactive():
267 if mode == realmode and ui.interactive():
267 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
268 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
268
269
269 if realmode == 'win32':
270 if realmode == 'win32':
270 _terminfo_params = {}
271 _terminfo_params = {}
271 if not w32effects:
272 if not w32effects:
272 modewarn()
273 modewarn()
273 return None
274 return None
274 _effects.update(w32effects)
275 _effects.update(w32effects)
275 elif realmode == 'ansi':
276 elif realmode == 'ansi':
276 _terminfo_params = {}
277 _terminfo_params = {}
277 elif realmode == 'terminfo':
278 elif realmode == 'terminfo':
278 _terminfosetup(ui, mode)
279 _terminfosetup(ui, mode)
279 if not _terminfo_params:
280 if not _terminfo_params:
280 ## FIXME Shouldn't we return None in this case too?
281 ## FIXME Shouldn't we return None in this case too?
281 modewarn()
282 modewarn()
282 realmode = 'ansi'
283 realmode = 'ansi'
283 else:
284 else:
284 return None
285 return None
285
286
286 if always or (auto and formatted):
287 if always or (auto and formatted):
287 return realmode
288 return realmode
288 return None
289 return None
289
290
290 try:
291 try:
291 import curses
292 import curses
292 # Mapping from effect name to terminfo attribute name or color number.
293 # Mapping from effect name to terminfo attribute name or color number.
293 # This will also force-load the curses module.
294 # This will also force-load the curses module.
294 _terminfo_params = {'none': (True, 'sgr0'),
295 _terminfo_params = {'none': (True, 'sgr0'),
295 'standout': (True, 'smso'),
296 'standout': (True, 'smso'),
296 'underline': (True, 'smul'),
297 'underline': (True, 'smul'),
297 'reverse': (True, 'rev'),
298 'reverse': (True, 'rev'),
298 'inverse': (True, 'rev'),
299 'inverse': (True, 'rev'),
299 'blink': (True, 'blink'),
300 'blink': (True, 'blink'),
300 'dim': (True, 'dim'),
301 'dim': (True, 'dim'),
301 'bold': (True, 'bold'),
302 'bold': (True, 'bold'),
302 'invisible': (True, 'invis'),
303 'invisible': (True, 'invis'),
303 'italic': (True, 'sitm'),
304 'italic': (True, 'sitm'),
304 'black': (False, curses.COLOR_BLACK),
305 'black': (False, curses.COLOR_BLACK),
305 'red': (False, curses.COLOR_RED),
306 'red': (False, curses.COLOR_RED),
306 'green': (False, curses.COLOR_GREEN),
307 'green': (False, curses.COLOR_GREEN),
307 'yellow': (False, curses.COLOR_YELLOW),
308 'yellow': (False, curses.COLOR_YELLOW),
308 'blue': (False, curses.COLOR_BLUE),
309 'blue': (False, curses.COLOR_BLUE),
309 'magenta': (False, curses.COLOR_MAGENTA),
310 'magenta': (False, curses.COLOR_MAGENTA),
310 'cyan': (False, curses.COLOR_CYAN),
311 'cyan': (False, curses.COLOR_CYAN),
311 'white': (False, curses.COLOR_WHITE)}
312 'white': (False, curses.COLOR_WHITE)}
312 except ImportError:
313 except ImportError:
313 _terminfo_params = {}
314 _terminfo_params = {}
314
315
315 _styles = {'grep.match': 'red bold',
316 _styles = {'grep.match': 'red bold',
316 'grep.linenumber': 'green',
317 'grep.linenumber': 'green',
317 'grep.rev': 'green',
318 'grep.rev': 'green',
318 'grep.change': 'green',
319 'grep.change': 'green',
319 'grep.sep': 'cyan',
320 'grep.sep': 'cyan',
320 'grep.filename': 'magenta',
321 'grep.filename': 'magenta',
321 'grep.user': 'magenta',
322 'grep.user': 'magenta',
322 'grep.date': 'magenta',
323 'grep.date': 'magenta',
323 'bookmarks.active': 'green',
324 'bookmarks.active': 'green',
324 'branches.active': 'none',
325 'branches.active': 'none',
325 'branches.closed': 'black bold',
326 'branches.closed': 'black bold',
326 'branches.current': 'green',
327 'branches.current': 'green',
327 'branches.inactive': 'none',
328 'branches.inactive': 'none',
328 'diff.changed': 'white',
329 'diff.changed': 'white',
329 'diff.deleted': 'red',
330 'diff.deleted': 'red',
330 'diff.diffline': 'bold',
331 'diff.diffline': 'bold',
331 'diff.extended': 'cyan bold',
332 'diff.extended': 'cyan bold',
332 'diff.file_a': 'red bold',
333 'diff.file_a': 'red bold',
333 'diff.file_b': 'green bold',
334 'diff.file_b': 'green bold',
334 'diff.hunk': 'magenta',
335 'diff.hunk': 'magenta',
335 'diff.inserted': 'green',
336 'diff.inserted': 'green',
336 'diff.tab': '',
337 'diff.tab': '',
337 'diff.trailingwhitespace': 'bold red_background',
338 'diff.trailingwhitespace': 'bold red_background',
338 'changeset.public' : '',
339 'changeset.public' : '',
339 'changeset.draft' : '',
340 'changeset.draft' : '',
340 'changeset.secret' : '',
341 'changeset.secret' : '',
341 'diffstat.deleted': 'red',
342 'diffstat.deleted': 'red',
342 'diffstat.inserted': 'green',
343 'diffstat.inserted': 'green',
343 'histedit.remaining': 'red bold',
344 'histedit.remaining': 'red bold',
344 'ui.prompt': 'yellow',
345 'ui.prompt': 'yellow',
345 'log.changeset': 'yellow',
346 'log.changeset': 'yellow',
346 'patchbomb.finalsummary': '',
347 'patchbomb.finalsummary': '',
347 'patchbomb.from': 'magenta',
348 'patchbomb.from': 'magenta',
348 'patchbomb.to': 'cyan',
349 'patchbomb.to': 'cyan',
349 'patchbomb.subject': 'green',
350 'patchbomb.subject': 'green',
350 'patchbomb.diffstats': '',
351 'patchbomb.diffstats': '',
351 'rebase.rebased': 'blue',
352 'rebase.rebased': 'blue',
352 'rebase.remaining': 'red bold',
353 'rebase.remaining': 'red bold',
353 'resolve.resolved': 'green bold',
354 'resolve.resolved': 'green bold',
354 'resolve.unresolved': 'red bold',
355 'resolve.unresolved': 'red bold',
355 'shelve.age': 'cyan',
356 'shelve.age': 'cyan',
356 'shelve.newest': 'green bold',
357 'shelve.newest': 'green bold',
357 'shelve.name': 'blue bold',
358 'shelve.name': 'blue bold',
358 'status.added': 'green bold',
359 'status.added': 'green bold',
359 'status.clean': 'none',
360 'status.clean': 'none',
360 'status.copied': 'none',
361 'status.copied': 'none',
361 'status.deleted': 'cyan bold underline',
362 'status.deleted': 'cyan bold underline',
362 'status.ignored': 'black bold',
363 'status.ignored': 'black bold',
363 'status.modified': 'blue bold',
364 'status.modified': 'blue bold',
364 'status.removed': 'red bold',
365 'status.removed': 'red bold',
365 'status.unknown': 'magenta bold underline',
366 'status.unknown': 'magenta bold underline',
366 'tags.normal': 'green',
367 'tags.normal': 'green',
367 'tags.local': 'black bold'}
368 'tags.local': 'black bold'}
368
369
369
370
370 def _effect_str(effect):
371 def _effect_str(effect):
371 '''Helper function for render_effects().'''
372 '''Helper function for render_effects().'''
372
373
373 bg = False
374 bg = False
374 if effect.endswith('_background'):
375 if effect.endswith('_background'):
375 bg = True
376 bg = True
376 effect = effect[:-11]
377 effect = effect[:-11]
377 attr, val = _terminfo_params[effect]
378 attr, val = _terminfo_params[effect]
378 if attr:
379 if attr:
379 return curses.tigetstr(val)
380 return curses.tigetstr(val)
380 elif bg:
381 elif bg:
381 return curses.tparm(curses.tigetstr('setab'), val)
382 return curses.tparm(curses.tigetstr('setab'), val)
382 else:
383 else:
383 return curses.tparm(curses.tigetstr('setaf'), val)
384 return curses.tparm(curses.tigetstr('setaf'), val)
384
385
385 def render_effects(text, effects):
386 def render_effects(text, effects):
386 'Wrap text in commands to turn on each effect.'
387 'Wrap text in commands to turn on each effect.'
387 if not text:
388 if not text:
388 return text
389 return text
389 if not _terminfo_params:
390 if not _terminfo_params:
390 start = [str(_effects[e]) for e in ['none'] + effects.split()]
391 start = [str(_effects[e]) for e in ['none'] + effects.split()]
391 start = '\033[' + ';'.join(start) + 'm'
392 start = '\033[' + ';'.join(start) + 'm'
392 stop = '\033[' + str(_effects['none']) + 'm'
393 stop = '\033[' + str(_effects['none']) + 'm'
393 else:
394 else:
394 start = ''.join(_effect_str(effect)
395 start = ''.join(_effect_str(effect)
395 for effect in ['none'] + effects.split())
396 for effect in ['none'] + effects.split())
396 stop = _effect_str('none')
397 stop = _effect_str('none')
397 return ''.join([start, text, stop])
398 return ''.join([start, text, stop])
398
399
399 def extstyles():
400 def extstyles():
400 for name, ext in extensions.extensions():
401 for name, ext in extensions.extensions():
401 _styles.update(getattr(ext, 'colortable', {}))
402 _styles.update(getattr(ext, 'colortable', {}))
402
403
403 def valideffect(effect):
404 def valideffect(effect):
404 'Determine if the effect is valid or not.'
405 'Determine if the effect is valid or not.'
405 good = False
406 good = False
406 if not _terminfo_params and effect in _effects:
407 if not _terminfo_params and effect in _effects:
407 good = True
408 good = True
408 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
409 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
409 good = True
410 good = True
410 return good
411 return good
411
412
412 def configstyles(ui):
413 def configstyles(ui):
413 for status, cfgeffects in ui.configitems('color'):
414 for status, cfgeffects in ui.configitems('color'):
414 if '.' not in status or status.startswith('color.'):
415 if '.' not in status or status.startswith('color.'):
415 continue
416 continue
416 cfgeffects = ui.configlist('color', status)
417 cfgeffects = ui.configlist('color', status)
417 if cfgeffects:
418 if cfgeffects:
418 good = []
419 good = []
419 for e in cfgeffects:
420 for e in cfgeffects:
420 if valideffect(e):
421 if valideffect(e):
421 good.append(e)
422 good.append(e)
422 else:
423 else:
423 ui.warn(_("ignoring unknown color/effect %r "
424 ui.warn(_("ignoring unknown color/effect %r "
424 "(configured in color.%s)\n")
425 "(configured in color.%s)\n")
425 % (e, status))
426 % (e, status))
426 _styles[status] = ' '.join(good)
427 _styles[status] = ' '.join(good)
427
428
428 class colorui(uimod.ui):
429 class colorui(uimod.ui):
429 _colormode = 'ansi'
430 _colormode = 'ansi'
430 def write(self, *args, **opts):
431 def write(self, *args, **opts):
431 if self._colormode is None:
432 if self._colormode is None:
432 return super(colorui, self).write(*args, **opts)
433 return super(colorui, self).write(*args, **opts)
433
434
434 label = opts.get('label', '')
435 label = opts.get('label', '')
435 if self._buffers and not opts.get('prompt', False):
436 if self._buffers and not opts.get('prompt', False):
436 if self._bufferapplylabels:
437 if self._bufferapplylabels:
437 self._buffers[-1].extend(self.label(a, label) for a in args)
438 self._buffers[-1].extend(self.label(a, label) for a in args)
438 else:
439 else:
439 self._buffers[-1].extend(args)
440 self._buffers[-1].extend(args)
440 elif self._colormode == 'win32':
441 elif self._colormode == 'win32':
441 for a in args:
442 for a in args:
442 win32print(a, super(colorui, self).write, **opts)
443 win32print(a, super(colorui, self).write, **opts)
443 else:
444 else:
444 return super(colorui, self).write(
445 return super(colorui, self).write(
445 *[self.label(a, label) for a in args], **opts)
446 *[self.label(a, label) for a in args], **opts)
446
447
447 def write_err(self, *args, **opts):
448 def write_err(self, *args, **opts):
448 if self._colormode is None:
449 if self._colormode is None:
449 return super(colorui, self).write_err(*args, **opts)
450 return super(colorui, self).write_err(*args, **opts)
450
451
451 label = opts.get('label', '')
452 label = opts.get('label', '')
452 if self._bufferstates and self._bufferstates[-1][0]:
453 if self._bufferstates and self._bufferstates[-1][0]:
453 return self.write(*args, **opts)
454 return self.write(*args, **opts)
454 if self._colormode == 'win32':
455 if self._colormode == 'win32':
455 for a in args:
456 for a in args:
456 win32print(a, super(colorui, self).write_err, **opts)
457 win32print(a, super(colorui, self).write_err, **opts)
457 else:
458 else:
458 return super(colorui, self).write_err(
459 return super(colorui, self).write_err(
459 *[self.label(a, label) for a in args], **opts)
460 *[self.label(a, label) for a in args], **opts)
460
461
461 def showlabel(self, msg, label):
462 def showlabel(self, msg, label):
462 if label and msg:
463 if label and msg:
463 if msg[-1] == '\n':
464 if msg[-1] == '\n':
464 return "[%s|%s]\n" % (label, msg[:-1])
465 return "[%s|%s]\n" % (label, msg[:-1])
465 else:
466 else:
466 return "[%s|%s]" % (label, msg)
467 return "[%s|%s]" % (label, msg)
467 else:
468 else:
468 return msg
469 return msg
469
470
470 def label(self, msg, label):
471 def label(self, msg, label):
471 if self._colormode is None:
472 if self._colormode is None:
472 return super(colorui, self).label(msg, label)
473 return super(colorui, self).label(msg, label)
473
474
474 if self._colormode == 'debug':
475 if self._colormode == 'debug':
475 return self.showlabel(msg, label)
476 return self.showlabel(msg, label)
476
477
477 effects = []
478 effects = []
478 for l in label.split():
479 for l in label.split():
479 s = _styles.get(l, '')
480 s = _styles.get(l, '')
480 if s:
481 if s:
481 effects.append(s)
482 effects.append(s)
482 elif valideffect(l):
483 elif valideffect(l):
483 effects.append(l)
484 effects.append(l)
484 effects = ' '.join(effects)
485 effects = ' '.join(effects)
485 if effects:
486 if effects:
486 return '\n'.join([render_effects(s, effects)
487 return '\n'.join([render_effects(s, effects)
487 for s in msg.split('\n')])
488 for s in msg.split('\n')])
488 return msg
489 return msg
489
490
490 def uisetup(ui):
491 def uisetup(ui):
491 if ui.plain():
492 if ui.plain():
492 return
493 return
493 if not isinstance(ui, colorui):
494 if not isinstance(ui, colorui):
494 colorui.__bases__ = (ui.__class__,)
495 colorui.__bases__ = (ui.__class__,)
495 ui.__class__ = colorui
496 ui.__class__ = colorui
496 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
497 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
497 mode = _modesetup(ui_, opts['color'])
498 mode = _modesetup(ui_, opts['color'])
498 colorui._colormode = mode
499 colorui._colormode = mode
499 if mode and mode != 'debug':
500 if mode and mode != 'debug':
500 extstyles()
501 extstyles()
501 configstyles(ui_)
502 configstyles(ui_)
502 return orig(ui_, opts, cmd, cmdfunc)
503 return orig(ui_, opts, cmd, cmdfunc)
503 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
504 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
504 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
505 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
505 # insert the argument in the front,
506 # insert the argument in the front,
506 # the end of git diff arguments is used for paths
507 # the end of git diff arguments is used for paths
507 commands.insert(1, '--color')
508 commands.insert(1, '--color')
508 return orig(gitsub, commands, env, stream, cwd)
509 return orig(gitsub, commands, env, stream, cwd)
509 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
510 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
510 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
511 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
511
512
512 def extsetup(ui):
513 def extsetup(ui):
513 commands.globalopts.append(
514 commands.globalopts.append(
514 ('', 'color', 'auto',
515 ('', 'color', 'auto',
515 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
516 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
516 # and should not be translated
517 # and should not be translated
517 _("when to colorize (boolean, always, auto, never, or debug)"),
518 _("when to colorize (boolean, always, auto, never, or debug)"),
518 _('TYPE')))
519 _('TYPE')))
519
520
520 @command('debugcolor', [], 'hg debugcolor')
521 @command('debugcolor', [], 'hg debugcolor')
521 def debugcolor(ui, repo, **opts):
522 def debugcolor(ui, repo, **opts):
522 global _styles
523 global _styles
523 _styles = {}
524 _styles = {}
524 for effect in _effects.keys():
525 for effect in _effects.keys():
525 _styles[effect] = effect
526 _styles[effect] = effect
526 ui.write(('color mode: %s\n') % ui._colormode)
527 ui.write(('color mode: %s\n') % ui._colormode)
527 ui.write(_('available colors:\n'))
528 ui.write(_('available colors:\n'))
528 for label, colors in _styles.items():
529 for label, colors in _styles.items():
529 ui.write(('%s\n') % colors, label=label)
530 ui.write(('%s\n') % colors, label=label)
530
531
531 if os.name != 'nt':
532 if os.name != 'nt':
532 w32effects = None
533 w32effects = None
533 else:
534 else:
534 import ctypes
535 import ctypes
535 import re
536 import re
536
537
537 _kernel32 = ctypes.windll.kernel32
538 _kernel32 = ctypes.windll.kernel32
538
539
539 _WORD = ctypes.c_ushort
540 _WORD = ctypes.c_ushort
540
541
541 _INVALID_HANDLE_VALUE = -1
542 _INVALID_HANDLE_VALUE = -1
542
543
543 class _COORD(ctypes.Structure):
544 class _COORD(ctypes.Structure):
544 _fields_ = [('X', ctypes.c_short),
545 _fields_ = [('X', ctypes.c_short),
545 ('Y', ctypes.c_short)]
546 ('Y', ctypes.c_short)]
546
547
547 class _SMALL_RECT(ctypes.Structure):
548 class _SMALL_RECT(ctypes.Structure):
548 _fields_ = [('Left', ctypes.c_short),
549 _fields_ = [('Left', ctypes.c_short),
549 ('Top', ctypes.c_short),
550 ('Top', ctypes.c_short),
550 ('Right', ctypes.c_short),
551 ('Right', ctypes.c_short),
551 ('Bottom', ctypes.c_short)]
552 ('Bottom', ctypes.c_short)]
552
553
553 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
554 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
554 _fields_ = [('dwSize', _COORD),
555 _fields_ = [('dwSize', _COORD),
555 ('dwCursorPosition', _COORD),
556 ('dwCursorPosition', _COORD),
556 ('wAttributes', _WORD),
557 ('wAttributes', _WORD),
557 ('srWindow', _SMALL_RECT),
558 ('srWindow', _SMALL_RECT),
558 ('dwMaximumWindowSize', _COORD)]
559 ('dwMaximumWindowSize', _COORD)]
559
560
560 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
561 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
561 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
562 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
562
563
563 _FOREGROUND_BLUE = 0x0001
564 _FOREGROUND_BLUE = 0x0001
564 _FOREGROUND_GREEN = 0x0002
565 _FOREGROUND_GREEN = 0x0002
565 _FOREGROUND_RED = 0x0004
566 _FOREGROUND_RED = 0x0004
566 _FOREGROUND_INTENSITY = 0x0008
567 _FOREGROUND_INTENSITY = 0x0008
567
568
568 _BACKGROUND_BLUE = 0x0010
569 _BACKGROUND_BLUE = 0x0010
569 _BACKGROUND_GREEN = 0x0020
570 _BACKGROUND_GREEN = 0x0020
570 _BACKGROUND_RED = 0x0040
571 _BACKGROUND_RED = 0x0040
571 _BACKGROUND_INTENSITY = 0x0080
572 _BACKGROUND_INTENSITY = 0x0080
572
573
573 _COMMON_LVB_REVERSE_VIDEO = 0x4000
574 _COMMON_LVB_REVERSE_VIDEO = 0x4000
574 _COMMON_LVB_UNDERSCORE = 0x8000
575 _COMMON_LVB_UNDERSCORE = 0x8000
575
576
576 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
577 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
577 w32effects = {
578 w32effects = {
578 'none': -1,
579 'none': -1,
579 'black': 0,
580 'black': 0,
580 'red': _FOREGROUND_RED,
581 'red': _FOREGROUND_RED,
581 'green': _FOREGROUND_GREEN,
582 'green': _FOREGROUND_GREEN,
582 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
583 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
583 'blue': _FOREGROUND_BLUE,
584 'blue': _FOREGROUND_BLUE,
584 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
585 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
585 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
586 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
586 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
587 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
587 'bold': _FOREGROUND_INTENSITY,
588 'bold': _FOREGROUND_INTENSITY,
588 'black_background': 0x100, # unused value > 0x0f
589 'black_background': 0x100, # unused value > 0x0f
589 'red_background': _BACKGROUND_RED,
590 'red_background': _BACKGROUND_RED,
590 'green_background': _BACKGROUND_GREEN,
591 'green_background': _BACKGROUND_GREEN,
591 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
592 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
592 'blue_background': _BACKGROUND_BLUE,
593 'blue_background': _BACKGROUND_BLUE,
593 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
594 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
594 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
595 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
595 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
596 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
596 _BACKGROUND_BLUE),
597 _BACKGROUND_BLUE),
597 'bold_background': _BACKGROUND_INTENSITY,
598 'bold_background': _BACKGROUND_INTENSITY,
598 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
599 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
599 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
600 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
600 }
601 }
601
602
602 passthrough = set([_FOREGROUND_INTENSITY,
603 passthrough = set([_FOREGROUND_INTENSITY,
603 _BACKGROUND_INTENSITY,
604 _BACKGROUND_INTENSITY,
604 _COMMON_LVB_UNDERSCORE,
605 _COMMON_LVB_UNDERSCORE,
605 _COMMON_LVB_REVERSE_VIDEO])
606 _COMMON_LVB_REVERSE_VIDEO])
606
607
607 stdout = _kernel32.GetStdHandle(
608 stdout = _kernel32.GetStdHandle(
608 _STD_OUTPUT_HANDLE) # don't close the handle returned
609 _STD_OUTPUT_HANDLE) # don't close the handle returned
609 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
610 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
610 w32effects = None
611 w32effects = None
611 else:
612 else:
612 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
613 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
613 if not _kernel32.GetConsoleScreenBufferInfo(
614 if not _kernel32.GetConsoleScreenBufferInfo(
614 stdout, ctypes.byref(csbi)):
615 stdout, ctypes.byref(csbi)):
615 # stdout may not support GetConsoleScreenBufferInfo()
616 # stdout may not support GetConsoleScreenBufferInfo()
616 # when called from subprocess or redirected
617 # when called from subprocess or redirected
617 w32effects = None
618 w32effects = None
618 else:
619 else:
619 origattr = csbi.wAttributes
620 origattr = csbi.wAttributes
620 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
621 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
621 re.MULTILINE | re.DOTALL)
622 re.MULTILINE | re.DOTALL)
622
623
623 def win32print(text, orig, **opts):
624 def win32print(text, orig, **opts):
624 label = opts.get('label', '')
625 label = opts.get('label', '')
625 attr = origattr
626 attr = origattr
626
627
627 def mapcolor(val, attr):
628 def mapcolor(val, attr):
628 if val == -1:
629 if val == -1:
629 return origattr
630 return origattr
630 elif val in passthrough:
631 elif val in passthrough:
631 return attr | val
632 return attr | val
632 elif val > 0x0f:
633 elif val > 0x0f:
633 return (val & 0x70) | (attr & 0x8f)
634 return (val & 0x70) | (attr & 0x8f)
634 else:
635 else:
635 return (val & 0x07) | (attr & 0xf8)
636 return (val & 0x07) | (attr & 0xf8)
636
637
637 # determine console attributes based on labels
638 # determine console attributes based on labels
638 for l in label.split():
639 for l in label.split():
639 style = _styles.get(l, '')
640 style = _styles.get(l, '')
640 for effect in style.split():
641 for effect in style.split():
641 try:
642 try:
642 attr = mapcolor(w32effects[effect], attr)
643 attr = mapcolor(w32effects[effect], attr)
643 except KeyError:
644 except KeyError:
644 # w32effects could not have certain attributes so we skip
645 # w32effects could not have certain attributes so we skip
645 # them if not found
646 # them if not found
646 pass
647 pass
647 # hack to ensure regexp finds data
648 # hack to ensure regexp finds data
648 if not text.startswith('\033['):
649 if not text.startswith('\033['):
649 text = '\033[m' + text
650 text = '\033[m' + text
650
651
651 # Look for ANSI-like codes embedded in text
652 # Look for ANSI-like codes embedded in text
652 m = re.match(ansire, text)
653 m = re.match(ansire, text)
653
654
654 try:
655 try:
655 while m:
656 while m:
656 for sattr in m.group(1).split(';'):
657 for sattr in m.group(1).split(';'):
657 if sattr:
658 if sattr:
658 attr = mapcolor(int(sattr), attr)
659 attr = mapcolor(int(sattr), attr)
659 _kernel32.SetConsoleTextAttribute(stdout, attr)
660 _kernel32.SetConsoleTextAttribute(stdout, attr)
660 orig(m.group(2), **opts)
661 orig(m.group(2), **opts)
661 m = re.match(ansire, m.group(3))
662 m = re.match(ansire, m.group(3))
662 finally:
663 finally:
663 # Explicitly reset original attributes
664 # Explicitly reset original attributes
664 _kernel32.SetConsoleTextAttribute(stdout, origattr)
665 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,458 +1,458
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 import (
13 from mercurial import (
13 cmdutil,
14 cmdutil,
14 registrar,
15 registrar,
15 )
16 )
16 from mercurial.i18n import _
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 = 'internal' 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 = 'internal'
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,295 +1,296
1 # bzr.py - bzr support for the convert extension
1 # bzr.py - bzr support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
9 # it cannot access 'bar' repositories, but they were never used very much
9 # it cannot access 'bar' repositories, but they were never used very much
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import os
12 import os
13
14 from mercurial.i18n import _
13 from mercurial import (
15 from mercurial import (
14 demandimport,
16 demandimport,
15 error
17 error
16 )
18 )
17 from mercurial.i18n import _
18 from . import common
19 from . import common
19
20
20 # these do not work with demandimport, blacklist
21 # these do not work with demandimport, blacklist
21 demandimport.ignore.extend([
22 demandimport.ignore.extend([
22 'bzrlib.transactions',
23 'bzrlib.transactions',
23 'bzrlib.urlutils',
24 'bzrlib.urlutils',
24 'ElementPath',
25 'ElementPath',
25 ])
26 ])
26
27
27 try:
28 try:
28 # bazaar imports
29 # bazaar imports
29 import bzrlib.bzrdir
30 import bzrlib.bzrdir
30 import bzrlib.errors
31 import bzrlib.errors
31 import bzrlib.revision
32 import bzrlib.revision
32 import bzrlib.revisionspec.RevisionSpec
33 import bzrlib.revisionspec.RevisionSpec
33 bzrdir = bzrlib.bzrdir
34 bzrdir = bzrlib.bzrdir
34 errors = bzrlib.errors
35 errors = bzrlib.errors
35 revision = bzrlib.revision
36 revision = bzrlib.revision
36 revisionspec = bzrlib.revisionspec
37 revisionspec = bzrlib.revisionspec
37 except ImportError:
38 except ImportError:
38 pass
39 pass
39
40
40 supportedkinds = ('file', 'symlink')
41 supportedkinds = ('file', 'symlink')
41
42
42 class bzr_source(common.converter_source):
43 class bzr_source(common.converter_source):
43 """Reads Bazaar repositories by using the Bazaar Python libraries"""
44 """Reads Bazaar repositories by using the Bazaar Python libraries"""
44
45
45 def __init__(self, ui, path, revs=None):
46 def __init__(self, ui, path, revs=None):
46 super(bzr_source, self).__init__(ui, path, revs=revs)
47 super(bzr_source, self).__init__(ui, path, revs=revs)
47
48
48 if not os.path.exists(os.path.join(path, '.bzr')):
49 if not os.path.exists(os.path.join(path, '.bzr')):
49 raise common.NoRepo(_('%s does not look like a Bazaar repository')
50 raise common.NoRepo(_('%s does not look like a Bazaar repository')
50 % path)
51 % path)
51
52
52 try:
53 try:
53 # access bzrlib stuff
54 # access bzrlib stuff
54 bzrdir
55 bzrdir
55 except NameError:
56 except NameError:
56 raise common.NoRepo(_('Bazaar modules could not be loaded'))
57 raise common.NoRepo(_('Bazaar modules could not be loaded'))
57
58
58 path = os.path.abspath(path)
59 path = os.path.abspath(path)
59 self._checkrepotype(path)
60 self._checkrepotype(path)
60 try:
61 try:
61 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
62 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
62 except errors.NoRepositoryPresent:
63 except errors.NoRepositoryPresent:
63 raise common.NoRepo(_('%s does not look like a Bazaar repository')
64 raise common.NoRepo(_('%s does not look like a Bazaar repository')
64 % path)
65 % path)
65 self._parentids = {}
66 self._parentids = {}
66
67
67 def _checkrepotype(self, path):
68 def _checkrepotype(self, path):
68 # Lightweight checkouts detection is informational but probably
69 # Lightweight checkouts detection is informational but probably
69 # fragile at API level. It should not terminate the conversion.
70 # fragile at API level. It should not terminate the conversion.
70 try:
71 try:
71 dir = bzrdir.BzrDir.open_containing(path)[0]
72 dir = bzrdir.BzrDir.open_containing(path)[0]
72 try:
73 try:
73 tree = dir.open_workingtree(recommend_upgrade=False)
74 tree = dir.open_workingtree(recommend_upgrade=False)
74 branch = tree.branch
75 branch = tree.branch
75 except (errors.NoWorkingTree, errors.NotLocalUrl):
76 except (errors.NoWorkingTree, errors.NotLocalUrl):
76 tree = None
77 tree = None
77 branch = dir.open_branch()
78 branch = dir.open_branch()
78 if (tree is not None and tree.bzrdir.root_transport.base !=
79 if (tree is not None and tree.bzrdir.root_transport.base !=
79 branch.bzrdir.root_transport.base):
80 branch.bzrdir.root_transport.base):
80 self.ui.warn(_('warning: lightweight checkouts may cause '
81 self.ui.warn(_('warning: lightweight checkouts may cause '
81 'conversion failures, try with a regular '
82 'conversion failures, try with a regular '
82 'branch instead.\n'))
83 'branch instead.\n'))
83 except Exception:
84 except Exception:
84 self.ui.note(_('bzr source type could not be determined\n'))
85 self.ui.note(_('bzr source type could not be determined\n'))
85
86
86 def before(self):
87 def before(self):
87 """Before the conversion begins, acquire a read lock
88 """Before the conversion begins, acquire a read lock
88 for all the operations that might need it. Fortunately
89 for all the operations that might need it. Fortunately
89 read locks don't block other reads or writes to the
90 read locks don't block other reads or writes to the
90 repository, so this shouldn't have any impact on the usage of
91 repository, so this shouldn't have any impact on the usage of
91 the source repository.
92 the source repository.
92
93
93 The alternative would be locking on every operation that
94 The alternative would be locking on every operation that
94 needs locks (there are currently two: getting the file and
95 needs locks (there are currently two: getting the file and
95 getting the parent map) and releasing immediately after,
96 getting the parent map) and releasing immediately after,
96 but this approach can take even 40% longer."""
97 but this approach can take even 40% longer."""
97 self.sourcerepo.lock_read()
98 self.sourcerepo.lock_read()
98
99
99 def after(self):
100 def after(self):
100 self.sourcerepo.unlock()
101 self.sourcerepo.unlock()
101
102
102 def _bzrbranches(self):
103 def _bzrbranches(self):
103 return self.sourcerepo.find_branches(using=True)
104 return self.sourcerepo.find_branches(using=True)
104
105
105 def getheads(self):
106 def getheads(self):
106 if not self.revs:
107 if not self.revs:
107 # Set using=True to avoid nested repositories (see issue3254)
108 # Set using=True to avoid nested repositories (see issue3254)
108 heads = sorted([b.last_revision() for b in self._bzrbranches()])
109 heads = sorted([b.last_revision() for b in self._bzrbranches()])
109 else:
110 else:
110 revid = None
111 revid = None
111 for branch in self._bzrbranches():
112 for branch in self._bzrbranches():
112 try:
113 try:
113 r = revisionspec.RevisionSpec.from_string(self.revs[0])
114 r = revisionspec.RevisionSpec.from_string(self.revs[0])
114 info = r.in_history(branch)
115 info = r.in_history(branch)
115 except errors.BzrError:
116 except errors.BzrError:
116 pass
117 pass
117 revid = info.rev_id
118 revid = info.rev_id
118 if revid is None:
119 if revid is None:
119 raise error.Abort(_('%s is not a valid revision')
120 raise error.Abort(_('%s is not a valid revision')
120 % self.revs[0])
121 % self.revs[0])
121 heads = [revid]
122 heads = [revid]
122 # Empty repositories return 'null:', which cannot be retrieved
123 # Empty repositories return 'null:', which cannot be retrieved
123 heads = [h for h in heads if h != 'null:']
124 heads = [h for h in heads if h != 'null:']
124 return heads
125 return heads
125
126
126 def getfile(self, name, rev):
127 def getfile(self, name, rev):
127 revtree = self.sourcerepo.revision_tree(rev)
128 revtree = self.sourcerepo.revision_tree(rev)
128 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
129 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
129 kind = None
130 kind = None
130 if fileid is not None:
131 if fileid is not None:
131 kind = revtree.kind(fileid)
132 kind = revtree.kind(fileid)
132 if kind not in supportedkinds:
133 if kind not in supportedkinds:
133 # the file is not available anymore - was deleted
134 # the file is not available anymore - was deleted
134 return None, None
135 return None, None
135 mode = self._modecache[(name, rev)]
136 mode = self._modecache[(name, rev)]
136 if kind == 'symlink':
137 if kind == 'symlink':
137 target = revtree.get_symlink_target(fileid)
138 target = revtree.get_symlink_target(fileid)
138 if target is None:
139 if target is None:
139 raise error.Abort(_('%s.%s symlink has no target')
140 raise error.Abort(_('%s.%s symlink has no target')
140 % (name, rev))
141 % (name, rev))
141 return target, mode
142 return target, mode
142 else:
143 else:
143 sio = revtree.get_file(fileid)
144 sio = revtree.get_file(fileid)
144 return sio.read(), mode
145 return sio.read(), mode
145
146
146 def getchanges(self, version, full):
147 def getchanges(self, version, full):
147 if full:
148 if full:
148 raise error.Abort(_("convert from cvs does not support --full"))
149 raise error.Abort(_("convert from cvs does not support --full"))
149 self._modecache = {}
150 self._modecache = {}
150 self._revtree = self.sourcerepo.revision_tree(version)
151 self._revtree = self.sourcerepo.revision_tree(version)
151 # get the parentids from the cache
152 # get the parentids from the cache
152 parentids = self._parentids.pop(version)
153 parentids = self._parentids.pop(version)
153 # only diff against first parent id
154 # only diff against first parent id
154 prevtree = self.sourcerepo.revision_tree(parentids[0])
155 prevtree = self.sourcerepo.revision_tree(parentids[0])
155 files, changes = self._gettreechanges(self._revtree, prevtree)
156 files, changes = self._gettreechanges(self._revtree, prevtree)
156 return files, changes, set()
157 return files, changes, set()
157
158
158 def getcommit(self, version):
159 def getcommit(self, version):
159 rev = self.sourcerepo.get_revision(version)
160 rev = self.sourcerepo.get_revision(version)
160 # populate parent id cache
161 # populate parent id cache
161 if not rev.parent_ids:
162 if not rev.parent_ids:
162 parents = []
163 parents = []
163 self._parentids[version] = (revision.NULL_REVISION,)
164 self._parentids[version] = (revision.NULL_REVISION,)
164 else:
165 else:
165 parents = self._filterghosts(rev.parent_ids)
166 parents = self._filterghosts(rev.parent_ids)
166 self._parentids[version] = parents
167 self._parentids[version] = parents
167
168
168 branch = self.recode(rev.properties.get('branch-nick', u'default'))
169 branch = self.recode(rev.properties.get('branch-nick', u'default'))
169 if branch == 'trunk':
170 if branch == 'trunk':
170 branch = 'default'
171 branch = 'default'
171 return common.commit(parents=parents,
172 return common.commit(parents=parents,
172 date='%d %d' % (rev.timestamp, -rev.timezone),
173 date='%d %d' % (rev.timestamp, -rev.timezone),
173 author=self.recode(rev.committer),
174 author=self.recode(rev.committer),
174 desc=self.recode(rev.message),
175 desc=self.recode(rev.message),
175 branch=branch,
176 branch=branch,
176 rev=version)
177 rev=version)
177
178
178 def gettags(self):
179 def gettags(self):
179 bytetags = {}
180 bytetags = {}
180 for branch in self._bzrbranches():
181 for branch in self._bzrbranches():
181 if not branch.supports_tags():
182 if not branch.supports_tags():
182 return {}
183 return {}
183 tagdict = branch.tags.get_tag_dict()
184 tagdict = branch.tags.get_tag_dict()
184 for name, rev in tagdict.iteritems():
185 for name, rev in tagdict.iteritems():
185 bytetags[self.recode(name)] = rev
186 bytetags[self.recode(name)] = rev
186 return bytetags
187 return bytetags
187
188
188 def getchangedfiles(self, rev, i):
189 def getchangedfiles(self, rev, i):
189 self._modecache = {}
190 self._modecache = {}
190 curtree = self.sourcerepo.revision_tree(rev)
191 curtree = self.sourcerepo.revision_tree(rev)
191 if i is not None:
192 if i is not None:
192 parentid = self._parentids[rev][i]
193 parentid = self._parentids[rev][i]
193 else:
194 else:
194 # no parent id, get the empty revision
195 # no parent id, get the empty revision
195 parentid = revision.NULL_REVISION
196 parentid = revision.NULL_REVISION
196
197
197 prevtree = self.sourcerepo.revision_tree(parentid)
198 prevtree = self.sourcerepo.revision_tree(parentid)
198 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
199 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
199 return changes
200 return changes
200
201
201 def _gettreechanges(self, current, origin):
202 def _gettreechanges(self, current, origin):
202 revid = current._revision_id
203 revid = current._revision_id
203 changes = []
204 changes = []
204 renames = {}
205 renames = {}
205 seen = set()
206 seen = set()
206 # Process the entries by reverse lexicographic name order to
207 # Process the entries by reverse lexicographic name order to
207 # handle nested renames correctly, most specific first.
208 # handle nested renames correctly, most specific first.
208 curchanges = sorted(current.iter_changes(origin),
209 curchanges = sorted(current.iter_changes(origin),
209 key=lambda c: c[1][0] or c[1][1],
210 key=lambda c: c[1][0] or c[1][1],
210 reverse=True)
211 reverse=True)
211 for (fileid, paths, changed_content, versioned, parent, name,
212 for (fileid, paths, changed_content, versioned, parent, name,
212 kind, executable) in curchanges:
213 kind, executable) in curchanges:
213
214
214 if paths[0] == u'' or paths[1] == u'':
215 if paths[0] == u'' or paths[1] == u'':
215 # ignore changes to tree root
216 # ignore changes to tree root
216 continue
217 continue
217
218
218 # bazaar tracks directories, mercurial does not, so
219 # bazaar tracks directories, mercurial does not, so
219 # we have to rename the directory contents
220 # we have to rename the directory contents
220 if kind[1] == 'directory':
221 if kind[1] == 'directory':
221 if kind[0] not in (None, 'directory'):
222 if kind[0] not in (None, 'directory'):
222 # Replacing 'something' with a directory, record it
223 # Replacing 'something' with a directory, record it
223 # so it can be removed.
224 # so it can be removed.
224 changes.append((self.recode(paths[0]), revid))
225 changes.append((self.recode(paths[0]), revid))
225
226
226 if kind[0] == 'directory' and None not in paths:
227 if kind[0] == 'directory' and None not in paths:
227 renaming = paths[0] != paths[1]
228 renaming = paths[0] != paths[1]
228 # neither an add nor an delete - a move
229 # neither an add nor an delete - a move
229 # rename all directory contents manually
230 # rename all directory contents manually
230 subdir = origin.inventory.path2id(paths[0])
231 subdir = origin.inventory.path2id(paths[0])
231 # get all child-entries of the directory
232 # get all child-entries of the directory
232 for name, entry in origin.inventory.iter_entries(subdir):
233 for name, entry in origin.inventory.iter_entries(subdir):
233 # hg does not track directory renames
234 # hg does not track directory renames
234 if entry.kind == 'directory':
235 if entry.kind == 'directory':
235 continue
236 continue
236 frompath = self.recode(paths[0] + '/' + name)
237 frompath = self.recode(paths[0] + '/' + name)
237 if frompath in seen:
238 if frompath in seen:
238 # Already handled by a more specific change entry
239 # Already handled by a more specific change entry
239 # This is important when you have:
240 # This is important when you have:
240 # a => b
241 # a => b
241 # a/c => a/c
242 # a/c => a/c
242 # Here a/c must not be renamed into b/c
243 # Here a/c must not be renamed into b/c
243 continue
244 continue
244 seen.add(frompath)
245 seen.add(frompath)
245 if not renaming:
246 if not renaming:
246 continue
247 continue
247 topath = self.recode(paths[1] + '/' + name)
248 topath = self.recode(paths[1] + '/' + name)
248 # register the files as changed
249 # register the files as changed
249 changes.append((frompath, revid))
250 changes.append((frompath, revid))
250 changes.append((topath, revid))
251 changes.append((topath, revid))
251 # add to mode cache
252 # add to mode cache
252 mode = ((entry.executable and 'x')
253 mode = ((entry.executable and 'x')
253 or (entry.kind == 'symlink' and 's')
254 or (entry.kind == 'symlink' and 's')
254 or '')
255 or '')
255 self._modecache[(topath, revid)] = mode
256 self._modecache[(topath, revid)] = mode
256 # register the change as move
257 # register the change as move
257 renames[topath] = frompath
258 renames[topath] = frompath
258
259
259 # no further changes, go to the next change
260 # no further changes, go to the next change
260 continue
261 continue
261
262
262 # we got unicode paths, need to convert them
263 # we got unicode paths, need to convert them
263 path, topath = paths
264 path, topath = paths
264 if path is not None:
265 if path is not None:
265 path = self.recode(path)
266 path = self.recode(path)
266 if topath is not None:
267 if topath is not None:
267 topath = self.recode(topath)
268 topath = self.recode(topath)
268 seen.add(path or topath)
269 seen.add(path or topath)
269
270
270 if topath is None:
271 if topath is None:
271 # file deleted
272 # file deleted
272 changes.append((path, revid))
273 changes.append((path, revid))
273 continue
274 continue
274
275
275 # renamed
276 # renamed
276 if path and path != topath:
277 if path and path != topath:
277 renames[topath] = path
278 renames[topath] = path
278 changes.append((path, revid))
279 changes.append((path, revid))
279
280
280 # populate the mode cache
281 # populate the mode cache
281 kind, executable = [e[1] for e in (kind, executable)]
282 kind, executable = [e[1] for e in (kind, executable)]
282 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
283 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
283 or '')
284 or '')
284 self._modecache[(topath, revid)] = mode
285 self._modecache[(topath, revid)] = mode
285 changes.append((topath, revid))
286 changes.append((topath, revid))
286
287
287 return changes, renames
288 return changes, renames
288
289
289 def _filterghosts(self, ids):
290 def _filterghosts(self, ids):
290 """Filters out ghost revisions which hg does not support, see
291 """Filters out ghost revisions which hg does not support, see
291 <http://bazaar-vcs.org/GhostRevision>
292 <http://bazaar-vcs.org/GhostRevision>
292 """
293 """
293 parentmap = self.sourcerepo.get_parent_map(ids)
294 parentmap = self.sourcerepo.get_parent_map(ids)
294 parents = tuple([parent for parent in ids if parent in parentmap])
295 parents = tuple([parent for parent in ids if parent in parentmap])
295 return parents
296 return parents
@@ -1,494 +1,494
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import base64
9 import base64
10 import cPickle as pickle
11 import datetime
10 import datetime
12 import errno
11 import errno
13 import os
12 import os
14 import re
13 import re
15 import subprocess
14 import subprocess
16
15
16 from mercurial.i18n import _
17 from mercurial import (
17 from mercurial import (
18 error,
18 error,
19 phases,
19 phases,
20 util,
20 util,
21 )
21 )
22 from mercurial.i18n import _
23
22
23 pickle = util.pickle
24 propertycache = util.propertycache
24 propertycache = util.propertycache
25
25
26 def encodeargs(args):
26 def encodeargs(args):
27 def encodearg(s):
27 def encodearg(s):
28 lines = base64.encodestring(s)
28 lines = base64.encodestring(s)
29 lines = [l.splitlines()[0] for l in lines]
29 lines = [l.splitlines()[0] for l in lines]
30 return ''.join(lines)
30 return ''.join(lines)
31
31
32 s = pickle.dumps(args)
32 s = pickle.dumps(args)
33 return encodearg(s)
33 return encodearg(s)
34
34
35 def decodeargs(s):
35 def decodeargs(s):
36 s = base64.decodestring(s)
36 s = base64.decodestring(s)
37 return pickle.loads(s)
37 return pickle.loads(s)
38
38
39 class MissingTool(Exception):
39 class MissingTool(Exception):
40 pass
40 pass
41
41
42 def checktool(exe, name=None, abort=True):
42 def checktool(exe, name=None, abort=True):
43 name = name or exe
43 name = name or exe
44 if not util.findexe(exe):
44 if not util.findexe(exe):
45 if abort:
45 if abort:
46 exc = error.Abort
46 exc = error.Abort
47 else:
47 else:
48 exc = MissingTool
48 exc = MissingTool
49 raise exc(_('cannot find required "%s" tool') % name)
49 raise exc(_('cannot find required "%s" tool') % name)
50
50
51 class NoRepo(Exception):
51 class NoRepo(Exception):
52 pass
52 pass
53
53
54 SKIPREV = 'SKIP'
54 SKIPREV = 'SKIP'
55
55
56 class commit(object):
56 class commit(object):
57 def __init__(self, author, date, desc, parents, branch=None, rev=None,
57 def __init__(self, author, date, desc, parents, branch=None, rev=None,
58 extra={}, sortkey=None, saverev=True, phase=phases.draft,
58 extra={}, sortkey=None, saverev=True, phase=phases.draft,
59 optparents=None):
59 optparents=None):
60 self.author = author or 'unknown'
60 self.author = author or 'unknown'
61 self.date = date or '0 0'
61 self.date = date or '0 0'
62 self.desc = desc
62 self.desc = desc
63 self.parents = parents # will be converted and used as parents
63 self.parents = parents # will be converted and used as parents
64 self.optparents = optparents or [] # will be used if already converted
64 self.optparents = optparents or [] # will be used if already converted
65 self.branch = branch
65 self.branch = branch
66 self.rev = rev
66 self.rev = rev
67 self.extra = extra
67 self.extra = extra
68 self.sortkey = sortkey
68 self.sortkey = sortkey
69 self.saverev = saverev
69 self.saverev = saverev
70 self.phase = phase
70 self.phase = phase
71
71
72 class converter_source(object):
72 class converter_source(object):
73 """Conversion source interface"""
73 """Conversion source interface"""
74
74
75 def __init__(self, ui, path=None, revs=None):
75 def __init__(self, ui, path=None, revs=None):
76 """Initialize conversion source (or raise NoRepo("message")
76 """Initialize conversion source (or raise NoRepo("message")
77 exception if path is not a valid repository)"""
77 exception if path is not a valid repository)"""
78 self.ui = ui
78 self.ui = ui
79 self.path = path
79 self.path = path
80 self.revs = revs
80 self.revs = revs
81
81
82 self.encoding = 'utf-8'
82 self.encoding = 'utf-8'
83
83
84 def checkhexformat(self, revstr, mapname='splicemap'):
84 def checkhexformat(self, revstr, mapname='splicemap'):
85 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
85 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
86 such format for their revision numbering
86 such format for their revision numbering
87 """
87 """
88 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
88 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
89 raise error.Abort(_('%s entry %s is not a valid revision'
89 raise error.Abort(_('%s entry %s is not a valid revision'
90 ' identifier') % (mapname, revstr))
90 ' identifier') % (mapname, revstr))
91
91
92 def before(self):
92 def before(self):
93 pass
93 pass
94
94
95 def after(self):
95 def after(self):
96 pass
96 pass
97
97
98 def targetfilebelongstosource(self, targetfilename):
98 def targetfilebelongstosource(self, targetfilename):
99 """Returns true if the given targetfile belongs to the source repo. This
99 """Returns true if the given targetfile belongs to the source repo. This
100 is useful when only a subdirectory of the target belongs to the source
100 is useful when only a subdirectory of the target belongs to the source
101 repo."""
101 repo."""
102 # For normal full repo converts, this is always True.
102 # For normal full repo converts, this is always True.
103 return True
103 return True
104
104
105 def setrevmap(self, revmap):
105 def setrevmap(self, revmap):
106 """set the map of already-converted revisions"""
106 """set the map of already-converted revisions"""
107 pass
107 pass
108
108
109 def getheads(self):
109 def getheads(self):
110 """Return a list of this repository's heads"""
110 """Return a list of this repository's heads"""
111 raise NotImplementedError
111 raise NotImplementedError
112
112
113 def getfile(self, name, rev):
113 def getfile(self, name, rev):
114 """Return a pair (data, mode) where data is the file content
114 """Return a pair (data, mode) where data is the file content
115 as a string and mode one of '', 'x' or 'l'. rev is the
115 as a string and mode one of '', 'x' or 'l'. rev is the
116 identifier returned by a previous call to getchanges().
116 identifier returned by a previous call to getchanges().
117 Data is None if file is missing/deleted in rev.
117 Data is None if file is missing/deleted in rev.
118 """
118 """
119 raise NotImplementedError
119 raise NotImplementedError
120
120
121 def getchanges(self, version, full):
121 def getchanges(self, version, full):
122 """Returns a tuple of (files, copies, cleanp2).
122 """Returns a tuple of (files, copies, cleanp2).
123
123
124 files is a sorted list of (filename, id) tuples for all files
124 files is a sorted list of (filename, id) tuples for all files
125 changed between version and its first parent returned by
125 changed between version and its first parent returned by
126 getcommit(). If full, all files in that revision is returned.
126 getcommit(). If full, all files in that revision is returned.
127 id is the source revision id of the file.
127 id is the source revision id of the file.
128
128
129 copies is a dictionary of dest: source
129 copies is a dictionary of dest: source
130
130
131 cleanp2 is the set of files filenames that are clean against p2.
131 cleanp2 is the set of files filenames that are clean against p2.
132 (Files that are clean against p1 are already not in files (unless
132 (Files that are clean against p1 are already not in files (unless
133 full). This makes it possible to handle p2 clean files similarly.)
133 full). This makes it possible to handle p2 clean files similarly.)
134 """
134 """
135 raise NotImplementedError
135 raise NotImplementedError
136
136
137 def getcommit(self, version):
137 def getcommit(self, version):
138 """Return the commit object for version"""
138 """Return the commit object for version"""
139 raise NotImplementedError
139 raise NotImplementedError
140
140
141 def numcommits(self):
141 def numcommits(self):
142 """Return the number of commits in this source.
142 """Return the number of commits in this source.
143
143
144 If unknown, return None.
144 If unknown, return None.
145 """
145 """
146 return None
146 return None
147
147
148 def gettags(self):
148 def gettags(self):
149 """Return the tags as a dictionary of name: revision
149 """Return the tags as a dictionary of name: revision
150
150
151 Tag names must be UTF-8 strings.
151 Tag names must be UTF-8 strings.
152 """
152 """
153 raise NotImplementedError
153 raise NotImplementedError
154
154
155 def recode(self, s, encoding=None):
155 def recode(self, s, encoding=None):
156 if not encoding:
156 if not encoding:
157 encoding = self.encoding or 'utf-8'
157 encoding = self.encoding or 'utf-8'
158
158
159 if isinstance(s, unicode):
159 if isinstance(s, unicode):
160 return s.encode("utf-8")
160 return s.encode("utf-8")
161 try:
161 try:
162 return s.decode(encoding).encode("utf-8")
162 return s.decode(encoding).encode("utf-8")
163 except UnicodeError:
163 except UnicodeError:
164 try:
164 try:
165 return s.decode("latin-1").encode("utf-8")
165 return s.decode("latin-1").encode("utf-8")
166 except UnicodeError:
166 except UnicodeError:
167 return s.decode(encoding, "replace").encode("utf-8")
167 return s.decode(encoding, "replace").encode("utf-8")
168
168
169 def getchangedfiles(self, rev, i):
169 def getchangedfiles(self, rev, i):
170 """Return the files changed by rev compared to parent[i].
170 """Return the files changed by rev compared to parent[i].
171
171
172 i is an index selecting one of the parents of rev. The return
172 i is an index selecting one of the parents of rev. The return
173 value should be the list of files that are different in rev and
173 value should be the list of files that are different in rev and
174 this parent.
174 this parent.
175
175
176 If rev has no parents, i is None.
176 If rev has no parents, i is None.
177
177
178 This function is only needed to support --filemap
178 This function is only needed to support --filemap
179 """
179 """
180 raise NotImplementedError
180 raise NotImplementedError
181
181
182 def converted(self, rev, sinkrev):
182 def converted(self, rev, sinkrev):
183 '''Notify the source that a revision has been converted.'''
183 '''Notify the source that a revision has been converted.'''
184 pass
184 pass
185
185
186 def hasnativeorder(self):
186 def hasnativeorder(self):
187 """Return true if this source has a meaningful, native revision
187 """Return true if this source has a meaningful, native revision
188 order. For instance, Mercurial revisions are store sequentially
188 order. For instance, Mercurial revisions are store sequentially
189 while there is no such global ordering with Darcs.
189 while there is no such global ordering with Darcs.
190 """
190 """
191 return False
191 return False
192
192
193 def hasnativeclose(self):
193 def hasnativeclose(self):
194 """Return true if this source has ability to close branch.
194 """Return true if this source has ability to close branch.
195 """
195 """
196 return False
196 return False
197
197
198 def lookuprev(self, rev):
198 def lookuprev(self, rev):
199 """If rev is a meaningful revision reference in source, return
199 """If rev is a meaningful revision reference in source, return
200 the referenced identifier in the same format used by getcommit().
200 the referenced identifier in the same format used by getcommit().
201 return None otherwise.
201 return None otherwise.
202 """
202 """
203 return None
203 return None
204
204
205 def getbookmarks(self):
205 def getbookmarks(self):
206 """Return the bookmarks as a dictionary of name: revision
206 """Return the bookmarks as a dictionary of name: revision
207
207
208 Bookmark names are to be UTF-8 strings.
208 Bookmark names are to be UTF-8 strings.
209 """
209 """
210 return {}
210 return {}
211
211
212 def checkrevformat(self, revstr, mapname='splicemap'):
212 def checkrevformat(self, revstr, mapname='splicemap'):
213 """revstr is a string that describes a revision in the given
213 """revstr is a string that describes a revision in the given
214 source control system. Return true if revstr has correct
214 source control system. Return true if revstr has correct
215 format.
215 format.
216 """
216 """
217 return True
217 return True
218
218
219 class converter_sink(object):
219 class converter_sink(object):
220 """Conversion sink (target) interface"""
220 """Conversion sink (target) interface"""
221
221
222 def __init__(self, ui, path):
222 def __init__(self, ui, path):
223 """Initialize conversion sink (or raise NoRepo("message")
223 """Initialize conversion sink (or raise NoRepo("message")
224 exception if path is not a valid repository)
224 exception if path is not a valid repository)
225
225
226 created is a list of paths to remove if a fatal error occurs
226 created is a list of paths to remove if a fatal error occurs
227 later"""
227 later"""
228 self.ui = ui
228 self.ui = ui
229 self.path = path
229 self.path = path
230 self.created = []
230 self.created = []
231
231
232 def revmapfile(self):
232 def revmapfile(self):
233 """Path to a file that will contain lines
233 """Path to a file that will contain lines
234 source_rev_id sink_rev_id
234 source_rev_id sink_rev_id
235 mapping equivalent revision identifiers for each system."""
235 mapping equivalent revision identifiers for each system."""
236 raise NotImplementedError
236 raise NotImplementedError
237
237
238 def authorfile(self):
238 def authorfile(self):
239 """Path to a file that will contain lines
239 """Path to a file that will contain lines
240 srcauthor=dstauthor
240 srcauthor=dstauthor
241 mapping equivalent authors identifiers for each system."""
241 mapping equivalent authors identifiers for each system."""
242 return None
242 return None
243
243
244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
245 cleanp2):
245 cleanp2):
246 """Create a revision with all changed files listed in 'files'
246 """Create a revision with all changed files listed in 'files'
247 and having listed parents. 'commit' is a commit object
247 and having listed parents. 'commit' is a commit object
248 containing at a minimum the author, date, and message for this
248 containing at a minimum the author, date, and message for this
249 changeset. 'files' is a list of (path, version) tuples,
249 changeset. 'files' is a list of (path, version) tuples,
250 'copies' is a dictionary mapping destinations to sources,
250 'copies' is a dictionary mapping destinations to sources,
251 'source' is the source repository, and 'revmap' is a mapfile
251 'source' is the source repository, and 'revmap' is a mapfile
252 of source revisions to converted revisions. Only getfile() and
252 of source revisions to converted revisions. Only getfile() and
253 lookuprev() should be called on 'source'. 'full' means that 'files'
253 lookuprev() should be called on 'source'. 'full' means that 'files'
254 is complete and all other files should be removed.
254 is complete and all other files should be removed.
255 'cleanp2' is a set of the filenames that are unchanged from p2
255 'cleanp2' is a set of the filenames that are unchanged from p2
256 (only in the common merge case where there two parents).
256 (only in the common merge case where there two parents).
257
257
258 Note that the sink repository is not told to update itself to
258 Note that the sink repository is not told to update itself to
259 a particular revision (or even what that revision would be)
259 a particular revision (or even what that revision would be)
260 before it receives the file data.
260 before it receives the file data.
261 """
261 """
262 raise NotImplementedError
262 raise NotImplementedError
263
263
264 def puttags(self, tags):
264 def puttags(self, tags):
265 """Put tags into sink.
265 """Put tags into sink.
266
266
267 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
267 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
268 Return a pair (tag_revision, tag_parent_revision), or (None, None)
268 Return a pair (tag_revision, tag_parent_revision), or (None, None)
269 if nothing was changed.
269 if nothing was changed.
270 """
270 """
271 raise NotImplementedError
271 raise NotImplementedError
272
272
273 def setbranch(self, branch, pbranches):
273 def setbranch(self, branch, pbranches):
274 """Set the current branch name. Called before the first putcommit
274 """Set the current branch name. Called before the first putcommit
275 on the branch.
275 on the branch.
276 branch: branch name for subsequent commits
276 branch: branch name for subsequent commits
277 pbranches: (converted parent revision, parent branch) tuples"""
277 pbranches: (converted parent revision, parent branch) tuples"""
278 pass
278 pass
279
279
280 def setfilemapmode(self, active):
280 def setfilemapmode(self, active):
281 """Tell the destination that we're using a filemap
281 """Tell the destination that we're using a filemap
282
282
283 Some converter_sources (svn in particular) can claim that a file
283 Some converter_sources (svn in particular) can claim that a file
284 was changed in a revision, even if there was no change. This method
284 was changed in a revision, even if there was no change. This method
285 tells the destination that we're using a filemap and that it should
285 tells the destination that we're using a filemap and that it should
286 filter empty revisions.
286 filter empty revisions.
287 """
287 """
288 pass
288 pass
289
289
290 def before(self):
290 def before(self):
291 pass
291 pass
292
292
293 def after(self):
293 def after(self):
294 pass
294 pass
295
295
296 def putbookmarks(self, bookmarks):
296 def putbookmarks(self, bookmarks):
297 """Put bookmarks into sink.
297 """Put bookmarks into sink.
298
298
299 bookmarks: {bookmarkname: sink_rev_id, ...}
299 bookmarks: {bookmarkname: sink_rev_id, ...}
300 where bookmarkname is an UTF-8 string.
300 where bookmarkname is an UTF-8 string.
301 """
301 """
302 pass
302 pass
303
303
304 def hascommitfrommap(self, rev):
304 def hascommitfrommap(self, rev):
305 """Return False if a rev mentioned in a filemap is known to not be
305 """Return False if a rev mentioned in a filemap is known to not be
306 present."""
306 present."""
307 raise NotImplementedError
307 raise NotImplementedError
308
308
309 def hascommitforsplicemap(self, rev):
309 def hascommitforsplicemap(self, rev):
310 """This method is for the special needs for splicemap handling and not
310 """This method is for the special needs for splicemap handling and not
311 for general use. Returns True if the sink contains rev, aborts on some
311 for general use. Returns True if the sink contains rev, aborts on some
312 special cases."""
312 special cases."""
313 raise NotImplementedError
313 raise NotImplementedError
314
314
315 class commandline(object):
315 class commandline(object):
316 def __init__(self, ui, command):
316 def __init__(self, ui, command):
317 self.ui = ui
317 self.ui = ui
318 self.command = command
318 self.command = command
319
319
320 def prerun(self):
320 def prerun(self):
321 pass
321 pass
322
322
323 def postrun(self):
323 def postrun(self):
324 pass
324 pass
325
325
326 def _cmdline(self, cmd, *args, **kwargs):
326 def _cmdline(self, cmd, *args, **kwargs):
327 cmdline = [self.command, cmd] + list(args)
327 cmdline = [self.command, cmd] + list(args)
328 for k, v in kwargs.iteritems():
328 for k, v in kwargs.iteritems():
329 if len(k) == 1:
329 if len(k) == 1:
330 cmdline.append('-' + k)
330 cmdline.append('-' + k)
331 else:
331 else:
332 cmdline.append('--' + k.replace('_', '-'))
332 cmdline.append('--' + k.replace('_', '-'))
333 try:
333 try:
334 if len(k) == 1:
334 if len(k) == 1:
335 cmdline.append('' + v)
335 cmdline.append('' + v)
336 else:
336 else:
337 cmdline[-1] += '=' + v
337 cmdline[-1] += '=' + v
338 except TypeError:
338 except TypeError:
339 pass
339 pass
340 cmdline = [util.shellquote(arg) for arg in cmdline]
340 cmdline = [util.shellquote(arg) for arg in cmdline]
341 if not self.ui.debugflag:
341 if not self.ui.debugflag:
342 cmdline += ['2>', os.devnull]
342 cmdline += ['2>', os.devnull]
343 cmdline = ' '.join(cmdline)
343 cmdline = ' '.join(cmdline)
344 return cmdline
344 return cmdline
345
345
346 def _run(self, cmd, *args, **kwargs):
346 def _run(self, cmd, *args, **kwargs):
347 def popen(cmdline):
347 def popen(cmdline):
348 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
348 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
349 close_fds=util.closefds,
349 close_fds=util.closefds,
350 stdout=subprocess.PIPE)
350 stdout=subprocess.PIPE)
351 return p
351 return p
352 return self._dorun(popen, cmd, *args, **kwargs)
352 return self._dorun(popen, cmd, *args, **kwargs)
353
353
354 def _run2(self, cmd, *args, **kwargs):
354 def _run2(self, cmd, *args, **kwargs):
355 return self._dorun(util.popen2, cmd, *args, **kwargs)
355 return self._dorun(util.popen2, cmd, *args, **kwargs)
356
356
357 def _run3(self, cmd, *args, **kwargs):
357 def _run3(self, cmd, *args, **kwargs):
358 return self._dorun(util.popen3, cmd, *args, **kwargs)
358 return self._dorun(util.popen3, cmd, *args, **kwargs)
359
359
360 def _dorun(self, openfunc, cmd, *args, **kwargs):
360 def _dorun(self, openfunc, cmd, *args, **kwargs):
361 cmdline = self._cmdline(cmd, *args, **kwargs)
361 cmdline = self._cmdline(cmd, *args, **kwargs)
362 self.ui.debug('running: %s\n' % (cmdline,))
362 self.ui.debug('running: %s\n' % (cmdline,))
363 self.prerun()
363 self.prerun()
364 try:
364 try:
365 return openfunc(cmdline)
365 return openfunc(cmdline)
366 finally:
366 finally:
367 self.postrun()
367 self.postrun()
368
368
369 def run(self, cmd, *args, **kwargs):
369 def run(self, cmd, *args, **kwargs):
370 p = self._run(cmd, *args, **kwargs)
370 p = self._run(cmd, *args, **kwargs)
371 output = p.communicate()[0]
371 output = p.communicate()[0]
372 self.ui.debug(output)
372 self.ui.debug(output)
373 return output, p.returncode
373 return output, p.returncode
374
374
375 def runlines(self, cmd, *args, **kwargs):
375 def runlines(self, cmd, *args, **kwargs):
376 p = self._run(cmd, *args, **kwargs)
376 p = self._run(cmd, *args, **kwargs)
377 output = p.stdout.readlines()
377 output = p.stdout.readlines()
378 p.wait()
378 p.wait()
379 self.ui.debug(''.join(output))
379 self.ui.debug(''.join(output))
380 return output, p.returncode
380 return output, p.returncode
381
381
382 def checkexit(self, status, output=''):
382 def checkexit(self, status, output=''):
383 if status:
383 if status:
384 if output:
384 if output:
385 self.ui.warn(_('%s error:\n') % self.command)
385 self.ui.warn(_('%s error:\n') % self.command)
386 self.ui.warn(output)
386 self.ui.warn(output)
387 msg = util.explainexit(status)[0]
387 msg = util.explainexit(status)[0]
388 raise error.Abort('%s %s' % (self.command, msg))
388 raise error.Abort('%s %s' % (self.command, msg))
389
389
390 def run0(self, cmd, *args, **kwargs):
390 def run0(self, cmd, *args, **kwargs):
391 output, status = self.run(cmd, *args, **kwargs)
391 output, status = self.run(cmd, *args, **kwargs)
392 self.checkexit(status, output)
392 self.checkexit(status, output)
393 return output
393 return output
394
394
395 def runlines0(self, cmd, *args, **kwargs):
395 def runlines0(self, cmd, *args, **kwargs):
396 output, status = self.runlines(cmd, *args, **kwargs)
396 output, status = self.runlines(cmd, *args, **kwargs)
397 self.checkexit(status, ''.join(output))
397 self.checkexit(status, ''.join(output))
398 return output
398 return output
399
399
400 @propertycache
400 @propertycache
401 def argmax(self):
401 def argmax(self):
402 # POSIX requires at least 4096 bytes for ARG_MAX
402 # POSIX requires at least 4096 bytes for ARG_MAX
403 argmax = 4096
403 argmax = 4096
404 try:
404 try:
405 argmax = os.sysconf("SC_ARG_MAX")
405 argmax = os.sysconf("SC_ARG_MAX")
406 except (AttributeError, ValueError):
406 except (AttributeError, ValueError):
407 pass
407 pass
408
408
409 # Windows shells impose their own limits on command line length,
409 # Windows shells impose their own limits on command line length,
410 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
410 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
411 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
411 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
412 # details about cmd.exe limitations.
412 # details about cmd.exe limitations.
413
413
414 # Since ARG_MAX is for command line _and_ environment, lower our limit
414 # Since ARG_MAX is for command line _and_ environment, lower our limit
415 # (and make happy Windows shells while doing this).
415 # (and make happy Windows shells while doing this).
416 return argmax // 2 - 1
416 return argmax // 2 - 1
417
417
418 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
418 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
419 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
419 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
420 limit = self.argmax - cmdlen
420 limit = self.argmax - cmdlen
421 bytes = 0
421 bytes = 0
422 fl = []
422 fl = []
423 for fn in arglist:
423 for fn in arglist:
424 b = len(fn) + 3
424 b = len(fn) + 3
425 if bytes + b < limit or len(fl) == 0:
425 if bytes + b < limit or len(fl) == 0:
426 fl.append(fn)
426 fl.append(fn)
427 bytes += b
427 bytes += b
428 else:
428 else:
429 yield fl
429 yield fl
430 fl = [fn]
430 fl = [fn]
431 bytes = b
431 bytes = b
432 if fl:
432 if fl:
433 yield fl
433 yield fl
434
434
435 def xargs(self, arglist, cmd, *args, **kwargs):
435 def xargs(self, arglist, cmd, *args, **kwargs):
436 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
436 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
437 self.run0(cmd, *(list(args) + l), **kwargs)
437 self.run0(cmd, *(list(args) + l), **kwargs)
438
438
439 class mapfile(dict):
439 class mapfile(dict):
440 def __init__(self, ui, path):
440 def __init__(self, ui, path):
441 super(mapfile, self).__init__()
441 super(mapfile, self).__init__()
442 self.ui = ui
442 self.ui = ui
443 self.path = path
443 self.path = path
444 self.fp = None
444 self.fp = None
445 self.order = []
445 self.order = []
446 self._read()
446 self._read()
447
447
448 def _read(self):
448 def _read(self):
449 if not self.path:
449 if not self.path:
450 return
450 return
451 try:
451 try:
452 fp = open(self.path, 'r')
452 fp = open(self.path, 'r')
453 except IOError as err:
453 except IOError as err:
454 if err.errno != errno.ENOENT:
454 if err.errno != errno.ENOENT:
455 raise
455 raise
456 return
456 return
457 for i, line in enumerate(fp):
457 for i, line in enumerate(fp):
458 line = line.splitlines()[0].rstrip()
458 line = line.splitlines()[0].rstrip()
459 if not line:
459 if not line:
460 # Ignore blank lines
460 # Ignore blank lines
461 continue
461 continue
462 try:
462 try:
463 key, value = line.rsplit(' ', 1)
463 key, value = line.rsplit(' ', 1)
464 except ValueError:
464 except ValueError:
465 raise error.Abort(
465 raise error.Abort(
466 _('syntax error in %s(%d): key/value pair expected')
466 _('syntax error in %s(%d): key/value pair expected')
467 % (self.path, i + 1))
467 % (self.path, i + 1))
468 if key not in self:
468 if key not in self:
469 self.order.append(key)
469 self.order.append(key)
470 super(mapfile, self).__setitem__(key, value)
470 super(mapfile, self).__setitem__(key, value)
471 fp.close()
471 fp.close()
472
472
473 def __setitem__(self, key, value):
473 def __setitem__(self, key, value):
474 if self.fp is None:
474 if self.fp is None:
475 try:
475 try:
476 self.fp = open(self.path, 'a')
476 self.fp = open(self.path, 'a')
477 except IOError as err:
477 except IOError as err:
478 raise error.Abort(_('could not open map file %r: %s') %
478 raise error.Abort(_('could not open map file %r: %s') %
479 (self.path, err.strerror))
479 (self.path, err.strerror))
480 self.fp.write('%s %s\n' % (key, value))
480 self.fp.write('%s %s\n' % (key, value))
481 self.fp.flush()
481 self.fp.flush()
482 super(mapfile, self).__setitem__(key, value)
482 super(mapfile, self).__setitem__(key, value)
483
483
484 def close(self):
484 def close(self):
485 if self.fp:
485 if self.fp:
486 self.fp.close()
486 self.fp.close()
487 self.fp = None
487 self.fp = None
488
488
489 def makedatetimestamp(t):
489 def makedatetimestamp(t):
490 """Like util.makedate() but for time t instead of current time"""
490 """Like util.makedate() but for time t instead of current time"""
491 delta = (datetime.datetime.utcfromtimestamp(t) -
491 delta = (datetime.datetime.utcfromtimestamp(t) -
492 datetime.datetime.fromtimestamp(t))
492 datetime.datetime.fromtimestamp(t))
493 tz = delta.days * 86400 + delta.seconds
493 tz = delta.days * 86400 + delta.seconds
494 return t, tz
494 return t, tz
@@ -1,611 +1,611
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import shlex
10 import shlex
11 import shutil
11 import shutil
12
12
13 from mercurial.i18n import _
13 from mercurial import (
14 from mercurial import (
14 encoding,
15 encoding,
15 error,
16 error,
16 hg,
17 hg,
17 util,
18 util,
18 )
19 )
19 from mercurial.i18n import _
20
20
21 from . import (
21 from . import (
22 bzr,
22 bzr,
23 common,
23 common,
24 cvs,
24 cvs,
25 darcs,
25 darcs,
26 filemap,
26 filemap,
27 git,
27 git,
28 gnuarch,
28 gnuarch,
29 hg as hgconvert,
29 hg as hgconvert,
30 monotone,
30 monotone,
31 p4,
31 p4,
32 subversion,
32 subversion,
33 )
33 )
34
34
35 mapfile = common.mapfile
35 mapfile = common.mapfile
36 MissingTool = common.MissingTool
36 MissingTool = common.MissingTool
37 NoRepo = common.NoRepo
37 NoRepo = common.NoRepo
38 SKIPREV = common.SKIPREV
38 SKIPREV = common.SKIPREV
39
39
40 bzr_source = bzr.bzr_source
40 bzr_source = bzr.bzr_source
41 convert_cvs = cvs.convert_cvs
41 convert_cvs = cvs.convert_cvs
42 convert_git = git.convert_git
42 convert_git = git.convert_git
43 darcs_source = darcs.darcs_source
43 darcs_source = darcs.darcs_source
44 gnuarch_source = gnuarch.gnuarch_source
44 gnuarch_source = gnuarch.gnuarch_source
45 mercurial_sink = hgconvert.mercurial_sink
45 mercurial_sink = hgconvert.mercurial_sink
46 mercurial_source = hgconvert.mercurial_source
46 mercurial_source = hgconvert.mercurial_source
47 monotone_source = monotone.monotone_source
47 monotone_source = monotone.monotone_source
48 p4_source = p4.p4_source
48 p4_source = p4.p4_source
49 svn_sink = subversion.svn_sink
49 svn_sink = subversion.svn_sink
50 svn_source = subversion.svn_source
50 svn_source = subversion.svn_source
51
51
52 orig_encoding = 'ascii'
52 orig_encoding = 'ascii'
53
53
54 def recode(s):
54 def recode(s):
55 if isinstance(s, unicode):
55 if isinstance(s, unicode):
56 return s.encode(orig_encoding, 'replace')
56 return s.encode(orig_encoding, 'replace')
57 else:
57 else:
58 return s.decode('utf-8').encode(orig_encoding, 'replace')
58 return s.decode('utf-8').encode(orig_encoding, 'replace')
59
59
60 def mapbranch(branch, branchmap):
60 def mapbranch(branch, branchmap):
61 '''
61 '''
62 >>> bmap = {'default': 'branch1'}
62 >>> bmap = {'default': 'branch1'}
63 >>> for i in ['', None]:
63 >>> for i in ['', None]:
64 ... mapbranch(i, bmap)
64 ... mapbranch(i, bmap)
65 'branch1'
65 'branch1'
66 'branch1'
66 'branch1'
67 >>> bmap = {'None': 'branch2'}
67 >>> bmap = {'None': 'branch2'}
68 >>> for i in ['', None]:
68 >>> for i in ['', None]:
69 ... mapbranch(i, bmap)
69 ... mapbranch(i, bmap)
70 'branch2'
70 'branch2'
71 'branch2'
71 'branch2'
72 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
72 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
73 >>> for i in ['None', '', None, 'default', 'branch5']:
73 >>> for i in ['None', '', None, 'default', 'branch5']:
74 ... mapbranch(i, bmap)
74 ... mapbranch(i, bmap)
75 'branch3'
75 'branch3'
76 'branch4'
76 'branch4'
77 'branch4'
77 'branch4'
78 'branch4'
78 'branch4'
79 'branch5'
79 'branch5'
80 '''
80 '''
81 # If branch is None or empty, this commit is coming from the source
81 # If branch is None or empty, this commit is coming from the source
82 # repository's default branch and destined for the default branch in the
82 # repository's default branch and destined for the default branch in the
83 # destination repository. For such commits, using a literal "default"
83 # destination repository. For such commits, using a literal "default"
84 # in branchmap below allows the user to map "default" to an alternate
84 # in branchmap below allows the user to map "default" to an alternate
85 # default branch in the destination repository.
85 # default branch in the destination repository.
86 branch = branchmap.get(branch or 'default', branch)
86 branch = branchmap.get(branch or 'default', branch)
87 # At some point we used "None" literal to denote the default branch,
87 # At some point we used "None" literal to denote the default branch,
88 # attempt to use that for backward compatibility.
88 # attempt to use that for backward compatibility.
89 if (not branch):
89 if (not branch):
90 branch = branchmap.get(str(None), branch)
90 branch = branchmap.get(str(None), branch)
91 return branch
91 return branch
92
92
93 source_converters = [
93 source_converters = [
94 ('cvs', convert_cvs, 'branchsort'),
94 ('cvs', convert_cvs, 'branchsort'),
95 ('git', convert_git, 'branchsort'),
95 ('git', convert_git, 'branchsort'),
96 ('svn', svn_source, 'branchsort'),
96 ('svn', svn_source, 'branchsort'),
97 ('hg', mercurial_source, 'sourcesort'),
97 ('hg', mercurial_source, 'sourcesort'),
98 ('darcs', darcs_source, 'branchsort'),
98 ('darcs', darcs_source, 'branchsort'),
99 ('mtn', monotone_source, 'branchsort'),
99 ('mtn', monotone_source, 'branchsort'),
100 ('gnuarch', gnuarch_source, 'branchsort'),
100 ('gnuarch', gnuarch_source, 'branchsort'),
101 ('bzr', bzr_source, 'branchsort'),
101 ('bzr', bzr_source, 'branchsort'),
102 ('p4', p4_source, 'branchsort'),
102 ('p4', p4_source, 'branchsort'),
103 ]
103 ]
104
104
105 sink_converters = [
105 sink_converters = [
106 ('hg', mercurial_sink),
106 ('hg', mercurial_sink),
107 ('svn', svn_sink),
107 ('svn', svn_sink),
108 ]
108 ]
109
109
110 def convertsource(ui, path, type, revs):
110 def convertsource(ui, path, type, revs):
111 exceptions = []
111 exceptions = []
112 if type and type not in [s[0] for s in source_converters]:
112 if type and type not in [s[0] for s in source_converters]:
113 raise error.Abort(_('%s: invalid source repository type') % type)
113 raise error.Abort(_('%s: invalid source repository type') % type)
114 for name, source, sortmode in source_converters:
114 for name, source, sortmode in source_converters:
115 try:
115 try:
116 if not type or name == type:
116 if not type or name == type:
117 return source(ui, path, revs), sortmode
117 return source(ui, path, revs), sortmode
118 except (NoRepo, MissingTool) as inst:
118 except (NoRepo, MissingTool) as inst:
119 exceptions.append(inst)
119 exceptions.append(inst)
120 if not ui.quiet:
120 if not ui.quiet:
121 for inst in exceptions:
121 for inst in exceptions:
122 ui.write("%s\n" % inst)
122 ui.write("%s\n" % inst)
123 raise error.Abort(_('%s: missing or unsupported repository') % path)
123 raise error.Abort(_('%s: missing or unsupported repository') % path)
124
124
125 def convertsink(ui, path, type):
125 def convertsink(ui, path, type):
126 if type and type not in [s[0] for s in sink_converters]:
126 if type and type not in [s[0] for s in sink_converters]:
127 raise error.Abort(_('%s: invalid destination repository type') % type)
127 raise error.Abort(_('%s: invalid destination repository type') % type)
128 for name, sink in sink_converters:
128 for name, sink in sink_converters:
129 try:
129 try:
130 if not type or name == type:
130 if not type or name == type:
131 return sink(ui, path)
131 return sink(ui, path)
132 except NoRepo as inst:
132 except NoRepo as inst:
133 ui.note(_("convert: %s\n") % inst)
133 ui.note(_("convert: %s\n") % inst)
134 except MissingTool as inst:
134 except MissingTool as inst:
135 raise error.Abort('%s\n' % inst)
135 raise error.Abort('%s\n' % inst)
136 raise error.Abort(_('%s: unknown repository type') % path)
136 raise error.Abort(_('%s: unknown repository type') % path)
137
137
138 class progresssource(object):
138 class progresssource(object):
139 def __init__(self, ui, source, filecount):
139 def __init__(self, ui, source, filecount):
140 self.ui = ui
140 self.ui = ui
141 self.source = source
141 self.source = source
142 self.filecount = filecount
142 self.filecount = filecount
143 self.retrieved = 0
143 self.retrieved = 0
144
144
145 def getfile(self, file, rev):
145 def getfile(self, file, rev):
146 self.retrieved += 1
146 self.retrieved += 1
147 self.ui.progress(_('getting files'), self.retrieved,
147 self.ui.progress(_('getting files'), self.retrieved,
148 item=file, total=self.filecount, unit=_('files'))
148 item=file, total=self.filecount, unit=_('files'))
149 return self.source.getfile(file, rev)
149 return self.source.getfile(file, rev)
150
150
151 def targetfilebelongstosource(self, targetfilename):
151 def targetfilebelongstosource(self, targetfilename):
152 return self.source.targetfilebelongstosource(targetfilename)
152 return self.source.targetfilebelongstosource(targetfilename)
153
153
154 def lookuprev(self, rev):
154 def lookuprev(self, rev):
155 return self.source.lookuprev(rev)
155 return self.source.lookuprev(rev)
156
156
157 def close(self):
157 def close(self):
158 self.ui.progress(_('getting files'), None)
158 self.ui.progress(_('getting files'), None)
159
159
160 class converter(object):
160 class converter(object):
161 def __init__(self, ui, source, dest, revmapfile, opts):
161 def __init__(self, ui, source, dest, revmapfile, opts):
162
162
163 self.source = source
163 self.source = source
164 self.dest = dest
164 self.dest = dest
165 self.ui = ui
165 self.ui = ui
166 self.opts = opts
166 self.opts = opts
167 self.commitcache = {}
167 self.commitcache = {}
168 self.authors = {}
168 self.authors = {}
169 self.authorfile = None
169 self.authorfile = None
170
170
171 # Record converted revisions persistently: maps source revision
171 # Record converted revisions persistently: maps source revision
172 # ID to target revision ID (both strings). (This is how
172 # ID to target revision ID (both strings). (This is how
173 # incremental conversions work.)
173 # incremental conversions work.)
174 self.map = mapfile(ui, revmapfile)
174 self.map = mapfile(ui, revmapfile)
175
175
176 # Read first the dst author map if any
176 # Read first the dst author map if any
177 authorfile = self.dest.authorfile()
177 authorfile = self.dest.authorfile()
178 if authorfile and os.path.exists(authorfile):
178 if authorfile and os.path.exists(authorfile):
179 self.readauthormap(authorfile)
179 self.readauthormap(authorfile)
180 # Extend/Override with new author map if necessary
180 # Extend/Override with new author map if necessary
181 if opts.get('authormap'):
181 if opts.get('authormap'):
182 self.readauthormap(opts.get('authormap'))
182 self.readauthormap(opts.get('authormap'))
183 self.authorfile = self.dest.authorfile()
183 self.authorfile = self.dest.authorfile()
184
184
185 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
185 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
186 self.branchmap = mapfile(ui, opts.get('branchmap'))
186 self.branchmap = mapfile(ui, opts.get('branchmap'))
187
187
188 def parsesplicemap(self, path):
188 def parsesplicemap(self, path):
189 """ check and validate the splicemap format and
189 """ check and validate the splicemap format and
190 return a child/parents dictionary.
190 return a child/parents dictionary.
191 Format checking has two parts.
191 Format checking has two parts.
192 1. generic format which is same across all source types
192 1. generic format which is same across all source types
193 2. specific format checking which may be different for
193 2. specific format checking which may be different for
194 different source type. This logic is implemented in
194 different source type. This logic is implemented in
195 checkrevformat function in source files like
195 checkrevformat function in source files like
196 hg.py, subversion.py etc.
196 hg.py, subversion.py etc.
197 """
197 """
198
198
199 if not path:
199 if not path:
200 return {}
200 return {}
201 m = {}
201 m = {}
202 try:
202 try:
203 fp = open(path, 'r')
203 fp = open(path, 'r')
204 for i, line in enumerate(fp):
204 for i, line in enumerate(fp):
205 line = line.splitlines()[0].rstrip()
205 line = line.splitlines()[0].rstrip()
206 if not line:
206 if not line:
207 # Ignore blank lines
207 # Ignore blank lines
208 continue
208 continue
209 # split line
209 # split line
210 lex = shlex.shlex(line, posix=True)
210 lex = shlex.shlex(line, posix=True)
211 lex.whitespace_split = True
211 lex.whitespace_split = True
212 lex.whitespace += ','
212 lex.whitespace += ','
213 line = list(lex)
213 line = list(lex)
214 # check number of parents
214 # check number of parents
215 if not (2 <= len(line) <= 3):
215 if not (2 <= len(line) <= 3):
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
217 '[,parent2] expected') % (path, i + 1))
217 '[,parent2] expected') % (path, i + 1))
218 for part in line:
218 for part in line:
219 self.source.checkrevformat(part)
219 self.source.checkrevformat(part)
220 child, p1, p2 = line[0], line[1:2], line[2:]
220 child, p1, p2 = line[0], line[1:2], line[2:]
221 if p1 == p2:
221 if p1 == p2:
222 m[child] = p1
222 m[child] = p1
223 else:
223 else:
224 m[child] = p1 + p2
224 m[child] = p1 + p2
225 # if file does not exist or error reading, exit
225 # if file does not exist or error reading, exit
226 except IOError:
226 except IOError:
227 raise error.Abort(_('splicemap file not found or error reading %s:')
227 raise error.Abort(_('splicemap file not found or error reading %s:')
228 % path)
228 % path)
229 return m
229 return m
230
230
231
231
232 def walktree(self, heads):
232 def walktree(self, heads):
233 '''Return a mapping that identifies the uncommitted parents of every
233 '''Return a mapping that identifies the uncommitted parents of every
234 uncommitted changeset.'''
234 uncommitted changeset.'''
235 visit = heads
235 visit = heads
236 known = set()
236 known = set()
237 parents = {}
237 parents = {}
238 numcommits = self.source.numcommits()
238 numcommits = self.source.numcommits()
239 while visit:
239 while visit:
240 n = visit.pop(0)
240 n = visit.pop(0)
241 if n in known:
241 if n in known:
242 continue
242 continue
243 if n in self.map:
243 if n in self.map:
244 m = self.map[n]
244 m = self.map[n]
245 if m == SKIPREV or self.dest.hascommitfrommap(m):
245 if m == SKIPREV or self.dest.hascommitfrommap(m):
246 continue
246 continue
247 known.add(n)
247 known.add(n)
248 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
248 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
249 total=numcommits)
249 total=numcommits)
250 commit = self.cachecommit(n)
250 commit = self.cachecommit(n)
251 parents[n] = []
251 parents[n] = []
252 for p in commit.parents:
252 for p in commit.parents:
253 parents[n].append(p)
253 parents[n].append(p)
254 visit.append(p)
254 visit.append(p)
255 self.ui.progress(_('scanning'), None)
255 self.ui.progress(_('scanning'), None)
256
256
257 return parents
257 return parents
258
258
259 def mergesplicemap(self, parents, splicemap):
259 def mergesplicemap(self, parents, splicemap):
260 """A splicemap redefines child/parent relationships. Check the
260 """A splicemap redefines child/parent relationships. Check the
261 map contains valid revision identifiers and merge the new
261 map contains valid revision identifiers and merge the new
262 links in the source graph.
262 links in the source graph.
263 """
263 """
264 for c in sorted(splicemap):
264 for c in sorted(splicemap):
265 if c not in parents:
265 if c not in parents:
266 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
266 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
267 # Could be in source but not converted during this run
267 # Could be in source but not converted during this run
268 self.ui.warn(_('splice map revision %s is not being '
268 self.ui.warn(_('splice map revision %s is not being '
269 'converted, ignoring\n') % c)
269 'converted, ignoring\n') % c)
270 continue
270 continue
271 pc = []
271 pc = []
272 for p in splicemap[c]:
272 for p in splicemap[c]:
273 # We do not have to wait for nodes already in dest.
273 # We do not have to wait for nodes already in dest.
274 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
274 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
275 continue
275 continue
276 # Parent is not in dest and not being converted, not good
276 # Parent is not in dest and not being converted, not good
277 if p not in parents:
277 if p not in parents:
278 raise error.Abort(_('unknown splice map parent: %s') % p)
278 raise error.Abort(_('unknown splice map parent: %s') % p)
279 pc.append(p)
279 pc.append(p)
280 parents[c] = pc
280 parents[c] = pc
281
281
282 def toposort(self, parents, sortmode):
282 def toposort(self, parents, sortmode):
283 '''Return an ordering such that every uncommitted changeset is
283 '''Return an ordering such that every uncommitted changeset is
284 preceded by all its uncommitted ancestors.'''
284 preceded by all its uncommitted ancestors.'''
285
285
286 def mapchildren(parents):
286 def mapchildren(parents):
287 """Return a (children, roots) tuple where 'children' maps parent
287 """Return a (children, roots) tuple where 'children' maps parent
288 revision identifiers to children ones, and 'roots' is the list of
288 revision identifiers to children ones, and 'roots' is the list of
289 revisions without parents. 'parents' must be a mapping of revision
289 revisions without parents. 'parents' must be a mapping of revision
290 identifier to its parents ones.
290 identifier to its parents ones.
291 """
291 """
292 visit = sorted(parents)
292 visit = sorted(parents)
293 seen = set()
293 seen = set()
294 children = {}
294 children = {}
295 roots = []
295 roots = []
296
296
297 while visit:
297 while visit:
298 n = visit.pop(0)
298 n = visit.pop(0)
299 if n in seen:
299 if n in seen:
300 continue
300 continue
301 seen.add(n)
301 seen.add(n)
302 # Ensure that nodes without parents are present in the
302 # Ensure that nodes without parents are present in the
303 # 'children' mapping.
303 # 'children' mapping.
304 children.setdefault(n, [])
304 children.setdefault(n, [])
305 hasparent = False
305 hasparent = False
306 for p in parents[n]:
306 for p in parents[n]:
307 if p not in self.map:
307 if p not in self.map:
308 visit.append(p)
308 visit.append(p)
309 hasparent = True
309 hasparent = True
310 children.setdefault(p, []).append(n)
310 children.setdefault(p, []).append(n)
311 if not hasparent:
311 if not hasparent:
312 roots.append(n)
312 roots.append(n)
313
313
314 return children, roots
314 return children, roots
315
315
316 # Sort functions are supposed to take a list of revisions which
316 # Sort functions are supposed to take a list of revisions which
317 # can be converted immediately and pick one
317 # can be converted immediately and pick one
318
318
319 def makebranchsorter():
319 def makebranchsorter():
320 """If the previously converted revision has a child in the
320 """If the previously converted revision has a child in the
321 eligible revisions list, pick it. Return the list head
321 eligible revisions list, pick it. Return the list head
322 otherwise. Branch sort attempts to minimize branch
322 otherwise. Branch sort attempts to minimize branch
323 switching, which is harmful for Mercurial backend
323 switching, which is harmful for Mercurial backend
324 compression.
324 compression.
325 """
325 """
326 prev = [None]
326 prev = [None]
327 def picknext(nodes):
327 def picknext(nodes):
328 next = nodes[0]
328 next = nodes[0]
329 for n in nodes:
329 for n in nodes:
330 if prev[0] in parents[n]:
330 if prev[0] in parents[n]:
331 next = n
331 next = n
332 break
332 break
333 prev[0] = next
333 prev[0] = next
334 return next
334 return next
335 return picknext
335 return picknext
336
336
337 def makesourcesorter():
337 def makesourcesorter():
338 """Source specific sort."""
338 """Source specific sort."""
339 keyfn = lambda n: self.commitcache[n].sortkey
339 keyfn = lambda n: self.commitcache[n].sortkey
340 def picknext(nodes):
340 def picknext(nodes):
341 return sorted(nodes, key=keyfn)[0]
341 return sorted(nodes, key=keyfn)[0]
342 return picknext
342 return picknext
343
343
344 def makeclosesorter():
344 def makeclosesorter():
345 """Close order sort."""
345 """Close order sort."""
346 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
346 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
347 self.commitcache[n].sortkey)
347 self.commitcache[n].sortkey)
348 def picknext(nodes):
348 def picknext(nodes):
349 return sorted(nodes, key=keyfn)[0]
349 return sorted(nodes, key=keyfn)[0]
350 return picknext
350 return picknext
351
351
352 def makedatesorter():
352 def makedatesorter():
353 """Sort revisions by date."""
353 """Sort revisions by date."""
354 dates = {}
354 dates = {}
355 def getdate(n):
355 def getdate(n):
356 if n not in dates:
356 if n not in dates:
357 dates[n] = util.parsedate(self.commitcache[n].date)
357 dates[n] = util.parsedate(self.commitcache[n].date)
358 return dates[n]
358 return dates[n]
359
359
360 def picknext(nodes):
360 def picknext(nodes):
361 return min([(getdate(n), n) for n in nodes])[1]
361 return min([(getdate(n), n) for n in nodes])[1]
362
362
363 return picknext
363 return picknext
364
364
365 if sortmode == 'branchsort':
365 if sortmode == 'branchsort':
366 picknext = makebranchsorter()
366 picknext = makebranchsorter()
367 elif sortmode == 'datesort':
367 elif sortmode == 'datesort':
368 picknext = makedatesorter()
368 picknext = makedatesorter()
369 elif sortmode == 'sourcesort':
369 elif sortmode == 'sourcesort':
370 picknext = makesourcesorter()
370 picknext = makesourcesorter()
371 elif sortmode == 'closesort':
371 elif sortmode == 'closesort':
372 picknext = makeclosesorter()
372 picknext = makeclosesorter()
373 else:
373 else:
374 raise error.Abort(_('unknown sort mode: %s') % sortmode)
374 raise error.Abort(_('unknown sort mode: %s') % sortmode)
375
375
376 children, actives = mapchildren(parents)
376 children, actives = mapchildren(parents)
377
377
378 s = []
378 s = []
379 pendings = {}
379 pendings = {}
380 while actives:
380 while actives:
381 n = picknext(actives)
381 n = picknext(actives)
382 actives.remove(n)
382 actives.remove(n)
383 s.append(n)
383 s.append(n)
384
384
385 # Update dependents list
385 # Update dependents list
386 for c in children.get(n, []):
386 for c in children.get(n, []):
387 if c not in pendings:
387 if c not in pendings:
388 pendings[c] = [p for p in parents[c] if p not in self.map]
388 pendings[c] = [p for p in parents[c] if p not in self.map]
389 try:
389 try:
390 pendings[c].remove(n)
390 pendings[c].remove(n)
391 except ValueError:
391 except ValueError:
392 raise error.Abort(_('cycle detected between %s and %s')
392 raise error.Abort(_('cycle detected between %s and %s')
393 % (recode(c), recode(n)))
393 % (recode(c), recode(n)))
394 if not pendings[c]:
394 if not pendings[c]:
395 # Parents are converted, node is eligible
395 # Parents are converted, node is eligible
396 actives.insert(0, c)
396 actives.insert(0, c)
397 pendings[c] = None
397 pendings[c] = None
398
398
399 if len(s) != len(parents):
399 if len(s) != len(parents):
400 raise error.Abort(_("not all revisions were sorted"))
400 raise error.Abort(_("not all revisions were sorted"))
401
401
402 return s
402 return s
403
403
404 def writeauthormap(self):
404 def writeauthormap(self):
405 authorfile = self.authorfile
405 authorfile = self.authorfile
406 if authorfile:
406 if authorfile:
407 self.ui.status(_('writing author map file %s\n') % authorfile)
407 self.ui.status(_('writing author map file %s\n') % authorfile)
408 ofile = open(authorfile, 'w+')
408 ofile = open(authorfile, 'w+')
409 for author in self.authors:
409 for author in self.authors:
410 ofile.write("%s=%s\n" % (author, self.authors[author]))
410 ofile.write("%s=%s\n" % (author, self.authors[author]))
411 ofile.close()
411 ofile.close()
412
412
413 def readauthormap(self, authorfile):
413 def readauthormap(self, authorfile):
414 afile = open(authorfile, 'r')
414 afile = open(authorfile, 'r')
415 for line in afile:
415 for line in afile:
416
416
417 line = line.strip()
417 line = line.strip()
418 if not line or line.startswith('#'):
418 if not line or line.startswith('#'):
419 continue
419 continue
420
420
421 try:
421 try:
422 srcauthor, dstauthor = line.split('=', 1)
422 srcauthor, dstauthor = line.split('=', 1)
423 except ValueError:
423 except ValueError:
424 msg = _('ignoring bad line in author map file %s: %s\n')
424 msg = _('ignoring bad line in author map file %s: %s\n')
425 self.ui.warn(msg % (authorfile, line.rstrip()))
425 self.ui.warn(msg % (authorfile, line.rstrip()))
426 continue
426 continue
427
427
428 srcauthor = srcauthor.strip()
428 srcauthor = srcauthor.strip()
429 dstauthor = dstauthor.strip()
429 dstauthor = dstauthor.strip()
430 if self.authors.get(srcauthor) in (None, dstauthor):
430 if self.authors.get(srcauthor) in (None, dstauthor):
431 msg = _('mapping author %s to %s\n')
431 msg = _('mapping author %s to %s\n')
432 self.ui.debug(msg % (srcauthor, dstauthor))
432 self.ui.debug(msg % (srcauthor, dstauthor))
433 self.authors[srcauthor] = dstauthor
433 self.authors[srcauthor] = dstauthor
434 continue
434 continue
435
435
436 m = _('overriding mapping for author %s, was %s, will be %s\n')
436 m = _('overriding mapping for author %s, was %s, will be %s\n')
437 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
437 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
438
438
439 afile.close()
439 afile.close()
440
440
441 def cachecommit(self, rev):
441 def cachecommit(self, rev):
442 commit = self.source.getcommit(rev)
442 commit = self.source.getcommit(rev)
443 commit.author = self.authors.get(commit.author, commit.author)
443 commit.author = self.authors.get(commit.author, commit.author)
444 commit.branch = mapbranch(commit.branch, self.branchmap)
444 commit.branch = mapbranch(commit.branch, self.branchmap)
445 self.commitcache[rev] = commit
445 self.commitcache[rev] = commit
446 return commit
446 return commit
447
447
448 def copy(self, rev):
448 def copy(self, rev):
449 commit = self.commitcache[rev]
449 commit = self.commitcache[rev]
450 full = self.opts.get('full')
450 full = self.opts.get('full')
451 changes = self.source.getchanges(rev, full)
451 changes = self.source.getchanges(rev, full)
452 if isinstance(changes, basestring):
452 if isinstance(changes, basestring):
453 if changes == SKIPREV:
453 if changes == SKIPREV:
454 dest = SKIPREV
454 dest = SKIPREV
455 else:
455 else:
456 dest = self.map[changes]
456 dest = self.map[changes]
457 self.map[rev] = dest
457 self.map[rev] = dest
458 return
458 return
459 files, copies, cleanp2 = changes
459 files, copies, cleanp2 = changes
460 pbranches = []
460 pbranches = []
461 if commit.parents:
461 if commit.parents:
462 for prev in commit.parents:
462 for prev in commit.parents:
463 if prev not in self.commitcache:
463 if prev not in self.commitcache:
464 self.cachecommit(prev)
464 self.cachecommit(prev)
465 pbranches.append((self.map[prev],
465 pbranches.append((self.map[prev],
466 self.commitcache[prev].branch))
466 self.commitcache[prev].branch))
467 self.dest.setbranch(commit.branch, pbranches)
467 self.dest.setbranch(commit.branch, pbranches)
468 try:
468 try:
469 parents = self.splicemap[rev]
469 parents = self.splicemap[rev]
470 self.ui.status(_('spliced in %s as parents of %s\n') %
470 self.ui.status(_('spliced in %s as parents of %s\n') %
471 (_(' and ').join(parents), rev))
471 (_(' and ').join(parents), rev))
472 parents = [self.map.get(p, p) for p in parents]
472 parents = [self.map.get(p, p) for p in parents]
473 except KeyError:
473 except KeyError:
474 parents = [b[0] for b in pbranches]
474 parents = [b[0] for b in pbranches]
475 parents.extend(self.map[x]
475 parents.extend(self.map[x]
476 for x in commit.optparents
476 for x in commit.optparents
477 if x in self.map)
477 if x in self.map)
478 if len(pbranches) != 2:
478 if len(pbranches) != 2:
479 cleanp2 = set()
479 cleanp2 = set()
480 if len(parents) < 3:
480 if len(parents) < 3:
481 source = progresssource(self.ui, self.source, len(files))
481 source = progresssource(self.ui, self.source, len(files))
482 else:
482 else:
483 # For an octopus merge, we end up traversing the list of
483 # For an octopus merge, we end up traversing the list of
484 # changed files N-1 times. This tweak to the number of
484 # changed files N-1 times. This tweak to the number of
485 # files makes it so the progress bar doesn't overflow
485 # files makes it so the progress bar doesn't overflow
486 # itself.
486 # itself.
487 source = progresssource(self.ui, self.source,
487 source = progresssource(self.ui, self.source,
488 len(files) * (len(parents) - 1))
488 len(files) * (len(parents) - 1))
489 newnode = self.dest.putcommit(files, copies, parents, commit,
489 newnode = self.dest.putcommit(files, copies, parents, commit,
490 source, self.map, full, cleanp2)
490 source, self.map, full, cleanp2)
491 source.close()
491 source.close()
492 self.source.converted(rev, newnode)
492 self.source.converted(rev, newnode)
493 self.map[rev] = newnode
493 self.map[rev] = newnode
494
494
495 def convert(self, sortmode):
495 def convert(self, sortmode):
496 try:
496 try:
497 self.source.before()
497 self.source.before()
498 self.dest.before()
498 self.dest.before()
499 self.source.setrevmap(self.map)
499 self.source.setrevmap(self.map)
500 self.ui.status(_("scanning source...\n"))
500 self.ui.status(_("scanning source...\n"))
501 heads = self.source.getheads()
501 heads = self.source.getheads()
502 parents = self.walktree(heads)
502 parents = self.walktree(heads)
503 self.mergesplicemap(parents, self.splicemap)
503 self.mergesplicemap(parents, self.splicemap)
504 self.ui.status(_("sorting...\n"))
504 self.ui.status(_("sorting...\n"))
505 t = self.toposort(parents, sortmode)
505 t = self.toposort(parents, sortmode)
506 num = len(t)
506 num = len(t)
507 c = None
507 c = None
508
508
509 self.ui.status(_("converting...\n"))
509 self.ui.status(_("converting...\n"))
510 for i, c in enumerate(t):
510 for i, c in enumerate(t):
511 num -= 1
511 num -= 1
512 desc = self.commitcache[c].desc
512 desc = self.commitcache[c].desc
513 if "\n" in desc:
513 if "\n" in desc:
514 desc = desc.splitlines()[0]
514 desc = desc.splitlines()[0]
515 # convert log message to local encoding without using
515 # convert log message to local encoding without using
516 # tolocal() because the encoding.encoding convert()
516 # tolocal() because the encoding.encoding convert()
517 # uses is 'utf-8'
517 # uses is 'utf-8'
518 self.ui.status("%d %s\n" % (num, recode(desc)))
518 self.ui.status("%d %s\n" % (num, recode(desc)))
519 self.ui.note(_("source: %s\n") % recode(c))
519 self.ui.note(_("source: %s\n") % recode(c))
520 self.ui.progress(_('converting'), i, unit=_('revisions'),
520 self.ui.progress(_('converting'), i, unit=_('revisions'),
521 total=len(t))
521 total=len(t))
522 self.copy(c)
522 self.copy(c)
523 self.ui.progress(_('converting'), None)
523 self.ui.progress(_('converting'), None)
524
524
525 if not self.ui.configbool('convert', 'skiptags'):
525 if not self.ui.configbool('convert', 'skiptags'):
526 tags = self.source.gettags()
526 tags = self.source.gettags()
527 ctags = {}
527 ctags = {}
528 for k in tags:
528 for k in tags:
529 v = tags[k]
529 v = tags[k]
530 if self.map.get(v, SKIPREV) != SKIPREV:
530 if self.map.get(v, SKIPREV) != SKIPREV:
531 ctags[k] = self.map[v]
531 ctags[k] = self.map[v]
532
532
533 if c and ctags:
533 if c and ctags:
534 nrev, tagsparent = self.dest.puttags(ctags)
534 nrev, tagsparent = self.dest.puttags(ctags)
535 if nrev and tagsparent:
535 if nrev and tagsparent:
536 # write another hash correspondence to override the
536 # write another hash correspondence to override the
537 # previous one so we don't end up with extra tag heads
537 # previous one so we don't end up with extra tag heads
538 tagsparents = [e for e in self.map.iteritems()
538 tagsparents = [e for e in self.map.iteritems()
539 if e[1] == tagsparent]
539 if e[1] == tagsparent]
540 if tagsparents:
540 if tagsparents:
541 self.map[tagsparents[0][0]] = nrev
541 self.map[tagsparents[0][0]] = nrev
542
542
543 bookmarks = self.source.getbookmarks()
543 bookmarks = self.source.getbookmarks()
544 cbookmarks = {}
544 cbookmarks = {}
545 for k in bookmarks:
545 for k in bookmarks:
546 v = bookmarks[k]
546 v = bookmarks[k]
547 if self.map.get(v, SKIPREV) != SKIPREV:
547 if self.map.get(v, SKIPREV) != SKIPREV:
548 cbookmarks[k] = self.map[v]
548 cbookmarks[k] = self.map[v]
549
549
550 if c and cbookmarks:
550 if c and cbookmarks:
551 self.dest.putbookmarks(cbookmarks)
551 self.dest.putbookmarks(cbookmarks)
552
552
553 self.writeauthormap()
553 self.writeauthormap()
554 finally:
554 finally:
555 self.cleanup()
555 self.cleanup()
556
556
557 def cleanup(self):
557 def cleanup(self):
558 try:
558 try:
559 self.dest.after()
559 self.dest.after()
560 finally:
560 finally:
561 self.source.after()
561 self.source.after()
562 self.map.close()
562 self.map.close()
563
563
564 def convert(ui, src, dest=None, revmapfile=None, **opts):
564 def convert(ui, src, dest=None, revmapfile=None, **opts):
565 global orig_encoding
565 global orig_encoding
566 orig_encoding = encoding.encoding
566 orig_encoding = encoding.encoding
567 encoding.encoding = 'UTF-8'
567 encoding.encoding = 'UTF-8'
568
568
569 # support --authors as an alias for --authormap
569 # support --authors as an alias for --authormap
570 if not opts.get('authormap'):
570 if not opts.get('authormap'):
571 opts['authormap'] = opts.get('authors')
571 opts['authormap'] = opts.get('authors')
572
572
573 if not dest:
573 if not dest:
574 dest = hg.defaultdest(src) + "-hg"
574 dest = hg.defaultdest(src) + "-hg"
575 ui.status(_("assuming destination %s\n") % dest)
575 ui.status(_("assuming destination %s\n") % dest)
576
576
577 destc = convertsink(ui, dest, opts.get('dest_type'))
577 destc = convertsink(ui, dest, opts.get('dest_type'))
578
578
579 try:
579 try:
580 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
580 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
581 opts.get('rev'))
581 opts.get('rev'))
582 except Exception:
582 except Exception:
583 for path in destc.created:
583 for path in destc.created:
584 shutil.rmtree(path, True)
584 shutil.rmtree(path, True)
585 raise
585 raise
586
586
587 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
587 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
588 sortmode = [m for m in sortmodes if opts.get(m)]
588 sortmode = [m for m in sortmodes if opts.get(m)]
589 if len(sortmode) > 1:
589 if len(sortmode) > 1:
590 raise error.Abort(_('more than one sort mode specified'))
590 raise error.Abort(_('more than one sort mode specified'))
591 if sortmode:
591 if sortmode:
592 sortmode = sortmode[0]
592 sortmode = sortmode[0]
593 else:
593 else:
594 sortmode = defaultsort
594 sortmode = defaultsort
595
595
596 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
596 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
597 raise error.Abort(_('--sourcesort is not supported by this data source')
597 raise error.Abort(_('--sourcesort is not supported by this data source')
598 )
598 )
599 if sortmode == 'closesort' and not srcc.hasnativeclose():
599 if sortmode == 'closesort' and not srcc.hasnativeclose():
600 raise error.Abort(_('--closesort is not supported by this data source'))
600 raise error.Abort(_('--closesort is not supported by this data source'))
601
601
602 fmap = opts.get('filemap')
602 fmap = opts.get('filemap')
603 if fmap:
603 if fmap:
604 srcc = filemap.filemap_source(ui, srcc, fmap)
604 srcc = filemap.filemap_source(ui, srcc, fmap)
605 destc.setfilemapmode(True)
605 destc.setfilemapmode(True)
606
606
607 if not revmapfile:
607 if not revmapfile:
608 revmapfile = destc.revmapfile()
608 revmapfile = destc.revmapfile()
609
609
610 c = converter(ui, srcc, destc, revmapfile, opts)
610 c = converter(ui, srcc, destc, revmapfile, opts)
611 c.convert(sortmode)
611 c.convert(sortmode)
@@ -1,296 +1,296
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import errno
9 import errno
10 import os
10 import os
11 import re
11 import re
12 import socket
12 import socket
13
13
14 from mercurial.i18n import _
14 from mercurial import (
15 from mercurial import (
15 encoding,
16 encoding,
16 error,
17 error,
17 util,
18 util,
18 )
19 )
19 from mercurial.i18n import _
20
20
21 from . import (
21 from . import (
22 common,
22 common,
23 cvsps,
23 cvsps,
24 )
24 )
25
25
26 stringio = util.stringio
26 stringio = util.stringio
27 checktool = common.checktool
27 checktool = common.checktool
28 commit = common.commit
28 commit = common.commit
29 converter_source = common.converter_source
29 converter_source = common.converter_source
30 makedatetimestamp = common.makedatetimestamp
30 makedatetimestamp = common.makedatetimestamp
31 NoRepo = common.NoRepo
31 NoRepo = common.NoRepo
32
32
33 class convert_cvs(converter_source):
33 class convert_cvs(converter_source):
34 def __init__(self, ui, path, revs=None):
34 def __init__(self, ui, path, revs=None):
35 super(convert_cvs, self).__init__(ui, path, revs=revs)
35 super(convert_cvs, self).__init__(ui, path, revs=revs)
36
36
37 cvs = os.path.join(path, "CVS")
37 cvs = os.path.join(path, "CVS")
38 if not os.path.exists(cvs):
38 if not os.path.exists(cvs):
39 raise NoRepo(_("%s does not look like a CVS checkout") % path)
39 raise NoRepo(_("%s does not look like a CVS checkout") % path)
40
40
41 checktool('cvs')
41 checktool('cvs')
42
42
43 self.changeset = None
43 self.changeset = None
44 self.files = {}
44 self.files = {}
45 self.tags = {}
45 self.tags = {}
46 self.lastbranch = {}
46 self.lastbranch = {}
47 self.socket = None
47 self.socket = None
48 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
48 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
49 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
49 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
50 self.encoding = encoding.encoding
50 self.encoding = encoding.encoding
51
51
52 self._connect()
52 self._connect()
53
53
54 def _parse(self):
54 def _parse(self):
55 if self.changeset is not None:
55 if self.changeset is not None:
56 return
56 return
57 self.changeset = {}
57 self.changeset = {}
58
58
59 maxrev = 0
59 maxrev = 0
60 if self.revs:
60 if self.revs:
61 if len(self.revs) > 1:
61 if len(self.revs) > 1:
62 raise error.Abort(_('cvs source does not support specifying '
62 raise error.Abort(_('cvs source does not support specifying '
63 'multiple revs'))
63 'multiple revs'))
64 # TODO: handle tags
64 # TODO: handle tags
65 try:
65 try:
66 # patchset number?
66 # patchset number?
67 maxrev = int(self.revs[0])
67 maxrev = int(self.revs[0])
68 except ValueError:
68 except ValueError:
69 raise error.Abort(_('revision %s is not a patchset number')
69 raise error.Abort(_('revision %s is not a patchset number')
70 % self.revs[0])
70 % self.revs[0])
71
71
72 d = os.getcwd()
72 d = os.getcwd()
73 try:
73 try:
74 os.chdir(self.path)
74 os.chdir(self.path)
75 id = None
75 id = None
76
76
77 cache = 'update'
77 cache = 'update'
78 if not self.ui.configbool('convert', 'cvsps.cache', True):
78 if not self.ui.configbool('convert', 'cvsps.cache', True):
79 cache = None
79 cache = None
80 db = cvsps.createlog(self.ui, cache=cache)
80 db = cvsps.createlog(self.ui, cache=cache)
81 db = cvsps.createchangeset(self.ui, db,
81 db = cvsps.createchangeset(self.ui, db,
82 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
82 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
83 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
83 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
84 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
84 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
85
85
86 for cs in db:
86 for cs in db:
87 if maxrev and cs.id > maxrev:
87 if maxrev and cs.id > maxrev:
88 break
88 break
89 id = str(cs.id)
89 id = str(cs.id)
90 cs.author = self.recode(cs.author)
90 cs.author = self.recode(cs.author)
91 self.lastbranch[cs.branch] = id
91 self.lastbranch[cs.branch] = id
92 cs.comment = self.recode(cs.comment)
92 cs.comment = self.recode(cs.comment)
93 if self.ui.configbool('convert', 'localtimezone'):
93 if self.ui.configbool('convert', 'localtimezone'):
94 cs.date = makedatetimestamp(cs.date[0])
94 cs.date = makedatetimestamp(cs.date[0])
95 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
95 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
96 self.tags.update(dict.fromkeys(cs.tags, id))
96 self.tags.update(dict.fromkeys(cs.tags, id))
97
97
98 files = {}
98 files = {}
99 for f in cs.entries:
99 for f in cs.entries:
100 files[f.file] = "%s%s" % ('.'.join([str(x)
100 files[f.file] = "%s%s" % ('.'.join([str(x)
101 for x in f.revision]),
101 for x in f.revision]),
102 ['', '(DEAD)'][f.dead])
102 ['', '(DEAD)'][f.dead])
103
103
104 # add current commit to set
104 # add current commit to set
105 c = commit(author=cs.author, date=date,
105 c = commit(author=cs.author, date=date,
106 parents=[str(p.id) for p in cs.parents],
106 parents=[str(p.id) for p in cs.parents],
107 desc=cs.comment, branch=cs.branch or '')
107 desc=cs.comment, branch=cs.branch or '')
108 self.changeset[id] = c
108 self.changeset[id] = c
109 self.files[id] = files
109 self.files[id] = files
110
110
111 self.heads = self.lastbranch.values()
111 self.heads = self.lastbranch.values()
112 finally:
112 finally:
113 os.chdir(d)
113 os.chdir(d)
114
114
115 def _connect(self):
115 def _connect(self):
116 root = self.cvsroot
116 root = self.cvsroot
117 conntype = None
117 conntype = None
118 user, host = None, None
118 user, host = None, None
119 cmd = ['cvs', 'server']
119 cmd = ['cvs', 'server']
120
120
121 self.ui.status(_("connecting to %s\n") % root)
121 self.ui.status(_("connecting to %s\n") % root)
122
122
123 if root.startswith(":pserver:"):
123 if root.startswith(":pserver:"):
124 root = root[9:]
124 root = root[9:]
125 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
125 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
126 root)
126 root)
127 if m:
127 if m:
128 conntype = "pserver"
128 conntype = "pserver"
129 user, passw, serv, port, root = m.groups()
129 user, passw, serv, port, root = m.groups()
130 if not user:
130 if not user:
131 user = "anonymous"
131 user = "anonymous"
132 if not port:
132 if not port:
133 port = 2401
133 port = 2401
134 else:
134 else:
135 port = int(port)
135 port = int(port)
136 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
136 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
137 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
137 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
138
138
139 if not passw:
139 if not passw:
140 passw = "A"
140 passw = "A"
141 cvspass = os.path.expanduser("~/.cvspass")
141 cvspass = os.path.expanduser("~/.cvspass")
142 try:
142 try:
143 pf = open(cvspass)
143 pf = open(cvspass)
144 for line in pf.read().splitlines():
144 for line in pf.read().splitlines():
145 part1, part2 = line.split(' ', 1)
145 part1, part2 = line.split(' ', 1)
146 # /1 :pserver:user@example.com:2401/cvsroot/foo
146 # /1 :pserver:user@example.com:2401/cvsroot/foo
147 # Ah<Z
147 # Ah<Z
148 if part1 == '/1':
148 if part1 == '/1':
149 part1, part2 = part2.split(' ', 1)
149 part1, part2 = part2.split(' ', 1)
150 format = format1
150 format = format1
151 # :pserver:user@example.com:/cvsroot/foo Ah<Z
151 # :pserver:user@example.com:/cvsroot/foo Ah<Z
152 else:
152 else:
153 format = format0
153 format = format0
154 if part1 == format:
154 if part1 == format:
155 passw = part2
155 passw = part2
156 break
156 break
157 pf.close()
157 pf.close()
158 except IOError as inst:
158 except IOError as inst:
159 if inst.errno != errno.ENOENT:
159 if inst.errno != errno.ENOENT:
160 if not getattr(inst, 'filename', None):
160 if not getattr(inst, 'filename', None):
161 inst.filename = cvspass
161 inst.filename = cvspass
162 raise
162 raise
163
163
164 sck = socket.socket()
164 sck = socket.socket()
165 sck.connect((serv, port))
165 sck.connect((serv, port))
166 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
166 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
167 "END AUTH REQUEST", ""]))
167 "END AUTH REQUEST", ""]))
168 if sck.recv(128) != "I LOVE YOU\n":
168 if sck.recv(128) != "I LOVE YOU\n":
169 raise error.Abort(_("CVS pserver authentication failed"))
169 raise error.Abort(_("CVS pserver authentication failed"))
170
170
171 self.writep = self.readp = sck.makefile('r+')
171 self.writep = self.readp = sck.makefile('r+')
172
172
173 if not conntype and root.startswith(":local:"):
173 if not conntype and root.startswith(":local:"):
174 conntype = "local"
174 conntype = "local"
175 root = root[7:]
175 root = root[7:]
176
176
177 if not conntype:
177 if not conntype:
178 # :ext:user@host/home/user/path/to/cvsroot
178 # :ext:user@host/home/user/path/to/cvsroot
179 if root.startswith(":ext:"):
179 if root.startswith(":ext:"):
180 root = root[5:]
180 root = root[5:]
181 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
181 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
182 # Do not take Windows path "c:\foo\bar" for a connection strings
182 # Do not take Windows path "c:\foo\bar" for a connection strings
183 if os.path.isdir(root) or not m:
183 if os.path.isdir(root) or not m:
184 conntype = "local"
184 conntype = "local"
185 else:
185 else:
186 conntype = "rsh"
186 conntype = "rsh"
187 user, host, root = m.group(1), m.group(2), m.group(3)
187 user, host, root = m.group(1), m.group(2), m.group(3)
188
188
189 if conntype != "pserver":
189 if conntype != "pserver":
190 if conntype == "rsh":
190 if conntype == "rsh":
191 rsh = os.environ.get("CVS_RSH") or "ssh"
191 rsh = os.environ.get("CVS_RSH") or "ssh"
192 if user:
192 if user:
193 cmd = [rsh, '-l', user, host] + cmd
193 cmd = [rsh, '-l', user, host] + cmd
194 else:
194 else:
195 cmd = [rsh, host] + cmd
195 cmd = [rsh, host] + cmd
196
196
197 # popen2 does not support argument lists under Windows
197 # popen2 does not support argument lists under Windows
198 cmd = [util.shellquote(arg) for arg in cmd]
198 cmd = [util.shellquote(arg) for arg in cmd]
199 cmd = util.quotecommand(' '.join(cmd))
199 cmd = util.quotecommand(' '.join(cmd))
200 self.writep, self.readp = util.popen2(cmd)
200 self.writep, self.readp = util.popen2(cmd)
201
201
202 self.realroot = root
202 self.realroot = root
203
203
204 self.writep.write("Root %s\n" % root)
204 self.writep.write("Root %s\n" % root)
205 self.writep.write("Valid-responses ok error Valid-requests Mode"
205 self.writep.write("Valid-responses ok error Valid-requests Mode"
206 " M Mbinary E Checked-in Created Updated"
206 " M Mbinary E Checked-in Created Updated"
207 " Merged Removed\n")
207 " Merged Removed\n")
208 self.writep.write("valid-requests\n")
208 self.writep.write("valid-requests\n")
209 self.writep.flush()
209 self.writep.flush()
210 r = self.readp.readline()
210 r = self.readp.readline()
211 if not r.startswith("Valid-requests"):
211 if not r.startswith("Valid-requests"):
212 raise error.Abort(_('unexpected response from CVS server '
212 raise error.Abort(_('unexpected response from CVS server '
213 '(expected "Valid-requests", but got %r)')
213 '(expected "Valid-requests", but got %r)')
214 % r)
214 % r)
215 if "UseUnchanged" in r:
215 if "UseUnchanged" in r:
216 self.writep.write("UseUnchanged\n")
216 self.writep.write("UseUnchanged\n")
217 self.writep.flush()
217 self.writep.flush()
218 r = self.readp.readline()
218 r = self.readp.readline()
219
219
220 def getheads(self):
220 def getheads(self):
221 self._parse()
221 self._parse()
222 return self.heads
222 return self.heads
223
223
224 def getfile(self, name, rev):
224 def getfile(self, name, rev):
225
225
226 def chunkedread(fp, count):
226 def chunkedread(fp, count):
227 # file-objects returned by socket.makefile() do not handle
227 # file-objects returned by socket.makefile() do not handle
228 # large read() requests very well.
228 # large read() requests very well.
229 chunksize = 65536
229 chunksize = 65536
230 output = stringio()
230 output = stringio()
231 while count > 0:
231 while count > 0:
232 data = fp.read(min(count, chunksize))
232 data = fp.read(min(count, chunksize))
233 if not data:
233 if not data:
234 raise error.Abort(_("%d bytes missing from remote file")
234 raise error.Abort(_("%d bytes missing from remote file")
235 % count)
235 % count)
236 count -= len(data)
236 count -= len(data)
237 output.write(data)
237 output.write(data)
238 return output.getvalue()
238 return output.getvalue()
239
239
240 self._parse()
240 self._parse()
241 if rev.endswith("(DEAD)"):
241 if rev.endswith("(DEAD)"):
242 return None, None
242 return None, None
243
243
244 args = ("-N -P -kk -r %s --" % rev).split()
244 args = ("-N -P -kk -r %s --" % rev).split()
245 args.append(self.cvsrepo + '/' + name)
245 args.append(self.cvsrepo + '/' + name)
246 for x in args:
246 for x in args:
247 self.writep.write("Argument %s\n" % x)
247 self.writep.write("Argument %s\n" % x)
248 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
248 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
249 self.writep.flush()
249 self.writep.flush()
250
250
251 data = ""
251 data = ""
252 mode = None
252 mode = None
253 while True:
253 while True:
254 line = self.readp.readline()
254 line = self.readp.readline()
255 if line.startswith("Created ") or line.startswith("Updated "):
255 if line.startswith("Created ") or line.startswith("Updated "):
256 self.readp.readline() # path
256 self.readp.readline() # path
257 self.readp.readline() # entries
257 self.readp.readline() # entries
258 mode = self.readp.readline()[:-1]
258 mode = self.readp.readline()[:-1]
259 count = int(self.readp.readline()[:-1])
259 count = int(self.readp.readline()[:-1])
260 data = chunkedread(self.readp, count)
260 data = chunkedread(self.readp, count)
261 elif line.startswith(" "):
261 elif line.startswith(" "):
262 data += line[1:]
262 data += line[1:]
263 elif line.startswith("M "):
263 elif line.startswith("M "):
264 pass
264 pass
265 elif line.startswith("Mbinary "):
265 elif line.startswith("Mbinary "):
266 count = int(self.readp.readline()[:-1])
266 count = int(self.readp.readline()[:-1])
267 data = chunkedread(self.readp, count)
267 data = chunkedread(self.readp, count)
268 else:
268 else:
269 if line == "ok\n":
269 if line == "ok\n":
270 if mode is None:
270 if mode is None:
271 raise error.Abort(_('malformed response from CVS'))
271 raise error.Abort(_('malformed response from CVS'))
272 return (data, "x" in mode and "x" or "")
272 return (data, "x" in mode and "x" or "")
273 elif line.startswith("E "):
273 elif line.startswith("E "):
274 self.ui.warn(_("cvs server: %s\n") % line[2:])
274 self.ui.warn(_("cvs server: %s\n") % line[2:])
275 elif line.startswith("Remove"):
275 elif line.startswith("Remove"):
276 self.readp.readline()
276 self.readp.readline()
277 else:
277 else:
278 raise error.Abort(_("unknown CVS response: %s") % line)
278 raise error.Abort(_("unknown CVS response: %s") % line)
279
279
280 def getchanges(self, rev, full):
280 def getchanges(self, rev, full):
281 if full:
281 if full:
282 raise error.Abort(_("convert from cvs does not support --full"))
282 raise error.Abort(_("convert from cvs does not support --full"))
283 self._parse()
283 self._parse()
284 return sorted(self.files[rev].iteritems()), {}, set()
284 return sorted(self.files[rev].iteritems()), {}, set()
285
285
286 def getcommit(self, rev):
286 def getcommit(self, rev):
287 self._parse()
287 self._parse()
288 return self.changeset[rev]
288 return self.changeset[rev]
289
289
290 def gettags(self):
290 def gettags(self):
291 self._parse()
291 self._parse()
292 return self.tags
292 return self.tags
293
293
294 def getchangedfiles(self, rev, i):
294 def getchangedfiles(self, rev, i):
295 self._parse()
295 self._parse()
296 return sorted(self.files[rev])
296 return sorted(self.files[rev])
@@ -1,918 +1,919
1 # Mercurial built-in replacement for cvsps.
1 # Mercurial built-in replacement for cvsps.
2 #
2 #
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import cPickle as pickle
10 import os
9 import os
11 import re
10 import re
12
11
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 hook,
14 hook,
15 util,
15 util,
16 )
16 )
17 from mercurial.i18n import _
17
18 pickle = util.pickle
18
19
19 class logentry(object):
20 class logentry(object):
20 '''Class logentry has the following attributes:
21 '''Class logentry has the following attributes:
21 .author - author name as CVS knows it
22 .author - author name as CVS knows it
22 .branch - name of branch this revision is on
23 .branch - name of branch this revision is on
23 .branches - revision tuple of branches starting at this revision
24 .branches - revision tuple of branches starting at this revision
24 .comment - commit message
25 .comment - commit message
25 .commitid - CVS commitid or None
26 .commitid - CVS commitid or None
26 .date - the commit date as a (time, tz) tuple
27 .date - the commit date as a (time, tz) tuple
27 .dead - true if file revision is dead
28 .dead - true if file revision is dead
28 .file - Name of file
29 .file - Name of file
29 .lines - a tuple (+lines, -lines) or None
30 .lines - a tuple (+lines, -lines) or None
30 .parent - Previous revision of this entry
31 .parent - Previous revision of this entry
31 .rcs - name of file as returned from CVS
32 .rcs - name of file as returned from CVS
32 .revision - revision number as tuple
33 .revision - revision number as tuple
33 .tags - list of tags on the file
34 .tags - list of tags on the file
34 .synthetic - is this a synthetic "file ... added on ..." revision?
35 .synthetic - is this a synthetic "file ... added on ..." revision?
35 .mergepoint - the branch that has been merged from (if present in
36 .mergepoint - the branch that has been merged from (if present in
36 rlog output) or None
37 rlog output) or None
37 .branchpoints - the branches that start at the current entry or empty
38 .branchpoints - the branches that start at the current entry or empty
38 '''
39 '''
39 def __init__(self, **entries):
40 def __init__(self, **entries):
40 self.synthetic = False
41 self.synthetic = False
41 self.__dict__.update(entries)
42 self.__dict__.update(entries)
42
43
43 def __repr__(self):
44 def __repr__(self):
44 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
45 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
45 return "%s(%s)"%(type(self).__name__, ", ".join(items))
46 return "%s(%s)"%(type(self).__name__, ", ".join(items))
46
47
47 class logerror(Exception):
48 class logerror(Exception):
48 pass
49 pass
49
50
50 def getrepopath(cvspath):
51 def getrepopath(cvspath):
51 """Return the repository path from a CVS path.
52 """Return the repository path from a CVS path.
52
53
53 >>> getrepopath('/foo/bar')
54 >>> getrepopath('/foo/bar')
54 '/foo/bar'
55 '/foo/bar'
55 >>> getrepopath('c:/foo/bar')
56 >>> getrepopath('c:/foo/bar')
56 '/foo/bar'
57 '/foo/bar'
57 >>> getrepopath(':pserver:10/foo/bar')
58 >>> getrepopath(':pserver:10/foo/bar')
58 '/foo/bar'
59 '/foo/bar'
59 >>> getrepopath(':pserver:10c:/foo/bar')
60 >>> getrepopath(':pserver:10c:/foo/bar')
60 '/foo/bar'
61 '/foo/bar'
61 >>> getrepopath(':pserver:/foo/bar')
62 >>> getrepopath(':pserver:/foo/bar')
62 '/foo/bar'
63 '/foo/bar'
63 >>> getrepopath(':pserver:c:/foo/bar')
64 >>> getrepopath(':pserver:c:/foo/bar')
64 '/foo/bar'
65 '/foo/bar'
65 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
66 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
66 '/foo/bar'
67 '/foo/bar'
67 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
68 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
68 '/foo/bar'
69 '/foo/bar'
69 >>> getrepopath('user@server/path/to/repository')
70 >>> getrepopath('user@server/path/to/repository')
70 '/path/to/repository'
71 '/path/to/repository'
71 """
72 """
72 # According to CVS manual, CVS paths are expressed like:
73 # According to CVS manual, CVS paths are expressed like:
73 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
74 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
74 #
75 #
75 # CVSpath is splitted into parts and then position of the first occurrence
76 # CVSpath is splitted into parts and then position of the first occurrence
76 # of the '/' char after the '@' is located. The solution is the rest of the
77 # of the '/' char after the '@' is located. The solution is the rest of the
77 # string after that '/' sign including it
78 # string after that '/' sign including it
78
79
79 parts = cvspath.split(':')
80 parts = cvspath.split(':')
80 atposition = parts[-1].find('@')
81 atposition = parts[-1].find('@')
81 start = 0
82 start = 0
82
83
83 if atposition != -1:
84 if atposition != -1:
84 start = atposition
85 start = atposition
85
86
86 repopath = parts[-1][parts[-1].find('/', start):]
87 repopath = parts[-1][parts[-1].find('/', start):]
87 return repopath
88 return repopath
88
89
89 def createlog(ui, directory=None, root="", rlog=True, cache=None):
90 def createlog(ui, directory=None, root="", rlog=True, cache=None):
90 '''Collect the CVS rlog'''
91 '''Collect the CVS rlog'''
91
92
92 # Because we store many duplicate commit log messages, reusing strings
93 # Because we store many duplicate commit log messages, reusing strings
93 # saves a lot of memory and pickle storage space.
94 # saves a lot of memory and pickle storage space.
94 _scache = {}
95 _scache = {}
95 def scache(s):
96 def scache(s):
96 "return a shared version of a string"
97 "return a shared version of a string"
97 return _scache.setdefault(s, s)
98 return _scache.setdefault(s, s)
98
99
99 ui.status(_('collecting CVS rlog\n'))
100 ui.status(_('collecting CVS rlog\n'))
100
101
101 log = [] # list of logentry objects containing the CVS state
102 log = [] # list of logentry objects containing the CVS state
102
103
103 # patterns to match in CVS (r)log output, by state of use
104 # patterns to match in CVS (r)log output, by state of use
104 re_00 = re.compile('RCS file: (.+)$')
105 re_00 = re.compile('RCS file: (.+)$')
105 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
106 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
106 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
107 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
107 re_03 = re.compile("(Cannot access.+CVSROOT)|"
108 re_03 = re.compile("(Cannot access.+CVSROOT)|"
108 "(can't create temporary directory.+)$")
109 "(can't create temporary directory.+)$")
109 re_10 = re.compile('Working file: (.+)$')
110 re_10 = re.compile('Working file: (.+)$')
110 re_20 = re.compile('symbolic names:')
111 re_20 = re.compile('symbolic names:')
111 re_30 = re.compile('\t(.+): ([\\d.]+)$')
112 re_30 = re.compile('\t(.+): ([\\d.]+)$')
112 re_31 = re.compile('----------------------------$')
113 re_31 = re.compile('----------------------------$')
113 re_32 = re.compile('======================================='
114 re_32 = re.compile('======================================='
114 '======================================$')
115 '======================================$')
115 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
116 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
116 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
117 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
117 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
118 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
118 r'(\s+commitid:\s+([^;]+);)?'
119 r'(\s+commitid:\s+([^;]+);)?'
119 r'(.*mergepoint:\s+([^;]+);)?')
120 r'(.*mergepoint:\s+([^;]+);)?')
120 re_70 = re.compile('branches: (.+);$')
121 re_70 = re.compile('branches: (.+);$')
121
122
122 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
123 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
123
124
124 prefix = '' # leading path to strip of what we get from CVS
125 prefix = '' # leading path to strip of what we get from CVS
125
126
126 if directory is None:
127 if directory is None:
127 # Current working directory
128 # Current working directory
128
129
129 # Get the real directory in the repository
130 # Get the real directory in the repository
130 try:
131 try:
131 prefix = open(os.path.join('CVS','Repository')).read().strip()
132 prefix = open(os.path.join('CVS','Repository')).read().strip()
132 directory = prefix
133 directory = prefix
133 if prefix == ".":
134 if prefix == ".":
134 prefix = ""
135 prefix = ""
135 except IOError:
136 except IOError:
136 raise logerror(_('not a CVS sandbox'))
137 raise logerror(_('not a CVS sandbox'))
137
138
138 if prefix and not prefix.endswith(os.sep):
139 if prefix and not prefix.endswith(os.sep):
139 prefix += os.sep
140 prefix += os.sep
140
141
141 # Use the Root file in the sandbox, if it exists
142 # Use the Root file in the sandbox, if it exists
142 try:
143 try:
143 root = open(os.path.join('CVS','Root')).read().strip()
144 root = open(os.path.join('CVS','Root')).read().strip()
144 except IOError:
145 except IOError:
145 pass
146 pass
146
147
147 if not root:
148 if not root:
148 root = os.environ.get('CVSROOT', '')
149 root = os.environ.get('CVSROOT', '')
149
150
150 # read log cache if one exists
151 # read log cache if one exists
151 oldlog = []
152 oldlog = []
152 date = None
153 date = None
153
154
154 if cache:
155 if cache:
155 cachedir = os.path.expanduser('~/.hg.cvsps')
156 cachedir = os.path.expanduser('~/.hg.cvsps')
156 if not os.path.exists(cachedir):
157 if not os.path.exists(cachedir):
157 os.mkdir(cachedir)
158 os.mkdir(cachedir)
158
159
159 # The cvsps cache pickle needs a uniquified name, based on the
160 # The cvsps cache pickle needs a uniquified name, based on the
160 # repository location. The address may have all sort of nasties
161 # repository location. The address may have all sort of nasties
161 # in it, slashes, colons and such. So here we take just the
162 # in it, slashes, colons and such. So here we take just the
162 # alphanumeric characters, concatenated in a way that does not
163 # alphanumeric characters, concatenated in a way that does not
163 # mix up the various components, so that
164 # mix up the various components, so that
164 # :pserver:user@server:/path
165 # :pserver:user@server:/path
165 # and
166 # and
166 # /pserver/user/server/path
167 # /pserver/user/server/path
167 # are mapped to different cache file names.
168 # are mapped to different cache file names.
168 cachefile = root.split(":") + [directory, "cache"]
169 cachefile = root.split(":") + [directory, "cache"]
169 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
170 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
170 cachefile = os.path.join(cachedir,
171 cachefile = os.path.join(cachedir,
171 '.'.join([s for s in cachefile if s]))
172 '.'.join([s for s in cachefile if s]))
172
173
173 if cache == 'update':
174 if cache == 'update':
174 try:
175 try:
175 ui.note(_('reading cvs log cache %s\n') % cachefile)
176 ui.note(_('reading cvs log cache %s\n') % cachefile)
176 oldlog = pickle.load(open(cachefile))
177 oldlog = pickle.load(open(cachefile))
177 for e in oldlog:
178 for e in oldlog:
178 if not (util.safehasattr(e, 'branchpoints') and
179 if not (util.safehasattr(e, 'branchpoints') and
179 util.safehasattr(e, 'commitid') and
180 util.safehasattr(e, 'commitid') and
180 util.safehasattr(e, 'mergepoint')):
181 util.safehasattr(e, 'mergepoint')):
181 ui.status(_('ignoring old cache\n'))
182 ui.status(_('ignoring old cache\n'))
182 oldlog = []
183 oldlog = []
183 break
184 break
184
185
185 ui.note(_('cache has %d log entries\n') % len(oldlog))
186 ui.note(_('cache has %d log entries\n') % len(oldlog))
186 except Exception as e:
187 except Exception as e:
187 ui.note(_('error reading cache: %r\n') % e)
188 ui.note(_('error reading cache: %r\n') % e)
188
189
189 if oldlog:
190 if oldlog:
190 date = oldlog[-1].date # last commit date as a (time,tz) tuple
191 date = oldlog[-1].date # last commit date as a (time,tz) tuple
191 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
192 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
192
193
193 # build the CVS commandline
194 # build the CVS commandline
194 cmd = ['cvs', '-q']
195 cmd = ['cvs', '-q']
195 if root:
196 if root:
196 cmd.append('-d%s' % root)
197 cmd.append('-d%s' % root)
197 p = util.normpath(getrepopath(root))
198 p = util.normpath(getrepopath(root))
198 if not p.endswith('/'):
199 if not p.endswith('/'):
199 p += '/'
200 p += '/'
200 if prefix:
201 if prefix:
201 # looks like normpath replaces "" by "."
202 # looks like normpath replaces "" by "."
202 prefix = p + util.normpath(prefix)
203 prefix = p + util.normpath(prefix)
203 else:
204 else:
204 prefix = p
205 prefix = p
205 cmd.append(['log', 'rlog'][rlog])
206 cmd.append(['log', 'rlog'][rlog])
206 if date:
207 if date:
207 # no space between option and date string
208 # no space between option and date string
208 cmd.append('-d>%s' % date)
209 cmd.append('-d>%s' % date)
209 cmd.append(directory)
210 cmd.append(directory)
210
211
211 # state machine begins here
212 # state machine begins here
212 tags = {} # dictionary of revisions on current file with their tags
213 tags = {} # dictionary of revisions on current file with their tags
213 branchmap = {} # mapping between branch names and revision numbers
214 branchmap = {} # mapping between branch names and revision numbers
214 rcsmap = {}
215 rcsmap = {}
215 state = 0
216 state = 0
216 store = False # set when a new record can be appended
217 store = False # set when a new record can be appended
217
218
218 cmd = [util.shellquote(arg) for arg in cmd]
219 cmd = [util.shellquote(arg) for arg in cmd]
219 ui.note(_("running %s\n") % (' '.join(cmd)))
220 ui.note(_("running %s\n") % (' '.join(cmd)))
220 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
221 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
221
222
222 pfp = util.popen(' '.join(cmd))
223 pfp = util.popen(' '.join(cmd))
223 peek = pfp.readline()
224 peek = pfp.readline()
224 while True:
225 while True:
225 line = peek
226 line = peek
226 if line == '':
227 if line == '':
227 break
228 break
228 peek = pfp.readline()
229 peek = pfp.readline()
229 if line.endswith('\n'):
230 if line.endswith('\n'):
230 line = line[:-1]
231 line = line[:-1]
231 #ui.debug('state=%d line=%r\n' % (state, line))
232 #ui.debug('state=%d line=%r\n' % (state, line))
232
233
233 if state == 0:
234 if state == 0:
234 # initial state, consume input until we see 'RCS file'
235 # initial state, consume input until we see 'RCS file'
235 match = re_00.match(line)
236 match = re_00.match(line)
236 if match:
237 if match:
237 rcs = match.group(1)
238 rcs = match.group(1)
238 tags = {}
239 tags = {}
239 if rlog:
240 if rlog:
240 filename = util.normpath(rcs[:-2])
241 filename = util.normpath(rcs[:-2])
241 if filename.startswith(prefix):
242 if filename.startswith(prefix):
242 filename = filename[len(prefix):]
243 filename = filename[len(prefix):]
243 if filename.startswith('/'):
244 if filename.startswith('/'):
244 filename = filename[1:]
245 filename = filename[1:]
245 if filename.startswith('Attic/'):
246 if filename.startswith('Attic/'):
246 filename = filename[6:]
247 filename = filename[6:]
247 else:
248 else:
248 filename = filename.replace('/Attic/', '/')
249 filename = filename.replace('/Attic/', '/')
249 state = 2
250 state = 2
250 continue
251 continue
251 state = 1
252 state = 1
252 continue
253 continue
253 match = re_01.match(line)
254 match = re_01.match(line)
254 if match:
255 if match:
255 raise logerror(match.group(1))
256 raise logerror(match.group(1))
256 match = re_02.match(line)
257 match = re_02.match(line)
257 if match:
258 if match:
258 raise logerror(match.group(2))
259 raise logerror(match.group(2))
259 if re_03.match(line):
260 if re_03.match(line):
260 raise logerror(line)
261 raise logerror(line)
261
262
262 elif state == 1:
263 elif state == 1:
263 # expect 'Working file' (only when using log instead of rlog)
264 # expect 'Working file' (only when using log instead of rlog)
264 match = re_10.match(line)
265 match = re_10.match(line)
265 assert match, _('RCS file must be followed by working file')
266 assert match, _('RCS file must be followed by working file')
266 filename = util.normpath(match.group(1))
267 filename = util.normpath(match.group(1))
267 state = 2
268 state = 2
268
269
269 elif state == 2:
270 elif state == 2:
270 # expect 'symbolic names'
271 # expect 'symbolic names'
271 if re_20.match(line):
272 if re_20.match(line):
272 branchmap = {}
273 branchmap = {}
273 state = 3
274 state = 3
274
275
275 elif state == 3:
276 elif state == 3:
276 # read the symbolic names and store as tags
277 # read the symbolic names and store as tags
277 match = re_30.match(line)
278 match = re_30.match(line)
278 if match:
279 if match:
279 rev = [int(x) for x in match.group(2).split('.')]
280 rev = [int(x) for x in match.group(2).split('.')]
280
281
281 # Convert magic branch number to an odd-numbered one
282 # Convert magic branch number to an odd-numbered one
282 revn = len(rev)
283 revn = len(rev)
283 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
284 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
284 rev = rev[:-2] + rev[-1:]
285 rev = rev[:-2] + rev[-1:]
285 rev = tuple(rev)
286 rev = tuple(rev)
286
287
287 if rev not in tags:
288 if rev not in tags:
288 tags[rev] = []
289 tags[rev] = []
289 tags[rev].append(match.group(1))
290 tags[rev].append(match.group(1))
290 branchmap[match.group(1)] = match.group(2)
291 branchmap[match.group(1)] = match.group(2)
291
292
292 elif re_31.match(line):
293 elif re_31.match(line):
293 state = 5
294 state = 5
294 elif re_32.match(line):
295 elif re_32.match(line):
295 state = 0
296 state = 0
296
297
297 elif state == 4:
298 elif state == 4:
298 # expecting '------' separator before first revision
299 # expecting '------' separator before first revision
299 if re_31.match(line):
300 if re_31.match(line):
300 state = 5
301 state = 5
301 else:
302 else:
302 assert not re_32.match(line), _('must have at least '
303 assert not re_32.match(line), _('must have at least '
303 'some revisions')
304 'some revisions')
304
305
305 elif state == 5:
306 elif state == 5:
306 # expecting revision number and possibly (ignored) lock indication
307 # expecting revision number and possibly (ignored) lock indication
307 # we create the logentry here from values stored in states 0 to 4,
308 # we create the logentry here from values stored in states 0 to 4,
308 # as this state is re-entered for subsequent revisions of a file.
309 # as this state is re-entered for subsequent revisions of a file.
309 match = re_50.match(line)
310 match = re_50.match(line)
310 assert match, _('expected revision number')
311 assert match, _('expected revision number')
311 e = logentry(rcs=scache(rcs),
312 e = logentry(rcs=scache(rcs),
312 file=scache(filename),
313 file=scache(filename),
313 revision=tuple([int(x) for x in
314 revision=tuple([int(x) for x in
314 match.group(1).split('.')]),
315 match.group(1).split('.')]),
315 branches=[],
316 branches=[],
316 parent=None,
317 parent=None,
317 commitid=None,
318 commitid=None,
318 mergepoint=None,
319 mergepoint=None,
319 branchpoints=set())
320 branchpoints=set())
320
321
321 state = 6
322 state = 6
322
323
323 elif state == 6:
324 elif state == 6:
324 # expecting date, author, state, lines changed
325 # expecting date, author, state, lines changed
325 match = re_60.match(line)
326 match = re_60.match(line)
326 assert match, _('revision must be followed by date line')
327 assert match, _('revision must be followed by date line')
327 d = match.group(1)
328 d = match.group(1)
328 if d[2] == '/':
329 if d[2] == '/':
329 # Y2K
330 # Y2K
330 d = '19' + d
331 d = '19' + d
331
332
332 if len(d.split()) != 3:
333 if len(d.split()) != 3:
333 # cvs log dates always in GMT
334 # cvs log dates always in GMT
334 d = d + ' UTC'
335 d = d + ' UTC'
335 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
336 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
336 '%Y/%m/%d %H:%M:%S',
337 '%Y/%m/%d %H:%M:%S',
337 '%Y-%m-%d %H:%M:%S'])
338 '%Y-%m-%d %H:%M:%S'])
338 e.author = scache(match.group(2))
339 e.author = scache(match.group(2))
339 e.dead = match.group(3).lower() == 'dead'
340 e.dead = match.group(3).lower() == 'dead'
340
341
341 if match.group(5):
342 if match.group(5):
342 if match.group(6):
343 if match.group(6):
343 e.lines = (int(match.group(5)), int(match.group(6)))
344 e.lines = (int(match.group(5)), int(match.group(6)))
344 else:
345 else:
345 e.lines = (int(match.group(5)), 0)
346 e.lines = (int(match.group(5)), 0)
346 elif match.group(6):
347 elif match.group(6):
347 e.lines = (0, int(match.group(6)))
348 e.lines = (0, int(match.group(6)))
348 else:
349 else:
349 e.lines = None
350 e.lines = None
350
351
351 if match.group(7): # cvs 1.12 commitid
352 if match.group(7): # cvs 1.12 commitid
352 e.commitid = match.group(8)
353 e.commitid = match.group(8)
353
354
354 if match.group(9): # cvsnt mergepoint
355 if match.group(9): # cvsnt mergepoint
355 myrev = match.group(10).split('.')
356 myrev = match.group(10).split('.')
356 if len(myrev) == 2: # head
357 if len(myrev) == 2: # head
357 e.mergepoint = 'HEAD'
358 e.mergepoint = 'HEAD'
358 else:
359 else:
359 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
360 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
360 branches = [b for b in branchmap if branchmap[b] == myrev]
361 branches = [b for b in branchmap if branchmap[b] == myrev]
361 assert len(branches) == 1, ('unknown branch: %s'
362 assert len(branches) == 1, ('unknown branch: %s'
362 % e.mergepoint)
363 % e.mergepoint)
363 e.mergepoint = branches[0]
364 e.mergepoint = branches[0]
364
365
365 e.comment = []
366 e.comment = []
366 state = 7
367 state = 7
367
368
368 elif state == 7:
369 elif state == 7:
369 # read the revision numbers of branches that start at this revision
370 # read the revision numbers of branches that start at this revision
370 # or store the commit log message otherwise
371 # or store the commit log message otherwise
371 m = re_70.match(line)
372 m = re_70.match(line)
372 if m:
373 if m:
373 e.branches = [tuple([int(y) for y in x.strip().split('.')])
374 e.branches = [tuple([int(y) for y in x.strip().split('.')])
374 for x in m.group(1).split(';')]
375 for x in m.group(1).split(';')]
375 state = 8
376 state = 8
376 elif re_31.match(line) and re_50.match(peek):
377 elif re_31.match(line) and re_50.match(peek):
377 state = 5
378 state = 5
378 store = True
379 store = True
379 elif re_32.match(line):
380 elif re_32.match(line):
380 state = 0
381 state = 0
381 store = True
382 store = True
382 else:
383 else:
383 e.comment.append(line)
384 e.comment.append(line)
384
385
385 elif state == 8:
386 elif state == 8:
386 # store commit log message
387 # store commit log message
387 if re_31.match(line):
388 if re_31.match(line):
388 cpeek = peek
389 cpeek = peek
389 if cpeek.endswith('\n'):
390 if cpeek.endswith('\n'):
390 cpeek = cpeek[:-1]
391 cpeek = cpeek[:-1]
391 if re_50.match(cpeek):
392 if re_50.match(cpeek):
392 state = 5
393 state = 5
393 store = True
394 store = True
394 else:
395 else:
395 e.comment.append(line)
396 e.comment.append(line)
396 elif re_32.match(line):
397 elif re_32.match(line):
397 state = 0
398 state = 0
398 store = True
399 store = True
399 else:
400 else:
400 e.comment.append(line)
401 e.comment.append(line)
401
402
402 # When a file is added on a branch B1, CVS creates a synthetic
403 # When a file is added on a branch B1, CVS creates a synthetic
403 # dead trunk revision 1.1 so that the branch has a root.
404 # dead trunk revision 1.1 so that the branch has a root.
404 # Likewise, if you merge such a file to a later branch B2 (one
405 # Likewise, if you merge such a file to a later branch B2 (one
405 # that already existed when the file was added on B1), CVS
406 # that already existed when the file was added on B1), CVS
406 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
407 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
407 # these revisions now, but mark them synthetic so
408 # these revisions now, but mark them synthetic so
408 # createchangeset() can take care of them.
409 # createchangeset() can take care of them.
409 if (store and
410 if (store and
410 e.dead and
411 e.dead and
411 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
412 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
412 len(e.comment) == 1 and
413 len(e.comment) == 1 and
413 file_added_re.match(e.comment[0])):
414 file_added_re.match(e.comment[0])):
414 ui.debug('found synthetic revision in %s: %r\n'
415 ui.debug('found synthetic revision in %s: %r\n'
415 % (e.rcs, e.comment[0]))
416 % (e.rcs, e.comment[0]))
416 e.synthetic = True
417 e.synthetic = True
417
418
418 if store:
419 if store:
419 # clean up the results and save in the log.
420 # clean up the results and save in the log.
420 store = False
421 store = False
421 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
422 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
422 e.comment = scache('\n'.join(e.comment))
423 e.comment = scache('\n'.join(e.comment))
423
424
424 revn = len(e.revision)
425 revn = len(e.revision)
425 if revn > 3 and (revn % 2) == 0:
426 if revn > 3 and (revn % 2) == 0:
426 e.branch = tags.get(e.revision[:-1], [None])[0]
427 e.branch = tags.get(e.revision[:-1], [None])[0]
427 else:
428 else:
428 e.branch = None
429 e.branch = None
429
430
430 # find the branches starting from this revision
431 # find the branches starting from this revision
431 branchpoints = set()
432 branchpoints = set()
432 for branch, revision in branchmap.iteritems():
433 for branch, revision in branchmap.iteritems():
433 revparts = tuple([int(i) for i in revision.split('.')])
434 revparts = tuple([int(i) for i in revision.split('.')])
434 if len(revparts) < 2: # bad tags
435 if len(revparts) < 2: # bad tags
435 continue
436 continue
436 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
437 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
437 # normal branch
438 # normal branch
438 if revparts[:-2] == e.revision:
439 if revparts[:-2] == e.revision:
439 branchpoints.add(branch)
440 branchpoints.add(branch)
440 elif revparts == (1, 1, 1): # vendor branch
441 elif revparts == (1, 1, 1): # vendor branch
441 if revparts in e.branches:
442 if revparts in e.branches:
442 branchpoints.add(branch)
443 branchpoints.add(branch)
443 e.branchpoints = branchpoints
444 e.branchpoints = branchpoints
444
445
445 log.append(e)
446 log.append(e)
446
447
447 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
448 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
448
449
449 if len(log) % 100 == 0:
450 if len(log) % 100 == 0:
450 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
451 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
451
452
452 log.sort(key=lambda x: (x.rcs, x.revision))
453 log.sort(key=lambda x: (x.rcs, x.revision))
453
454
454 # find parent revisions of individual files
455 # find parent revisions of individual files
455 versions = {}
456 versions = {}
456 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
457 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
457 rcs = e.rcs.replace('/Attic/', '/')
458 rcs = e.rcs.replace('/Attic/', '/')
458 if rcs in rcsmap:
459 if rcs in rcsmap:
459 e.rcs = rcsmap[rcs]
460 e.rcs = rcsmap[rcs]
460 branch = e.revision[:-1]
461 branch = e.revision[:-1]
461 versions[(e.rcs, branch)] = e.revision
462 versions[(e.rcs, branch)] = e.revision
462
463
463 for e in log:
464 for e in log:
464 branch = e.revision[:-1]
465 branch = e.revision[:-1]
465 p = versions.get((e.rcs, branch), None)
466 p = versions.get((e.rcs, branch), None)
466 if p is None:
467 if p is None:
467 p = e.revision[:-2]
468 p = e.revision[:-2]
468 e.parent = p
469 e.parent = p
469 versions[(e.rcs, branch)] = e.revision
470 versions[(e.rcs, branch)] = e.revision
470
471
471 # update the log cache
472 # update the log cache
472 if cache:
473 if cache:
473 if log:
474 if log:
474 # join up the old and new logs
475 # join up the old and new logs
475 log.sort(key=lambda x: x.date)
476 log.sort(key=lambda x: x.date)
476
477
477 if oldlog and oldlog[-1].date >= log[0].date:
478 if oldlog and oldlog[-1].date >= log[0].date:
478 raise logerror(_('log cache overlaps with new log entries,'
479 raise logerror(_('log cache overlaps with new log entries,'
479 ' re-run without cache.'))
480 ' re-run without cache.'))
480
481
481 log = oldlog + log
482 log = oldlog + log
482
483
483 # write the new cachefile
484 # write the new cachefile
484 ui.note(_('writing cvs log cache %s\n') % cachefile)
485 ui.note(_('writing cvs log cache %s\n') % cachefile)
485 pickle.dump(log, open(cachefile, 'w'))
486 pickle.dump(log, open(cachefile, 'w'))
486 else:
487 else:
487 log = oldlog
488 log = oldlog
488
489
489 ui.status(_('%d log entries\n') % len(log))
490 ui.status(_('%d log entries\n') % len(log))
490
491
491 hook.hook(ui, None, "cvslog", True, log=log)
492 hook.hook(ui, None, "cvslog", True, log=log)
492
493
493 return log
494 return log
494
495
495
496
496 class changeset(object):
497 class changeset(object):
497 '''Class changeset has the following attributes:
498 '''Class changeset has the following attributes:
498 .id - integer identifying this changeset (list index)
499 .id - integer identifying this changeset (list index)
499 .author - author name as CVS knows it
500 .author - author name as CVS knows it
500 .branch - name of branch this changeset is on, or None
501 .branch - name of branch this changeset is on, or None
501 .comment - commit message
502 .comment - commit message
502 .commitid - CVS commitid or None
503 .commitid - CVS commitid or None
503 .date - the commit date as a (time,tz) tuple
504 .date - the commit date as a (time,tz) tuple
504 .entries - list of logentry objects in this changeset
505 .entries - list of logentry objects in this changeset
505 .parents - list of one or two parent changesets
506 .parents - list of one or two parent changesets
506 .tags - list of tags on this changeset
507 .tags - list of tags on this changeset
507 .synthetic - from synthetic revision "file ... added on branch ..."
508 .synthetic - from synthetic revision "file ... added on branch ..."
508 .mergepoint- the branch that has been merged from or None
509 .mergepoint- the branch that has been merged from or None
509 .branchpoints- the branches that start at the current entry or empty
510 .branchpoints- the branches that start at the current entry or empty
510 '''
511 '''
511 def __init__(self, **entries):
512 def __init__(self, **entries):
512 self.id = None
513 self.id = None
513 self.synthetic = False
514 self.synthetic = False
514 self.__dict__.update(entries)
515 self.__dict__.update(entries)
515
516
516 def __repr__(self):
517 def __repr__(self):
517 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
518 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
518 return "%s(%s)"%(type(self).__name__, ", ".join(items))
519 return "%s(%s)"%(type(self).__name__, ", ".join(items))
519
520
520 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
521 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
521 '''Convert log into changesets.'''
522 '''Convert log into changesets.'''
522
523
523 ui.status(_('creating changesets\n'))
524 ui.status(_('creating changesets\n'))
524
525
525 # try to order commitids by date
526 # try to order commitids by date
526 mindate = {}
527 mindate = {}
527 for e in log:
528 for e in log:
528 if e.commitid:
529 if e.commitid:
529 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
530 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
530
531
531 # Merge changesets
532 # Merge changesets
532 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
533 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
533 x.author, x.branch, x.date, x.branchpoints))
534 x.author, x.branch, x.date, x.branchpoints))
534
535
535 changesets = []
536 changesets = []
536 files = set()
537 files = set()
537 c = None
538 c = None
538 for i, e in enumerate(log):
539 for i, e in enumerate(log):
539
540
540 # Check if log entry belongs to the current changeset or not.
541 # Check if log entry belongs to the current changeset or not.
541
542
542 # Since CVS is file-centric, two different file revisions with
543 # Since CVS is file-centric, two different file revisions with
543 # different branchpoints should be treated as belonging to two
544 # different branchpoints should be treated as belonging to two
544 # different changesets (and the ordering is important and not
545 # different changesets (and the ordering is important and not
545 # honoured by cvsps at this point).
546 # honoured by cvsps at this point).
546 #
547 #
547 # Consider the following case:
548 # Consider the following case:
548 # foo 1.1 branchpoints: [MYBRANCH]
549 # foo 1.1 branchpoints: [MYBRANCH]
549 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
550 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
550 #
551 #
551 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
552 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
552 # later version of foo may be in MYBRANCH2, so foo should be the
553 # later version of foo may be in MYBRANCH2, so foo should be the
553 # first changeset and bar the next and MYBRANCH and MYBRANCH2
554 # first changeset and bar the next and MYBRANCH and MYBRANCH2
554 # should both start off of the bar changeset. No provisions are
555 # should both start off of the bar changeset. No provisions are
555 # made to ensure that this is, in fact, what happens.
556 # made to ensure that this is, in fact, what happens.
556 if not (c and e.branchpoints == c.branchpoints and
557 if not (c and e.branchpoints == c.branchpoints and
557 (# cvs commitids
558 (# cvs commitids
558 (e.commitid is not None and e.commitid == c.commitid) or
559 (e.commitid is not None and e.commitid == c.commitid) or
559 (# no commitids, use fuzzy commit detection
560 (# no commitids, use fuzzy commit detection
560 (e.commitid is None or c.commitid is None) and
561 (e.commitid is None or c.commitid is None) and
561 e.comment == c.comment and
562 e.comment == c.comment and
562 e.author == c.author and
563 e.author == c.author and
563 e.branch == c.branch and
564 e.branch == c.branch and
564 ((c.date[0] + c.date[1]) <=
565 ((c.date[0] + c.date[1]) <=
565 (e.date[0] + e.date[1]) <=
566 (e.date[0] + e.date[1]) <=
566 (c.date[0] + c.date[1]) + fuzz) and
567 (c.date[0] + c.date[1]) + fuzz) and
567 e.file not in files))):
568 e.file not in files))):
568 c = changeset(comment=e.comment, author=e.author,
569 c = changeset(comment=e.comment, author=e.author,
569 branch=e.branch, date=e.date,
570 branch=e.branch, date=e.date,
570 entries=[], mergepoint=e.mergepoint,
571 entries=[], mergepoint=e.mergepoint,
571 branchpoints=e.branchpoints, commitid=e.commitid)
572 branchpoints=e.branchpoints, commitid=e.commitid)
572 changesets.append(c)
573 changesets.append(c)
573
574
574 files = set()
575 files = set()
575 if len(changesets) % 100 == 0:
576 if len(changesets) % 100 == 0:
576 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
577 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
577 ui.status(util.ellipsis(t, 80) + '\n')
578 ui.status(util.ellipsis(t, 80) + '\n')
578
579
579 c.entries.append(e)
580 c.entries.append(e)
580 files.add(e.file)
581 files.add(e.file)
581 c.date = e.date # changeset date is date of latest commit in it
582 c.date = e.date # changeset date is date of latest commit in it
582
583
583 # Mark synthetic changesets
584 # Mark synthetic changesets
584
585
585 for c in changesets:
586 for c in changesets:
586 # Synthetic revisions always get their own changeset, because
587 # Synthetic revisions always get their own changeset, because
587 # the log message includes the filename. E.g. if you add file3
588 # the log message includes the filename. E.g. if you add file3
588 # and file4 on a branch, you get four log entries and three
589 # and file4 on a branch, you get four log entries and three
589 # changesets:
590 # changesets:
590 # "File file3 was added on branch ..." (synthetic, 1 entry)
591 # "File file3 was added on branch ..." (synthetic, 1 entry)
591 # "File file4 was added on branch ..." (synthetic, 1 entry)
592 # "File file4 was added on branch ..." (synthetic, 1 entry)
592 # "Add file3 and file4 to fix ..." (real, 2 entries)
593 # "Add file3 and file4 to fix ..." (real, 2 entries)
593 # Hence the check for 1 entry here.
594 # Hence the check for 1 entry here.
594 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
595 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
595
596
596 # Sort files in each changeset
597 # Sort files in each changeset
597
598
598 def entitycompare(l, r):
599 def entitycompare(l, r):
599 'Mimic cvsps sorting order'
600 'Mimic cvsps sorting order'
600 l = l.file.split('/')
601 l = l.file.split('/')
601 r = r.file.split('/')
602 r = r.file.split('/')
602 nl = len(l)
603 nl = len(l)
603 nr = len(r)
604 nr = len(r)
604 n = min(nl, nr)
605 n = min(nl, nr)
605 for i in range(n):
606 for i in range(n):
606 if i + 1 == nl and nl < nr:
607 if i + 1 == nl and nl < nr:
607 return -1
608 return -1
608 elif i + 1 == nr and nl > nr:
609 elif i + 1 == nr and nl > nr:
609 return +1
610 return +1
610 elif l[i] < r[i]:
611 elif l[i] < r[i]:
611 return -1
612 return -1
612 elif l[i] > r[i]:
613 elif l[i] > r[i]:
613 return +1
614 return +1
614 return 0
615 return 0
615
616
616 for c in changesets:
617 for c in changesets:
617 c.entries.sort(entitycompare)
618 c.entries.sort(entitycompare)
618
619
619 # Sort changesets by date
620 # Sort changesets by date
620
621
621 odd = set()
622 odd = set()
622 def cscmp(l, r, odd=odd):
623 def cscmp(l, r, odd=odd):
623 d = sum(l.date) - sum(r.date)
624 d = sum(l.date) - sum(r.date)
624 if d:
625 if d:
625 return d
626 return d
626
627
627 # detect vendor branches and initial commits on a branch
628 # detect vendor branches and initial commits on a branch
628 le = {}
629 le = {}
629 for e in l.entries:
630 for e in l.entries:
630 le[e.rcs] = e.revision
631 le[e.rcs] = e.revision
631 re = {}
632 re = {}
632 for e in r.entries:
633 for e in r.entries:
633 re[e.rcs] = e.revision
634 re[e.rcs] = e.revision
634
635
635 d = 0
636 d = 0
636 for e in l.entries:
637 for e in l.entries:
637 if re.get(e.rcs, None) == e.parent:
638 if re.get(e.rcs, None) == e.parent:
638 assert not d
639 assert not d
639 d = 1
640 d = 1
640 break
641 break
641
642
642 for e in r.entries:
643 for e in r.entries:
643 if le.get(e.rcs, None) == e.parent:
644 if le.get(e.rcs, None) == e.parent:
644 if d:
645 if d:
645 odd.add((l, r))
646 odd.add((l, r))
646 d = -1
647 d = -1
647 break
648 break
648 # By this point, the changesets are sufficiently compared that
649 # By this point, the changesets are sufficiently compared that
649 # we don't really care about ordering. However, this leaves
650 # we don't really care about ordering. However, this leaves
650 # some race conditions in the tests, so we compare on the
651 # some race conditions in the tests, so we compare on the
651 # number of files modified, the files contained in each
652 # number of files modified, the files contained in each
652 # changeset, and the branchpoints in the change to ensure test
653 # changeset, and the branchpoints in the change to ensure test
653 # output remains stable.
654 # output remains stable.
654
655
655 # recommended replacement for cmp from
656 # recommended replacement for cmp from
656 # https://docs.python.org/3.0/whatsnew/3.0.html
657 # https://docs.python.org/3.0/whatsnew/3.0.html
657 c = lambda x, y: (x > y) - (x < y)
658 c = lambda x, y: (x > y) - (x < y)
658 # Sort bigger changes first.
659 # Sort bigger changes first.
659 if not d:
660 if not d:
660 d = c(len(l.entries), len(r.entries))
661 d = c(len(l.entries), len(r.entries))
661 # Try sorting by filename in the change.
662 # Try sorting by filename in the change.
662 if not d:
663 if not d:
663 d = c([e.file for e in l.entries], [e.file for e in r.entries])
664 d = c([e.file for e in l.entries], [e.file for e in r.entries])
664 # Try and put changes without a branch point before ones with
665 # Try and put changes without a branch point before ones with
665 # a branch point.
666 # a branch point.
666 if not d:
667 if not d:
667 d = c(len(l.branchpoints), len(r.branchpoints))
668 d = c(len(l.branchpoints), len(r.branchpoints))
668 return d
669 return d
669
670
670 changesets.sort(cscmp)
671 changesets.sort(cscmp)
671
672
672 # Collect tags
673 # Collect tags
673
674
674 globaltags = {}
675 globaltags = {}
675 for c in changesets:
676 for c in changesets:
676 for e in c.entries:
677 for e in c.entries:
677 for tag in e.tags:
678 for tag in e.tags:
678 # remember which is the latest changeset to have this tag
679 # remember which is the latest changeset to have this tag
679 globaltags[tag] = c
680 globaltags[tag] = c
680
681
681 for c in changesets:
682 for c in changesets:
682 tags = set()
683 tags = set()
683 for e in c.entries:
684 for e in c.entries:
684 tags.update(e.tags)
685 tags.update(e.tags)
685 # remember tags only if this is the latest changeset to have it
686 # remember tags only if this is the latest changeset to have it
686 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
687 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
687
688
688 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
689 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
689 # by inserting dummy changesets with two parents, and handle
690 # by inserting dummy changesets with two parents, and handle
690 # {{mergefrombranch BRANCHNAME}} by setting two parents.
691 # {{mergefrombranch BRANCHNAME}} by setting two parents.
691
692
692 if mergeto is None:
693 if mergeto is None:
693 mergeto = r'{{mergetobranch ([-\w]+)}}'
694 mergeto = r'{{mergetobranch ([-\w]+)}}'
694 if mergeto:
695 if mergeto:
695 mergeto = re.compile(mergeto)
696 mergeto = re.compile(mergeto)
696
697
697 if mergefrom is None:
698 if mergefrom is None:
698 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
699 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
699 if mergefrom:
700 if mergefrom:
700 mergefrom = re.compile(mergefrom)
701 mergefrom = re.compile(mergefrom)
701
702
702 versions = {} # changeset index where we saw any particular file version
703 versions = {} # changeset index where we saw any particular file version
703 branches = {} # changeset index where we saw a branch
704 branches = {} # changeset index where we saw a branch
704 n = len(changesets)
705 n = len(changesets)
705 i = 0
706 i = 0
706 while i < n:
707 while i < n:
707 c = changesets[i]
708 c = changesets[i]
708
709
709 for f in c.entries:
710 for f in c.entries:
710 versions[(f.rcs, f.revision)] = i
711 versions[(f.rcs, f.revision)] = i
711
712
712 p = None
713 p = None
713 if c.branch in branches:
714 if c.branch in branches:
714 p = branches[c.branch]
715 p = branches[c.branch]
715 else:
716 else:
716 # first changeset on a new branch
717 # first changeset on a new branch
717 # the parent is a changeset with the branch in its
718 # the parent is a changeset with the branch in its
718 # branchpoints such that it is the latest possible
719 # branchpoints such that it is the latest possible
719 # commit without any intervening, unrelated commits.
720 # commit without any intervening, unrelated commits.
720
721
721 for candidate in xrange(i):
722 for candidate in xrange(i):
722 if c.branch not in changesets[candidate].branchpoints:
723 if c.branch not in changesets[candidate].branchpoints:
723 if p is not None:
724 if p is not None:
724 break
725 break
725 continue
726 continue
726 p = candidate
727 p = candidate
727
728
728 c.parents = []
729 c.parents = []
729 if p is not None:
730 if p is not None:
730 p = changesets[p]
731 p = changesets[p]
731
732
732 # Ensure no changeset has a synthetic changeset as a parent.
733 # Ensure no changeset has a synthetic changeset as a parent.
733 while p.synthetic:
734 while p.synthetic:
734 assert len(p.parents) <= 1, \
735 assert len(p.parents) <= 1, \
735 _('synthetic changeset cannot have multiple parents')
736 _('synthetic changeset cannot have multiple parents')
736 if p.parents:
737 if p.parents:
737 p = p.parents[0]
738 p = p.parents[0]
738 else:
739 else:
739 p = None
740 p = None
740 break
741 break
741
742
742 if p is not None:
743 if p is not None:
743 c.parents.append(p)
744 c.parents.append(p)
744
745
745 if c.mergepoint:
746 if c.mergepoint:
746 if c.mergepoint == 'HEAD':
747 if c.mergepoint == 'HEAD':
747 c.mergepoint = None
748 c.mergepoint = None
748 c.parents.append(changesets[branches[c.mergepoint]])
749 c.parents.append(changesets[branches[c.mergepoint]])
749
750
750 if mergefrom:
751 if mergefrom:
751 m = mergefrom.search(c.comment)
752 m = mergefrom.search(c.comment)
752 if m:
753 if m:
753 m = m.group(1)
754 m = m.group(1)
754 if m == 'HEAD':
755 if m == 'HEAD':
755 m = None
756 m = None
756 try:
757 try:
757 candidate = changesets[branches[m]]
758 candidate = changesets[branches[m]]
758 except KeyError:
759 except KeyError:
759 ui.warn(_("warning: CVS commit message references "
760 ui.warn(_("warning: CVS commit message references "
760 "non-existent branch %r:\n%s\n")
761 "non-existent branch %r:\n%s\n")
761 % (m, c.comment))
762 % (m, c.comment))
762 if m in branches and c.branch != m and not candidate.synthetic:
763 if m in branches and c.branch != m and not candidate.synthetic:
763 c.parents.append(candidate)
764 c.parents.append(candidate)
764
765
765 if mergeto:
766 if mergeto:
766 m = mergeto.search(c.comment)
767 m = mergeto.search(c.comment)
767 if m:
768 if m:
768 if m.groups():
769 if m.groups():
769 m = m.group(1)
770 m = m.group(1)
770 if m == 'HEAD':
771 if m == 'HEAD':
771 m = None
772 m = None
772 else:
773 else:
773 m = None # if no group found then merge to HEAD
774 m = None # if no group found then merge to HEAD
774 if m in branches and c.branch != m:
775 if m in branches and c.branch != m:
775 # insert empty changeset for merge
776 # insert empty changeset for merge
776 cc = changeset(
777 cc = changeset(
777 author=c.author, branch=m, date=c.date,
778 author=c.author, branch=m, date=c.date,
778 comment='convert-repo: CVS merge from branch %s'
779 comment='convert-repo: CVS merge from branch %s'
779 % c.branch,
780 % c.branch,
780 entries=[], tags=[],
781 entries=[], tags=[],
781 parents=[changesets[branches[m]], c])
782 parents=[changesets[branches[m]], c])
782 changesets.insert(i + 1, cc)
783 changesets.insert(i + 1, cc)
783 branches[m] = i + 1
784 branches[m] = i + 1
784
785
785 # adjust our loop counters now we have inserted a new entry
786 # adjust our loop counters now we have inserted a new entry
786 n += 1
787 n += 1
787 i += 2
788 i += 2
788 continue
789 continue
789
790
790 branches[c.branch] = i
791 branches[c.branch] = i
791 i += 1
792 i += 1
792
793
793 # Drop synthetic changesets (safe now that we have ensured no other
794 # Drop synthetic changesets (safe now that we have ensured no other
794 # changesets can have them as parents).
795 # changesets can have them as parents).
795 i = 0
796 i = 0
796 while i < len(changesets):
797 while i < len(changesets):
797 if changesets[i].synthetic:
798 if changesets[i].synthetic:
798 del changesets[i]
799 del changesets[i]
799 else:
800 else:
800 i += 1
801 i += 1
801
802
802 # Number changesets
803 # Number changesets
803
804
804 for i, c in enumerate(changesets):
805 for i, c in enumerate(changesets):
805 c.id = i + 1
806 c.id = i + 1
806
807
807 if odd:
808 if odd:
808 for l, r in odd:
809 for l, r in odd:
809 if l.id is not None and r.id is not None:
810 if l.id is not None and r.id is not None:
810 ui.warn(_('changeset %d is both before and after %d\n')
811 ui.warn(_('changeset %d is both before and after %d\n')
811 % (l.id, r.id))
812 % (l.id, r.id))
812
813
813 ui.status(_('%d changeset entries\n') % len(changesets))
814 ui.status(_('%d changeset entries\n') % len(changesets))
814
815
815 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
816 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
816
817
817 return changesets
818 return changesets
818
819
819
820
820 def debugcvsps(ui, *args, **opts):
821 def debugcvsps(ui, *args, **opts):
821 '''Read CVS rlog for current directory or named path in
822 '''Read CVS rlog for current directory or named path in
822 repository, and convert the log to changesets based on matching
823 repository, and convert the log to changesets based on matching
823 commit log entries and dates.
824 commit log entries and dates.
824 '''
825 '''
825 if opts["new_cache"]:
826 if opts["new_cache"]:
826 cache = "write"
827 cache = "write"
827 elif opts["update_cache"]:
828 elif opts["update_cache"]:
828 cache = "update"
829 cache = "update"
829 else:
830 else:
830 cache = None
831 cache = None
831
832
832 revisions = opts["revisions"]
833 revisions = opts["revisions"]
833
834
834 try:
835 try:
835 if args:
836 if args:
836 log = []
837 log = []
837 for d in args:
838 for d in args:
838 log += createlog(ui, d, root=opts["root"], cache=cache)
839 log += createlog(ui, d, root=opts["root"], cache=cache)
839 else:
840 else:
840 log = createlog(ui, root=opts["root"], cache=cache)
841 log = createlog(ui, root=opts["root"], cache=cache)
841 except logerror as e:
842 except logerror as e:
842 ui.write("%r\n"%e)
843 ui.write("%r\n"%e)
843 return
844 return
844
845
845 changesets = createchangeset(ui, log, opts["fuzz"])
846 changesets = createchangeset(ui, log, opts["fuzz"])
846 del log
847 del log
847
848
848 # Print changesets (optionally filtered)
849 # Print changesets (optionally filtered)
849
850
850 off = len(revisions)
851 off = len(revisions)
851 branches = {} # latest version number in each branch
852 branches = {} # latest version number in each branch
852 ancestors = {} # parent branch
853 ancestors = {} # parent branch
853 for cs in changesets:
854 for cs in changesets:
854
855
855 if opts["ancestors"]:
856 if opts["ancestors"]:
856 if cs.branch not in branches and cs.parents and cs.parents[0].id:
857 if cs.branch not in branches and cs.parents and cs.parents[0].id:
857 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
858 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
858 cs.parents[0].id)
859 cs.parents[0].id)
859 branches[cs.branch] = cs.id
860 branches[cs.branch] = cs.id
860
861
861 # limit by branches
862 # limit by branches
862 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
863 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
863 continue
864 continue
864
865
865 if not off:
866 if not off:
866 # Note: trailing spaces on several lines here are needed to have
867 # Note: trailing spaces on several lines here are needed to have
867 # bug-for-bug compatibility with cvsps.
868 # bug-for-bug compatibility with cvsps.
868 ui.write('---------------------\n')
869 ui.write('---------------------\n')
869 ui.write(('PatchSet %d \n' % cs.id))
870 ui.write(('PatchSet %d \n' % cs.id))
870 ui.write(('Date: %s\n' % util.datestr(cs.date,
871 ui.write(('Date: %s\n' % util.datestr(cs.date,
871 '%Y/%m/%d %H:%M:%S %1%2')))
872 '%Y/%m/%d %H:%M:%S %1%2')))
872 ui.write(('Author: %s\n' % cs.author))
873 ui.write(('Author: %s\n' % cs.author))
873 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
874 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
874 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
875 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
875 ','.join(cs.tags) or '(none)')))
876 ','.join(cs.tags) or '(none)')))
876 if cs.branchpoints:
877 if cs.branchpoints:
877 ui.write(('Branchpoints: %s \n') %
878 ui.write(('Branchpoints: %s \n') %
878 ', '.join(sorted(cs.branchpoints)))
879 ', '.join(sorted(cs.branchpoints)))
879 if opts["parents"] and cs.parents:
880 if opts["parents"] and cs.parents:
880 if len(cs.parents) > 1:
881 if len(cs.parents) > 1:
881 ui.write(('Parents: %s\n' %
882 ui.write(('Parents: %s\n' %
882 (','.join([str(p.id) for p in cs.parents]))))
883 (','.join([str(p.id) for p in cs.parents]))))
883 else:
884 else:
884 ui.write(('Parent: %d\n' % cs.parents[0].id))
885 ui.write(('Parent: %d\n' % cs.parents[0].id))
885
886
886 if opts["ancestors"]:
887 if opts["ancestors"]:
887 b = cs.branch
888 b = cs.branch
888 r = []
889 r = []
889 while b:
890 while b:
890 b, c = ancestors[b]
891 b, c = ancestors[b]
891 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
892 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
892 if r:
893 if r:
893 ui.write(('Ancestors: %s\n' % (','.join(r))))
894 ui.write(('Ancestors: %s\n' % (','.join(r))))
894
895
895 ui.write(('Log:\n'))
896 ui.write(('Log:\n'))
896 ui.write('%s\n\n' % cs.comment)
897 ui.write('%s\n\n' % cs.comment)
897 ui.write(('Members: \n'))
898 ui.write(('Members: \n'))
898 for f in cs.entries:
899 for f in cs.entries:
899 fn = f.file
900 fn = f.file
900 if fn.startswith(opts["prefix"]):
901 if fn.startswith(opts["prefix"]):
901 fn = fn[len(opts["prefix"]):]
902 fn = fn[len(opts["prefix"]):]
902 ui.write('\t%s:%s->%s%s \n' % (
903 ui.write('\t%s:%s->%s%s \n' % (
903 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
904 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
904 '.'.join([str(x) for x in f.revision]),
905 '.'.join([str(x) for x in f.revision]),
905 ['', '(DEAD)'][f.dead]))
906 ['', '(DEAD)'][f.dead]))
906 ui.write('\n')
907 ui.write('\n')
907
908
908 # have we seen the start tag?
909 # have we seen the start tag?
909 if revisions and off:
910 if revisions and off:
910 if revisions[0] == str(cs.id) or \
911 if revisions[0] == str(cs.id) or \
911 revisions[0] in cs.tags:
912 revisions[0] in cs.tags:
912 off = False
913 off = False
913
914
914 # see if we reached the end tag
915 # see if we reached the end tag
915 if len(revisions) > 1 and not off:
916 if len(revisions) > 1 and not off:
916 if revisions[1] == str(cs.id) or \
917 if revisions[1] == str(cs.id) or \
917 revisions[1] in cs.tags:
918 revisions[1] in cs.tags:
918 break
919 break
@@ -1,462 +1,463
1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import posixpath
8 import posixpath
9 import shlex
9 import shlex
10
11 from mercurial.i18n import _
10 from mercurial import (
12 from mercurial import (
11 error,
13 error,
12 )
14 )
13 from mercurial.i18n import _
14 from . import common
15 from . import common
15 SKIPREV = common.SKIPREV
16 SKIPREV = common.SKIPREV
16
17
17 def rpairs(path):
18 def rpairs(path):
18 '''Yield tuples with path split at '/', starting with the full path.
19 '''Yield tuples with path split at '/', starting with the full path.
19 No leading, trailing or double '/', please.
20 No leading, trailing or double '/', please.
20 >>> for x in rpairs('foo/bar/baz'): print x
21 >>> for x in rpairs('foo/bar/baz'): print x
21 ('foo/bar/baz', '')
22 ('foo/bar/baz', '')
22 ('foo/bar', 'baz')
23 ('foo/bar', 'baz')
23 ('foo', 'bar/baz')
24 ('foo', 'bar/baz')
24 ('.', 'foo/bar/baz')
25 ('.', 'foo/bar/baz')
25 '''
26 '''
26 i = len(path)
27 i = len(path)
27 while i != -1:
28 while i != -1:
28 yield path[:i], path[i + 1:]
29 yield path[:i], path[i + 1:]
29 i = path.rfind('/', 0, i)
30 i = path.rfind('/', 0, i)
30 yield '.', path
31 yield '.', path
31
32
32 def normalize(path):
33 def normalize(path):
33 ''' We use posixpath.normpath to support cross-platform path format.
34 ''' We use posixpath.normpath to support cross-platform path format.
34 However, it doesn't handle None input. So we wrap it up. '''
35 However, it doesn't handle None input. So we wrap it up. '''
35 if path is None:
36 if path is None:
36 return None
37 return None
37 return posixpath.normpath(path)
38 return posixpath.normpath(path)
38
39
39 class filemapper(object):
40 class filemapper(object):
40 '''Map and filter filenames when importing.
41 '''Map and filter filenames when importing.
41 A name can be mapped to itself, a new name, or None (omit from new
42 A name can be mapped to itself, a new name, or None (omit from new
42 repository).'''
43 repository).'''
43
44
44 def __init__(self, ui, path=None):
45 def __init__(self, ui, path=None):
45 self.ui = ui
46 self.ui = ui
46 self.include = {}
47 self.include = {}
47 self.exclude = {}
48 self.exclude = {}
48 self.rename = {}
49 self.rename = {}
49 self.targetprefixes = None
50 self.targetprefixes = None
50 if path:
51 if path:
51 if self.parse(path):
52 if self.parse(path):
52 raise error.Abort(_('errors in filemap'))
53 raise error.Abort(_('errors in filemap'))
53
54
54 def parse(self, path):
55 def parse(self, path):
55 errs = 0
56 errs = 0
56 def check(name, mapping, listname):
57 def check(name, mapping, listname):
57 if not name:
58 if not name:
58 self.ui.warn(_('%s:%d: path to %s is missing\n') %
59 self.ui.warn(_('%s:%d: path to %s is missing\n') %
59 (lex.infile, lex.lineno, listname))
60 (lex.infile, lex.lineno, listname))
60 return 1
61 return 1
61 if name in mapping:
62 if name in mapping:
62 self.ui.warn(_('%s:%d: %r already in %s list\n') %
63 self.ui.warn(_('%s:%d: %r already in %s list\n') %
63 (lex.infile, lex.lineno, name, listname))
64 (lex.infile, lex.lineno, name, listname))
64 return 1
65 return 1
65 if (name.startswith('/') or
66 if (name.startswith('/') or
66 name.endswith('/') or
67 name.endswith('/') or
67 '//' in name):
68 '//' in name):
68 self.ui.warn(_('%s:%d: superfluous / in %s %r\n') %
69 self.ui.warn(_('%s:%d: superfluous / in %s %r\n') %
69 (lex.infile, lex.lineno, listname, name))
70 (lex.infile, lex.lineno, listname, name))
70 return 1
71 return 1
71 return 0
72 return 0
72 lex = shlex.shlex(open(path), path, True)
73 lex = shlex.shlex(open(path), path, True)
73 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
74 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
74 cmd = lex.get_token()
75 cmd = lex.get_token()
75 while cmd:
76 while cmd:
76 if cmd == 'include':
77 if cmd == 'include':
77 name = normalize(lex.get_token())
78 name = normalize(lex.get_token())
78 errs += check(name, self.exclude, 'exclude')
79 errs += check(name, self.exclude, 'exclude')
79 self.include[name] = name
80 self.include[name] = name
80 elif cmd == 'exclude':
81 elif cmd == 'exclude':
81 name = normalize(lex.get_token())
82 name = normalize(lex.get_token())
82 errs += check(name, self.include, 'include')
83 errs += check(name, self.include, 'include')
83 errs += check(name, self.rename, 'rename')
84 errs += check(name, self.rename, 'rename')
84 self.exclude[name] = name
85 self.exclude[name] = name
85 elif cmd == 'rename':
86 elif cmd == 'rename':
86 src = normalize(lex.get_token())
87 src = normalize(lex.get_token())
87 dest = normalize(lex.get_token())
88 dest = normalize(lex.get_token())
88 errs += check(src, self.exclude, 'exclude')
89 errs += check(src, self.exclude, 'exclude')
89 self.rename[src] = dest
90 self.rename[src] = dest
90 elif cmd == 'source':
91 elif cmd == 'source':
91 errs += self.parse(normalize(lex.get_token()))
92 errs += self.parse(normalize(lex.get_token()))
92 else:
93 else:
93 self.ui.warn(_('%s:%d: unknown directive %r\n') %
94 self.ui.warn(_('%s:%d: unknown directive %r\n') %
94 (lex.infile, lex.lineno, cmd))
95 (lex.infile, lex.lineno, cmd))
95 errs += 1
96 errs += 1
96 cmd = lex.get_token()
97 cmd = lex.get_token()
97 return errs
98 return errs
98
99
99 def lookup(self, name, mapping):
100 def lookup(self, name, mapping):
100 name = normalize(name)
101 name = normalize(name)
101 for pre, suf in rpairs(name):
102 for pre, suf in rpairs(name):
102 try:
103 try:
103 return mapping[pre], pre, suf
104 return mapping[pre], pre, suf
104 except KeyError:
105 except KeyError:
105 pass
106 pass
106 return '', name, ''
107 return '', name, ''
107
108
108 def istargetfile(self, filename):
109 def istargetfile(self, filename):
109 """Return true if the given target filename is covered as a destination
110 """Return true if the given target filename is covered as a destination
110 of the filemap. This is useful for identifying what parts of the target
111 of the filemap. This is useful for identifying what parts of the target
111 repo belong to the source repo and what parts don't."""
112 repo belong to the source repo and what parts don't."""
112 if self.targetprefixes is None:
113 if self.targetprefixes is None:
113 self.targetprefixes = set()
114 self.targetprefixes = set()
114 for before, after in self.rename.iteritems():
115 for before, after in self.rename.iteritems():
115 self.targetprefixes.add(after)
116 self.targetprefixes.add(after)
116
117
117 # If "." is a target, then all target files are considered from the
118 # If "." is a target, then all target files are considered from the
118 # source.
119 # source.
119 if not self.targetprefixes or '.' in self.targetprefixes:
120 if not self.targetprefixes or '.' in self.targetprefixes:
120 return True
121 return True
121
122
122 filename = normalize(filename)
123 filename = normalize(filename)
123 for pre, suf in rpairs(filename):
124 for pre, suf in rpairs(filename):
124 # This check is imperfect since it doesn't account for the
125 # This check is imperfect since it doesn't account for the
125 # include/exclude list, but it should work in filemaps that don't
126 # include/exclude list, but it should work in filemaps that don't
126 # apply include/exclude to the same source directories they are
127 # apply include/exclude to the same source directories they are
127 # renaming.
128 # renaming.
128 if pre in self.targetprefixes:
129 if pre in self.targetprefixes:
129 return True
130 return True
130 return False
131 return False
131
132
132 def __call__(self, name):
133 def __call__(self, name):
133 if self.include:
134 if self.include:
134 inc = self.lookup(name, self.include)[0]
135 inc = self.lookup(name, self.include)[0]
135 else:
136 else:
136 inc = name
137 inc = name
137 if self.exclude:
138 if self.exclude:
138 exc = self.lookup(name, self.exclude)[0]
139 exc = self.lookup(name, self.exclude)[0]
139 else:
140 else:
140 exc = ''
141 exc = ''
141 if (not self.include and exc) or (len(inc) <= len(exc)):
142 if (not self.include and exc) or (len(inc) <= len(exc)):
142 return None
143 return None
143 newpre, pre, suf = self.lookup(name, self.rename)
144 newpre, pre, suf = self.lookup(name, self.rename)
144 if newpre:
145 if newpre:
145 if newpre == '.':
146 if newpre == '.':
146 return suf
147 return suf
147 if suf:
148 if suf:
148 if newpre.endswith('/'):
149 if newpre.endswith('/'):
149 return newpre + suf
150 return newpre + suf
150 return newpre + '/' + suf
151 return newpre + '/' + suf
151 return newpre
152 return newpre
152 return name
153 return name
153
154
154 def active(self):
155 def active(self):
155 return bool(self.include or self.exclude or self.rename)
156 return bool(self.include or self.exclude or self.rename)
156
157
157 # This class does two additional things compared to a regular source:
158 # This class does two additional things compared to a regular source:
158 #
159 #
159 # - Filter and rename files. This is mostly wrapped by the filemapper
160 # - Filter and rename files. This is mostly wrapped by the filemapper
160 # class above. We hide the original filename in the revision that is
161 # class above. We hide the original filename in the revision that is
161 # returned by getchanges to be able to find things later in getfile.
162 # returned by getchanges to be able to find things later in getfile.
162 #
163 #
163 # - Return only revisions that matter for the files we're interested in.
164 # - Return only revisions that matter for the files we're interested in.
164 # This involves rewriting the parents of the original revision to
165 # This involves rewriting the parents of the original revision to
165 # create a graph that is restricted to those revisions.
166 # create a graph that is restricted to those revisions.
166 #
167 #
167 # This set of revisions includes not only revisions that directly
168 # This set of revisions includes not only revisions that directly
168 # touch files we're interested in, but also merges that merge two
169 # touch files we're interested in, but also merges that merge two
169 # or more interesting revisions.
170 # or more interesting revisions.
170
171
171 class filemap_source(common.converter_source):
172 class filemap_source(common.converter_source):
172 def __init__(self, ui, baseconverter, filemap):
173 def __init__(self, ui, baseconverter, filemap):
173 super(filemap_source, self).__init__(ui)
174 super(filemap_source, self).__init__(ui)
174 self.base = baseconverter
175 self.base = baseconverter
175 self.filemapper = filemapper(ui, filemap)
176 self.filemapper = filemapper(ui, filemap)
176 self.commits = {}
177 self.commits = {}
177 # if a revision rev has parent p in the original revision graph, then
178 # if a revision rev has parent p in the original revision graph, then
178 # rev will have parent self.parentmap[p] in the restricted graph.
179 # rev will have parent self.parentmap[p] in the restricted graph.
179 self.parentmap = {}
180 self.parentmap = {}
180 # self.wantedancestors[rev] is the set of all ancestors of rev that
181 # self.wantedancestors[rev] is the set of all ancestors of rev that
181 # are in the restricted graph.
182 # are in the restricted graph.
182 self.wantedancestors = {}
183 self.wantedancestors = {}
183 self.convertedorder = None
184 self.convertedorder = None
184 self._rebuilt = False
185 self._rebuilt = False
185 self.origparents = {}
186 self.origparents = {}
186 self.children = {}
187 self.children = {}
187 self.seenchildren = {}
188 self.seenchildren = {}
188 # experimental config: convert.ignoreancestorcheck
189 # experimental config: convert.ignoreancestorcheck
189 self.ignoreancestorcheck = self.ui.configbool('convert',
190 self.ignoreancestorcheck = self.ui.configbool('convert',
190 'ignoreancestorcheck')
191 'ignoreancestorcheck')
191
192
192 def before(self):
193 def before(self):
193 self.base.before()
194 self.base.before()
194
195
195 def after(self):
196 def after(self):
196 self.base.after()
197 self.base.after()
197
198
198 def setrevmap(self, revmap):
199 def setrevmap(self, revmap):
199 # rebuild our state to make things restartable
200 # rebuild our state to make things restartable
200 #
201 #
201 # To avoid calling getcommit for every revision that has already
202 # To avoid calling getcommit for every revision that has already
202 # been converted, we rebuild only the parentmap, delaying the
203 # been converted, we rebuild only the parentmap, delaying the
203 # rebuild of wantedancestors until we need it (i.e. until a
204 # rebuild of wantedancestors until we need it (i.e. until a
204 # merge).
205 # merge).
205 #
206 #
206 # We assume the order argument lists the revisions in
207 # We assume the order argument lists the revisions in
207 # topological order, so that we can infer which revisions were
208 # topological order, so that we can infer which revisions were
208 # wanted by previous runs.
209 # wanted by previous runs.
209 self._rebuilt = not revmap
210 self._rebuilt = not revmap
210 seen = {SKIPREV: SKIPREV}
211 seen = {SKIPREV: SKIPREV}
211 dummyset = set()
212 dummyset = set()
212 converted = []
213 converted = []
213 for rev in revmap.order:
214 for rev in revmap.order:
214 mapped = revmap[rev]
215 mapped = revmap[rev]
215 wanted = mapped not in seen
216 wanted = mapped not in seen
216 if wanted:
217 if wanted:
217 seen[mapped] = rev
218 seen[mapped] = rev
218 self.parentmap[rev] = rev
219 self.parentmap[rev] = rev
219 else:
220 else:
220 self.parentmap[rev] = seen[mapped]
221 self.parentmap[rev] = seen[mapped]
221 self.wantedancestors[rev] = dummyset
222 self.wantedancestors[rev] = dummyset
222 arg = seen[mapped]
223 arg = seen[mapped]
223 if arg == SKIPREV:
224 if arg == SKIPREV:
224 arg = None
225 arg = None
225 converted.append((rev, wanted, arg))
226 converted.append((rev, wanted, arg))
226 self.convertedorder = converted
227 self.convertedorder = converted
227 return self.base.setrevmap(revmap)
228 return self.base.setrevmap(revmap)
228
229
229 def rebuild(self):
230 def rebuild(self):
230 if self._rebuilt:
231 if self._rebuilt:
231 return True
232 return True
232 self._rebuilt = True
233 self._rebuilt = True
233 self.parentmap.clear()
234 self.parentmap.clear()
234 self.wantedancestors.clear()
235 self.wantedancestors.clear()
235 self.seenchildren.clear()
236 self.seenchildren.clear()
236 for rev, wanted, arg in self.convertedorder:
237 for rev, wanted, arg in self.convertedorder:
237 if rev not in self.origparents:
238 if rev not in self.origparents:
238 try:
239 try:
239 self.origparents[rev] = self.getcommit(rev).parents
240 self.origparents[rev] = self.getcommit(rev).parents
240 except error.RepoLookupError:
241 except error.RepoLookupError:
241 self.ui.debug("unknown revmap source: %s\n" % rev)
242 self.ui.debug("unknown revmap source: %s\n" % rev)
242 continue
243 continue
243 if arg is not None:
244 if arg is not None:
244 self.children[arg] = self.children.get(arg, 0) + 1
245 self.children[arg] = self.children.get(arg, 0) + 1
245
246
246 for rev, wanted, arg in self.convertedorder:
247 for rev, wanted, arg in self.convertedorder:
247 try:
248 try:
248 parents = self.origparents[rev]
249 parents = self.origparents[rev]
249 except KeyError:
250 except KeyError:
250 continue # unknown revmap source
251 continue # unknown revmap source
251 if wanted:
252 if wanted:
252 self.mark_wanted(rev, parents)
253 self.mark_wanted(rev, parents)
253 else:
254 else:
254 self.mark_not_wanted(rev, arg)
255 self.mark_not_wanted(rev, arg)
255 self._discard(arg, *parents)
256 self._discard(arg, *parents)
256
257
257 return True
258 return True
258
259
259 def getheads(self):
260 def getheads(self):
260 return self.base.getheads()
261 return self.base.getheads()
261
262
262 def getcommit(self, rev):
263 def getcommit(self, rev):
263 # We want to save a reference to the commit objects to be able
264 # We want to save a reference to the commit objects to be able
264 # to rewrite their parents later on.
265 # to rewrite their parents later on.
265 c = self.commits[rev] = self.base.getcommit(rev)
266 c = self.commits[rev] = self.base.getcommit(rev)
266 for p in c.parents:
267 for p in c.parents:
267 self.children[p] = self.children.get(p, 0) + 1
268 self.children[p] = self.children.get(p, 0) + 1
268 return c
269 return c
269
270
270 def _cachedcommit(self, rev):
271 def _cachedcommit(self, rev):
271 if rev in self.commits:
272 if rev in self.commits:
272 return self.commits[rev]
273 return self.commits[rev]
273 return self.base.getcommit(rev)
274 return self.base.getcommit(rev)
274
275
275 def _discard(self, *revs):
276 def _discard(self, *revs):
276 for r in revs:
277 for r in revs:
277 if r is None:
278 if r is None:
278 continue
279 continue
279 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
280 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
280 if self.seenchildren[r] == self.children[r]:
281 if self.seenchildren[r] == self.children[r]:
281 self.wantedancestors.pop(r, None)
282 self.wantedancestors.pop(r, None)
282 self.parentmap.pop(r, None)
283 self.parentmap.pop(r, None)
283 del self.seenchildren[r]
284 del self.seenchildren[r]
284 if self._rebuilt:
285 if self._rebuilt:
285 del self.children[r]
286 del self.children[r]
286
287
287 def wanted(self, rev, i):
288 def wanted(self, rev, i):
288 # Return True if we're directly interested in rev.
289 # Return True if we're directly interested in rev.
289 #
290 #
290 # i is an index selecting one of the parents of rev (if rev
291 # i is an index selecting one of the parents of rev (if rev
291 # has no parents, i is None). getchangedfiles will give us
292 # has no parents, i is None). getchangedfiles will give us
292 # the list of files that are different in rev and in the parent
293 # the list of files that are different in rev and in the parent
293 # indicated by i. If we're interested in any of these files,
294 # indicated by i. If we're interested in any of these files,
294 # we're interested in rev.
295 # we're interested in rev.
295 try:
296 try:
296 files = self.base.getchangedfiles(rev, i)
297 files = self.base.getchangedfiles(rev, i)
297 except NotImplementedError:
298 except NotImplementedError:
298 raise error.Abort(_("source repository doesn't support --filemap"))
299 raise error.Abort(_("source repository doesn't support --filemap"))
299 for f in files:
300 for f in files:
300 if self.filemapper(f):
301 if self.filemapper(f):
301 return True
302 return True
302 return False
303 return False
303
304
304 def mark_not_wanted(self, rev, p):
305 def mark_not_wanted(self, rev, p):
305 # Mark rev as not interesting and update data structures.
306 # Mark rev as not interesting and update data structures.
306
307
307 if p is None:
308 if p is None:
308 # A root revision. Use SKIPREV to indicate that it doesn't
309 # A root revision. Use SKIPREV to indicate that it doesn't
309 # map to any revision in the restricted graph. Put SKIPREV
310 # map to any revision in the restricted graph. Put SKIPREV
310 # in the set of wanted ancestors to simplify code elsewhere
311 # in the set of wanted ancestors to simplify code elsewhere
311 self.parentmap[rev] = SKIPREV
312 self.parentmap[rev] = SKIPREV
312 self.wantedancestors[rev] = set((SKIPREV,))
313 self.wantedancestors[rev] = set((SKIPREV,))
313 return
314 return
314
315
315 # Reuse the data from our parent.
316 # Reuse the data from our parent.
316 self.parentmap[rev] = self.parentmap[p]
317 self.parentmap[rev] = self.parentmap[p]
317 self.wantedancestors[rev] = self.wantedancestors[p]
318 self.wantedancestors[rev] = self.wantedancestors[p]
318
319
319 def mark_wanted(self, rev, parents):
320 def mark_wanted(self, rev, parents):
320 # Mark rev ss wanted and update data structures.
321 # Mark rev ss wanted and update data structures.
321
322
322 # rev will be in the restricted graph, so children of rev in
323 # rev will be in the restricted graph, so children of rev in
323 # the original graph should still have rev as a parent in the
324 # the original graph should still have rev as a parent in the
324 # restricted graph.
325 # restricted graph.
325 self.parentmap[rev] = rev
326 self.parentmap[rev] = rev
326
327
327 # The set of wanted ancestors of rev is the union of the sets
328 # The set of wanted ancestors of rev is the union of the sets
328 # of wanted ancestors of its parents. Plus rev itself.
329 # of wanted ancestors of its parents. Plus rev itself.
329 wrev = set()
330 wrev = set()
330 for p in parents:
331 for p in parents:
331 if p in self.wantedancestors:
332 if p in self.wantedancestors:
332 wrev.update(self.wantedancestors[p])
333 wrev.update(self.wantedancestors[p])
333 else:
334 else:
334 self.ui.warn(_('warning: %s parent %s is missing\n') %
335 self.ui.warn(_('warning: %s parent %s is missing\n') %
335 (rev, p))
336 (rev, p))
336 wrev.add(rev)
337 wrev.add(rev)
337 self.wantedancestors[rev] = wrev
338 self.wantedancestors[rev] = wrev
338
339
339 def getchanges(self, rev, full):
340 def getchanges(self, rev, full):
340 parents = self.commits[rev].parents
341 parents = self.commits[rev].parents
341 if len(parents) > 1 and not self.ignoreancestorcheck:
342 if len(parents) > 1 and not self.ignoreancestorcheck:
342 self.rebuild()
343 self.rebuild()
343
344
344 # To decide whether we're interested in rev we:
345 # To decide whether we're interested in rev we:
345 #
346 #
346 # - calculate what parents rev will have if it turns out we're
347 # - calculate what parents rev will have if it turns out we're
347 # interested in it. If it's going to have more than 1 parent,
348 # interested in it. If it's going to have more than 1 parent,
348 # we're interested in it.
349 # we're interested in it.
349 #
350 #
350 # - otherwise, we'll compare it with the single parent we found.
351 # - otherwise, we'll compare it with the single parent we found.
351 # If any of the files we're interested in is different in the
352 # If any of the files we're interested in is different in the
352 # the two revisions, we're interested in rev.
353 # the two revisions, we're interested in rev.
353
354
354 # A parent p is interesting if its mapped version (self.parentmap[p]):
355 # A parent p is interesting if its mapped version (self.parentmap[p]):
355 # - is not SKIPREV
356 # - is not SKIPREV
356 # - is still not in the list of parents (we don't want duplicates)
357 # - is still not in the list of parents (we don't want duplicates)
357 # - is not an ancestor of the mapped versions of the other parents or
358 # - is not an ancestor of the mapped versions of the other parents or
358 # there is no parent in the same branch than the current revision.
359 # there is no parent in the same branch than the current revision.
359 mparents = []
360 mparents = []
360 knownparents = set()
361 knownparents = set()
361 branch = self.commits[rev].branch
362 branch = self.commits[rev].branch
362 hasbranchparent = False
363 hasbranchparent = False
363 for i, p1 in enumerate(parents):
364 for i, p1 in enumerate(parents):
364 mp1 = self.parentmap[p1]
365 mp1 = self.parentmap[p1]
365 if mp1 == SKIPREV or mp1 in knownparents:
366 if mp1 == SKIPREV or mp1 in knownparents:
366 continue
367 continue
367
368
368 isancestor = (not self.ignoreancestorcheck and
369 isancestor = (not self.ignoreancestorcheck and
369 any(p2 for p2 in parents
370 any(p2 for p2 in parents
370 if p1 != p2 and mp1 != self.parentmap[p2]
371 if p1 != p2 and mp1 != self.parentmap[p2]
371 and mp1 in self.wantedancestors[p2]))
372 and mp1 in self.wantedancestors[p2]))
372 if not isancestor and not hasbranchparent and len(parents) > 1:
373 if not isancestor and not hasbranchparent and len(parents) > 1:
373 # This could be expensive, avoid unnecessary calls.
374 # This could be expensive, avoid unnecessary calls.
374 if self._cachedcommit(p1).branch == branch:
375 if self._cachedcommit(p1).branch == branch:
375 hasbranchparent = True
376 hasbranchparent = True
376 mparents.append((p1, mp1, i, isancestor))
377 mparents.append((p1, mp1, i, isancestor))
377 knownparents.add(mp1)
378 knownparents.add(mp1)
378 # Discard parents ancestors of other parents if there is a
379 # Discard parents ancestors of other parents if there is a
379 # non-ancestor one on the same branch than current revision.
380 # non-ancestor one on the same branch than current revision.
380 if hasbranchparent:
381 if hasbranchparent:
381 mparents = [p for p in mparents if not p[3]]
382 mparents = [p for p in mparents if not p[3]]
382 wp = None
383 wp = None
383 if mparents:
384 if mparents:
384 wp = max(p[2] for p in mparents)
385 wp = max(p[2] for p in mparents)
385 mparents = [p[1] for p in mparents]
386 mparents = [p[1] for p in mparents]
386 elif parents:
387 elif parents:
387 wp = 0
388 wp = 0
388
389
389 self.origparents[rev] = parents
390 self.origparents[rev] = parents
390
391
391 closed = False
392 closed = False
392 if 'close' in self.commits[rev].extra:
393 if 'close' in self.commits[rev].extra:
393 # A branch closing revision is only useful if one of its
394 # A branch closing revision is only useful if one of its
394 # parents belong to the branch being closed
395 # parents belong to the branch being closed
395 pbranches = [self._cachedcommit(p).branch for p in mparents]
396 pbranches = [self._cachedcommit(p).branch for p in mparents]
396 if branch in pbranches:
397 if branch in pbranches:
397 closed = True
398 closed = True
398
399
399 if len(mparents) < 2 and not closed and not self.wanted(rev, wp):
400 if len(mparents) < 2 and not closed and not self.wanted(rev, wp):
400 # We don't want this revision.
401 # We don't want this revision.
401 # Update our state and tell the convert process to map this
402 # Update our state and tell the convert process to map this
402 # revision to the same revision its parent as mapped to.
403 # revision to the same revision its parent as mapped to.
403 p = None
404 p = None
404 if parents:
405 if parents:
405 p = parents[wp]
406 p = parents[wp]
406 self.mark_not_wanted(rev, p)
407 self.mark_not_wanted(rev, p)
407 self.convertedorder.append((rev, False, p))
408 self.convertedorder.append((rev, False, p))
408 self._discard(*parents)
409 self._discard(*parents)
409 return self.parentmap[rev]
410 return self.parentmap[rev]
410
411
411 # We want this revision.
412 # We want this revision.
412 # Rewrite the parents of the commit object
413 # Rewrite the parents of the commit object
413 self.commits[rev].parents = mparents
414 self.commits[rev].parents = mparents
414 self.mark_wanted(rev, parents)
415 self.mark_wanted(rev, parents)
415 self.convertedorder.append((rev, True, None))
416 self.convertedorder.append((rev, True, None))
416 self._discard(*parents)
417 self._discard(*parents)
417
418
418 # Get the real changes and do the filtering/mapping. To be
419 # Get the real changes and do the filtering/mapping. To be
419 # able to get the files later on in getfile, we hide the
420 # able to get the files later on in getfile, we hide the
420 # original filename in the rev part of the return value.
421 # original filename in the rev part of the return value.
421 changes, copies, cleanp2 = self.base.getchanges(rev, full)
422 changes, copies, cleanp2 = self.base.getchanges(rev, full)
422 files = {}
423 files = {}
423 ncleanp2 = set(cleanp2)
424 ncleanp2 = set(cleanp2)
424 for f, r in changes:
425 for f, r in changes:
425 newf = self.filemapper(f)
426 newf = self.filemapper(f)
426 if newf and (newf != f or newf not in files):
427 if newf and (newf != f or newf not in files):
427 files[newf] = (f, r)
428 files[newf] = (f, r)
428 if newf != f:
429 if newf != f:
429 ncleanp2.discard(f)
430 ncleanp2.discard(f)
430 files = sorted(files.items())
431 files = sorted(files.items())
431
432
432 ncopies = {}
433 ncopies = {}
433 for c in copies:
434 for c in copies:
434 newc = self.filemapper(c)
435 newc = self.filemapper(c)
435 if newc:
436 if newc:
436 newsource = self.filemapper(copies[c])
437 newsource = self.filemapper(copies[c])
437 if newsource:
438 if newsource:
438 ncopies[newc] = newsource
439 ncopies[newc] = newsource
439
440
440 return files, ncopies, ncleanp2
441 return files, ncopies, ncleanp2
441
442
442 def targetfilebelongstosource(self, targetfilename):
443 def targetfilebelongstosource(self, targetfilename):
443 return self.filemapper.istargetfile(targetfilename)
444 return self.filemapper.istargetfile(targetfilename)
444
445
445 def getfile(self, name, rev):
446 def getfile(self, name, rev):
446 realname, realrev = rev
447 realname, realrev = rev
447 return self.base.getfile(realname, realrev)
448 return self.base.getfile(realname, realrev)
448
449
449 def gettags(self):
450 def gettags(self):
450 return self.base.gettags()
451 return self.base.gettags()
451
452
452 def hasnativeorder(self):
453 def hasnativeorder(self):
453 return self.base.hasnativeorder()
454 return self.base.hasnativeorder()
454
455
455 def lookuprev(self, rev):
456 def lookuprev(self, rev):
456 return self.base.lookuprev(rev)
457 return self.base.lookuprev(rev)
457
458
458 def getbookmarks(self):
459 def getbookmarks(self):
459 return self.base.getbookmarks()
460 return self.base.getbookmarks()
460
461
461 def converted(self, rev, sinkrev):
462 def converted(self, rev, sinkrev):
462 self.base.converted(rev, sinkrev)
463 self.base.converted(rev, sinkrev)
@@ -1,398 +1,399
1 # git.py - git support for the convert extension
1 # git.py - git support for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10
11 from mercurial.i18n import _
10 from mercurial import (
12 from mercurial import (
11 config,
13 config,
12 error,
14 error,
13 node as nodemod,
15 node as nodemod,
14 )
16 )
15 from mercurial.i18n import _
16
17
17 from . import (
18 from . import (
18 common,
19 common,
19 )
20 )
20
21
21 class submodule(object):
22 class submodule(object):
22 def __init__(self, path, node, url):
23 def __init__(self, path, node, url):
23 self.path = path
24 self.path = path
24 self.node = node
25 self.node = node
25 self.url = url
26 self.url = url
26
27
27 def hgsub(self):
28 def hgsub(self):
28 return "%s = [git]%s" % (self.path, self.url)
29 return "%s = [git]%s" % (self.path, self.url)
29
30
30 def hgsubstate(self):
31 def hgsubstate(self):
31 return "%s %s" % (self.node, self.path)
32 return "%s %s" % (self.node, self.path)
32
33
33 class convert_git(common.converter_source, common.commandline):
34 class convert_git(common.converter_source, common.commandline):
34 # Windows does not support GIT_DIR= construct while other systems
35 # Windows does not support GIT_DIR= construct while other systems
35 # cannot remove environment variable. Just assume none have
36 # cannot remove environment variable. Just assume none have
36 # both issues.
37 # both issues.
37
38
38 def _gitcmd(self, cmd, *args, **kwargs):
39 def _gitcmd(self, cmd, *args, **kwargs):
39 return cmd('--git-dir=%s' % self.path, *args, **kwargs)
40 return cmd('--git-dir=%s' % self.path, *args, **kwargs)
40
41
41 def gitrun0(self, *args, **kwargs):
42 def gitrun0(self, *args, **kwargs):
42 return self._gitcmd(self.run0, *args, **kwargs)
43 return self._gitcmd(self.run0, *args, **kwargs)
43
44
44 def gitrun(self, *args, **kwargs):
45 def gitrun(self, *args, **kwargs):
45 return self._gitcmd(self.run, *args, **kwargs)
46 return self._gitcmd(self.run, *args, **kwargs)
46
47
47 def gitrunlines0(self, *args, **kwargs):
48 def gitrunlines0(self, *args, **kwargs):
48 return self._gitcmd(self.runlines0, *args, **kwargs)
49 return self._gitcmd(self.runlines0, *args, **kwargs)
49
50
50 def gitrunlines(self, *args, **kwargs):
51 def gitrunlines(self, *args, **kwargs):
51 return self._gitcmd(self.runlines, *args, **kwargs)
52 return self._gitcmd(self.runlines, *args, **kwargs)
52
53
53 def gitpipe(self, *args, **kwargs):
54 def gitpipe(self, *args, **kwargs):
54 return self._gitcmd(self._run3, *args, **kwargs)
55 return self._gitcmd(self._run3, *args, **kwargs)
55
56
56 def __init__(self, ui, path, revs=None):
57 def __init__(self, ui, path, revs=None):
57 super(convert_git, self).__init__(ui, path, revs=revs)
58 super(convert_git, self).__init__(ui, path, revs=revs)
58 common.commandline.__init__(self, ui, 'git')
59 common.commandline.__init__(self, ui, 'git')
59
60
60 # Pass an absolute path to git to prevent from ever being interpreted
61 # Pass an absolute path to git to prevent from ever being interpreted
61 # as a URL
62 # as a URL
62 path = os.path.abspath(path)
63 path = os.path.abspath(path)
63
64
64 if os.path.isdir(path + "/.git"):
65 if os.path.isdir(path + "/.git"):
65 path += "/.git"
66 path += "/.git"
66 if not os.path.exists(path + "/objects"):
67 if not os.path.exists(path + "/objects"):
67 raise common.NoRepo(_("%s does not look like a Git repository") %
68 raise common.NoRepo(_("%s does not look like a Git repository") %
68 path)
69 path)
69
70
70 # The default value (50) is based on the default for 'git diff'.
71 # The default value (50) is based on the default for 'git diff'.
71 similarity = ui.configint('convert', 'git.similarity', default=50)
72 similarity = ui.configint('convert', 'git.similarity', default=50)
72 if similarity < 0 or similarity > 100:
73 if similarity < 0 or similarity > 100:
73 raise error.Abort(_('similarity must be between 0 and 100'))
74 raise error.Abort(_('similarity must be between 0 and 100'))
74 if similarity > 0:
75 if similarity > 0:
75 self.simopt = ['-C%d%%' % similarity]
76 self.simopt = ['-C%d%%' % similarity]
76 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
77 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
77 False)
78 False)
78 if findcopiesharder:
79 if findcopiesharder:
79 self.simopt.append('--find-copies-harder')
80 self.simopt.append('--find-copies-harder')
80 else:
81 else:
81 self.simopt = []
82 self.simopt = []
82
83
83 common.checktool('git', 'git')
84 common.checktool('git', 'git')
84
85
85 self.path = path
86 self.path = path
86 self.submodules = []
87 self.submodules = []
87
88
88 self.catfilepipe = self.gitpipe('cat-file', '--batch')
89 self.catfilepipe = self.gitpipe('cat-file', '--batch')
89
90
90 def after(self):
91 def after(self):
91 for f in self.catfilepipe:
92 for f in self.catfilepipe:
92 f.close()
93 f.close()
93
94
94 def getheads(self):
95 def getheads(self):
95 if not self.revs:
96 if not self.revs:
96 output, status = self.gitrun('rev-parse', '--branches', '--remotes')
97 output, status = self.gitrun('rev-parse', '--branches', '--remotes')
97 heads = output.splitlines()
98 heads = output.splitlines()
98 if status:
99 if status:
99 raise error.Abort(_('cannot retrieve git heads'))
100 raise error.Abort(_('cannot retrieve git heads'))
100 else:
101 else:
101 heads = []
102 heads = []
102 for rev in self.revs:
103 for rev in self.revs:
103 rawhead, ret = self.gitrun('rev-parse', '--verify', rev)
104 rawhead, ret = self.gitrun('rev-parse', '--verify', rev)
104 heads.append(rawhead[:-1])
105 heads.append(rawhead[:-1])
105 if ret:
106 if ret:
106 raise error.Abort(_('cannot retrieve git head "%s"') % rev)
107 raise error.Abort(_('cannot retrieve git head "%s"') % rev)
107 return heads
108 return heads
108
109
109 def catfile(self, rev, type):
110 def catfile(self, rev, type):
110 if rev == nodemod.nullhex:
111 if rev == nodemod.nullhex:
111 raise IOError
112 raise IOError
112 self.catfilepipe[0].write(rev+'\n')
113 self.catfilepipe[0].write(rev+'\n')
113 self.catfilepipe[0].flush()
114 self.catfilepipe[0].flush()
114 info = self.catfilepipe[1].readline().split()
115 info = self.catfilepipe[1].readline().split()
115 if info[1] != type:
116 if info[1] != type:
116 raise error.Abort(_('cannot read %r object at %s') % (type, rev))
117 raise error.Abort(_('cannot read %r object at %s') % (type, rev))
117 size = int(info[2])
118 size = int(info[2])
118 data = self.catfilepipe[1].read(size)
119 data = self.catfilepipe[1].read(size)
119 if len(data) < size:
120 if len(data) < size:
120 raise error.Abort(_('cannot read %r object at %s: unexpected size')
121 raise error.Abort(_('cannot read %r object at %s: unexpected size')
121 % (type, rev))
122 % (type, rev))
122 # read the trailing newline
123 # read the trailing newline
123 self.catfilepipe[1].read(1)
124 self.catfilepipe[1].read(1)
124 return data
125 return data
125
126
126 def getfile(self, name, rev):
127 def getfile(self, name, rev):
127 if rev == nodemod.nullhex:
128 if rev == nodemod.nullhex:
128 return None, None
129 return None, None
129 if name == '.hgsub':
130 if name == '.hgsub':
130 data = '\n'.join([m.hgsub() for m in self.submoditer()])
131 data = '\n'.join([m.hgsub() for m in self.submoditer()])
131 mode = ''
132 mode = ''
132 elif name == '.hgsubstate':
133 elif name == '.hgsubstate':
133 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
134 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
134 mode = ''
135 mode = ''
135 else:
136 else:
136 data = self.catfile(rev, "blob")
137 data = self.catfile(rev, "blob")
137 mode = self.modecache[(name, rev)]
138 mode = self.modecache[(name, rev)]
138 return data, mode
139 return data, mode
139
140
140 def submoditer(self):
141 def submoditer(self):
141 null = nodemod.nullhex
142 null = nodemod.nullhex
142 for m in sorted(self.submodules, key=lambda p: p.path):
143 for m in sorted(self.submodules, key=lambda p: p.path):
143 if m.node != null:
144 if m.node != null:
144 yield m
145 yield m
145
146
146 def parsegitmodules(self, content):
147 def parsegitmodules(self, content):
147 """Parse the formatted .gitmodules file, example file format:
148 """Parse the formatted .gitmodules file, example file format:
148 [submodule "sub"]\n
149 [submodule "sub"]\n
149 \tpath = sub\n
150 \tpath = sub\n
150 \turl = git://giturl\n
151 \turl = git://giturl\n
151 """
152 """
152 self.submodules = []
153 self.submodules = []
153 c = config.config()
154 c = config.config()
154 # Each item in .gitmodules starts with whitespace that cant be parsed
155 # Each item in .gitmodules starts with whitespace that cant be parsed
155 c.parse('.gitmodules', '\n'.join(line.strip() for line in
156 c.parse('.gitmodules', '\n'.join(line.strip() for line in
156 content.split('\n')))
157 content.split('\n')))
157 for sec in c.sections():
158 for sec in c.sections():
158 s = c[sec]
159 s = c[sec]
159 if 'url' in s and 'path' in s:
160 if 'url' in s and 'path' in s:
160 self.submodules.append(submodule(s['path'], '', s['url']))
161 self.submodules.append(submodule(s['path'], '', s['url']))
161
162
162 def retrievegitmodules(self, version):
163 def retrievegitmodules(self, version):
163 modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules'))
164 modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules'))
164 if ret:
165 if ret:
165 # This can happen if a file is in the repo that has permissions
166 # This can happen if a file is in the repo that has permissions
166 # 160000, but there is no .gitmodules file.
167 # 160000, but there is no .gitmodules file.
167 self.ui.warn(_("warning: cannot read submodules config file in "
168 self.ui.warn(_("warning: cannot read submodules config file in "
168 "%s\n") % version)
169 "%s\n") % version)
169 return
170 return
170
171
171 try:
172 try:
172 self.parsegitmodules(modules)
173 self.parsegitmodules(modules)
173 except error.ParseError:
174 except error.ParseError:
174 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
175 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
175 % version)
176 % version)
176 return
177 return
177
178
178 for m in self.submodules:
179 for m in self.submodules:
179 node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path))
180 node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path))
180 if ret:
181 if ret:
181 continue
182 continue
182 m.node = node.strip()
183 m.node = node.strip()
183
184
184 def getchanges(self, version, full):
185 def getchanges(self, version, full):
185 if full:
186 if full:
186 raise error.Abort(_("convert from git does not support --full"))
187 raise error.Abort(_("convert from git does not support --full"))
187 self.modecache = {}
188 self.modecache = {}
188 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
189 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
189 output, status = self.gitrun(*cmd)
190 output, status = self.gitrun(*cmd)
190 if status:
191 if status:
191 raise error.Abort(_('cannot read changes in %s') % version)
192 raise error.Abort(_('cannot read changes in %s') % version)
192 changes = []
193 changes = []
193 copies = {}
194 copies = {}
194 seen = set()
195 seen = set()
195 entry = None
196 entry = None
196 subexists = [False]
197 subexists = [False]
197 subdeleted = [False]
198 subdeleted = [False]
198 difftree = output.split('\x00')
199 difftree = output.split('\x00')
199 lcount = len(difftree)
200 lcount = len(difftree)
200 i = 0
201 i = 0
201
202
202 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules',
203 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules',
203 False)
204 False)
204 def add(entry, f, isdest):
205 def add(entry, f, isdest):
205 seen.add(f)
206 seen.add(f)
206 h = entry[3]
207 h = entry[3]
207 p = (entry[1] == "100755")
208 p = (entry[1] == "100755")
208 s = (entry[1] == "120000")
209 s = (entry[1] == "120000")
209 renamesource = (not isdest and entry[4][0] == 'R')
210 renamesource = (not isdest and entry[4][0] == 'R')
210
211
211 if f == '.gitmodules':
212 if f == '.gitmodules':
212 if skipsubmodules:
213 if skipsubmodules:
213 return
214 return
214
215
215 subexists[0] = True
216 subexists[0] = True
216 if entry[4] == 'D' or renamesource:
217 if entry[4] == 'D' or renamesource:
217 subdeleted[0] = True
218 subdeleted[0] = True
218 changes.append(('.hgsub', nodemod.nullhex))
219 changes.append(('.hgsub', nodemod.nullhex))
219 else:
220 else:
220 changes.append(('.hgsub', ''))
221 changes.append(('.hgsub', ''))
221 elif entry[1] == '160000' or entry[0] == ':160000':
222 elif entry[1] == '160000' or entry[0] == ':160000':
222 if not skipsubmodules:
223 if not skipsubmodules:
223 subexists[0] = True
224 subexists[0] = True
224 else:
225 else:
225 if renamesource:
226 if renamesource:
226 h = nodemod.nullhex
227 h = nodemod.nullhex
227 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
228 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
228 changes.append((f, h))
229 changes.append((f, h))
229
230
230 while i < lcount:
231 while i < lcount:
231 l = difftree[i]
232 l = difftree[i]
232 i += 1
233 i += 1
233 if not entry:
234 if not entry:
234 if not l.startswith(':'):
235 if not l.startswith(':'):
235 continue
236 continue
236 entry = l.split()
237 entry = l.split()
237 continue
238 continue
238 f = l
239 f = l
239 if entry[4][0] == 'C':
240 if entry[4][0] == 'C':
240 copysrc = f
241 copysrc = f
241 copydest = difftree[i]
242 copydest = difftree[i]
242 i += 1
243 i += 1
243 f = copydest
244 f = copydest
244 copies[copydest] = copysrc
245 copies[copydest] = copysrc
245 if f not in seen:
246 if f not in seen:
246 add(entry, f, False)
247 add(entry, f, False)
247 # A file can be copied multiple times, or modified and copied
248 # A file can be copied multiple times, or modified and copied
248 # simultaneously. So f can be repeated even if fdest isn't.
249 # simultaneously. So f can be repeated even if fdest isn't.
249 if entry[4][0] == 'R':
250 if entry[4][0] == 'R':
250 # rename: next line is the destination
251 # rename: next line is the destination
251 fdest = difftree[i]
252 fdest = difftree[i]
252 i += 1
253 i += 1
253 if fdest not in seen:
254 if fdest not in seen:
254 add(entry, fdest, True)
255 add(entry, fdest, True)
255 # .gitmodules isn't imported at all, so it being copied to
256 # .gitmodules isn't imported at all, so it being copied to
256 # and fro doesn't really make sense
257 # and fro doesn't really make sense
257 if f != '.gitmodules' and fdest != '.gitmodules':
258 if f != '.gitmodules' and fdest != '.gitmodules':
258 copies[fdest] = f
259 copies[fdest] = f
259 entry = None
260 entry = None
260
261
261 if subexists[0]:
262 if subexists[0]:
262 if subdeleted[0]:
263 if subdeleted[0]:
263 changes.append(('.hgsubstate', nodemod.nullhex))
264 changes.append(('.hgsubstate', nodemod.nullhex))
264 else:
265 else:
265 self.retrievegitmodules(version)
266 self.retrievegitmodules(version)
266 changes.append(('.hgsubstate', ''))
267 changes.append(('.hgsubstate', ''))
267 return (changes, copies, set())
268 return (changes, copies, set())
268
269
269 def getcommit(self, version):
270 def getcommit(self, version):
270 c = self.catfile(version, "commit") # read the commit hash
271 c = self.catfile(version, "commit") # read the commit hash
271 end = c.find("\n\n")
272 end = c.find("\n\n")
272 message = c[end + 2:]
273 message = c[end + 2:]
273 message = self.recode(message)
274 message = self.recode(message)
274 l = c[:end].splitlines()
275 l = c[:end].splitlines()
275 parents = []
276 parents = []
276 author = committer = None
277 author = committer = None
277 for e in l[1:]:
278 for e in l[1:]:
278 n, v = e.split(" ", 1)
279 n, v = e.split(" ", 1)
279 if n == "author":
280 if n == "author":
280 p = v.split()
281 p = v.split()
281 tm, tz = p[-2:]
282 tm, tz = p[-2:]
282 author = " ".join(p[:-2])
283 author = " ".join(p[:-2])
283 if author[0] == "<": author = author[1:-1]
284 if author[0] == "<": author = author[1:-1]
284 author = self.recode(author)
285 author = self.recode(author)
285 if n == "committer":
286 if n == "committer":
286 p = v.split()
287 p = v.split()
287 tm, tz = p[-2:]
288 tm, tz = p[-2:]
288 committer = " ".join(p[:-2])
289 committer = " ".join(p[:-2])
289 if committer[0] == "<": committer = committer[1:-1]
290 if committer[0] == "<": committer = committer[1:-1]
290 committer = self.recode(committer)
291 committer = self.recode(committer)
291 if n == "parent":
292 if n == "parent":
292 parents.append(v)
293 parents.append(v)
293
294
294 if committer and committer != author:
295 if committer and committer != author:
295 message += "\ncommitter: %s\n" % committer
296 message += "\ncommitter: %s\n" % committer
296 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
297 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
297 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
298 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
298 date = tm + " " + str(tz)
299 date = tm + " " + str(tz)
299
300
300 c = common.commit(parents=parents, date=date, author=author,
301 c = common.commit(parents=parents, date=date, author=author,
301 desc=message,
302 desc=message,
302 rev=version)
303 rev=version)
303 return c
304 return c
304
305
305 def numcommits(self):
306 def numcommits(self):
306 output, ret = self.gitrunlines('rev-list', '--all')
307 output, ret = self.gitrunlines('rev-list', '--all')
307 if ret:
308 if ret:
308 raise error.Abort(_('cannot retrieve number of commits in %s') \
309 raise error.Abort(_('cannot retrieve number of commits in %s') \
309 % self.path)
310 % self.path)
310 return len(output)
311 return len(output)
311
312
312 def gettags(self):
313 def gettags(self):
313 tags = {}
314 tags = {}
314 alltags = {}
315 alltags = {}
315 output, status = self.gitrunlines('ls-remote', '--tags', self.path)
316 output, status = self.gitrunlines('ls-remote', '--tags', self.path)
316
317
317 if status:
318 if status:
318 raise error.Abort(_('cannot read tags from %s') % self.path)
319 raise error.Abort(_('cannot read tags from %s') % self.path)
319 prefix = 'refs/tags/'
320 prefix = 'refs/tags/'
320
321
321 # Build complete list of tags, both annotated and bare ones
322 # Build complete list of tags, both annotated and bare ones
322 for line in output:
323 for line in output:
323 line = line.strip()
324 line = line.strip()
324 if line.startswith("error:") or line.startswith("fatal:"):
325 if line.startswith("error:") or line.startswith("fatal:"):
325 raise error.Abort(_('cannot read tags from %s') % self.path)
326 raise error.Abort(_('cannot read tags from %s') % self.path)
326 node, tag = line.split(None, 1)
327 node, tag = line.split(None, 1)
327 if not tag.startswith(prefix):
328 if not tag.startswith(prefix):
328 continue
329 continue
329 alltags[tag[len(prefix):]] = node
330 alltags[tag[len(prefix):]] = node
330
331
331 # Filter out tag objects for annotated tag refs
332 # Filter out tag objects for annotated tag refs
332 for tag in alltags:
333 for tag in alltags:
333 if tag.endswith('^{}'):
334 if tag.endswith('^{}'):
334 tags[tag[:-3]] = alltags[tag]
335 tags[tag[:-3]] = alltags[tag]
335 else:
336 else:
336 if tag + '^{}' in alltags:
337 if tag + '^{}' in alltags:
337 continue
338 continue
338 else:
339 else:
339 tags[tag] = alltags[tag]
340 tags[tag] = alltags[tag]
340
341
341 return tags
342 return tags
342
343
343 def getchangedfiles(self, version, i):
344 def getchangedfiles(self, version, i):
344 changes = []
345 changes = []
345 if i is None:
346 if i is None:
346 output, status = self.gitrunlines('diff-tree', '--root', '-m',
347 output, status = self.gitrunlines('diff-tree', '--root', '-m',
347 '-r', version)
348 '-r', version)
348 if status:
349 if status:
349 raise error.Abort(_('cannot read changes in %s') % version)
350 raise error.Abort(_('cannot read changes in %s') % version)
350 for l in output:
351 for l in output:
351 if "\t" not in l:
352 if "\t" not in l:
352 continue
353 continue
353 m, f = l[:-1].split("\t")
354 m, f = l[:-1].split("\t")
354 changes.append(f)
355 changes.append(f)
355 else:
356 else:
356 output, status = self.gitrunlines('diff-tree', '--name-only',
357 output, status = self.gitrunlines('diff-tree', '--name-only',
357 '--root', '-r', version,
358 '--root', '-r', version,
358 '%s^%s' % (version, i + 1), '--')
359 '%s^%s' % (version, i + 1), '--')
359 if status:
360 if status:
360 raise error.Abort(_('cannot read changes in %s') % version)
361 raise error.Abort(_('cannot read changes in %s') % version)
361 changes = [f.rstrip('\n') for f in output]
362 changes = [f.rstrip('\n') for f in output]
362
363
363 return changes
364 return changes
364
365
365 def getbookmarks(self):
366 def getbookmarks(self):
366 bookmarks = {}
367 bookmarks = {}
367
368
368 # Handle local and remote branches
369 # Handle local and remote branches
369 remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote')
370 remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote')
370 reftypes = [
371 reftypes = [
371 # (git prefix, hg prefix)
372 # (git prefix, hg prefix)
372 ('refs/remotes/origin/', remoteprefix + '/'),
373 ('refs/remotes/origin/', remoteprefix + '/'),
373 ('refs/heads/', '')
374 ('refs/heads/', '')
374 ]
375 ]
375
376
376 exclude = set([
377 exclude = set([
377 'refs/remotes/origin/HEAD',
378 'refs/remotes/origin/HEAD',
378 ])
379 ])
379
380
380 try:
381 try:
381 output, status = self.gitrunlines('show-ref')
382 output, status = self.gitrunlines('show-ref')
382 for line in output:
383 for line in output:
383 line = line.strip()
384 line = line.strip()
384 rev, name = line.split(None, 1)
385 rev, name = line.split(None, 1)
385 # Process each type of branch
386 # Process each type of branch
386 for gitprefix, hgprefix in reftypes:
387 for gitprefix, hgprefix in reftypes:
387 if not name.startswith(gitprefix) or name in exclude:
388 if not name.startswith(gitprefix) or name in exclude:
388 continue
389 continue
389 name = '%s%s' % (hgprefix, name[len(gitprefix):])
390 name = '%s%s' % (hgprefix, name[len(gitprefix):])
390 bookmarks[name] = rev
391 bookmarks[name] = rev
391 except Exception:
392 except Exception:
392 pass
393 pass
393
394
394 return bookmarks
395 return bookmarks
395
396
396 def checkrevformat(self, revstr, mapname='splicemap'):
397 def checkrevformat(self, revstr, mapname='splicemap'):
397 """ git revision string is a 40 byte hex """
398 """ git revision string is a 40 byte hex """
398 self.checkhexformat(revstr, mapname)
399 self.checkhexformat(revstr, mapname)
@@ -1,351 +1,352
1 # gnuarch.py - GNU Arch support for the convert extension
1 # gnuarch.py - GNU Arch support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 # and others
4 # and others
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import email
10 import email
11 import os
11 import os
12 import shutil
12 import shutil
13 import stat
13 import stat
14 import tempfile
14 import tempfile
15
16 from mercurial.i18n import _
15 from mercurial import (
17 from mercurial import (
16 encoding,
18 encoding,
17 error,
19 error,
18 util,
20 util,
19 )
21 )
20 from mercurial.i18n import _
21 from . import common
22 from . import common
22
23
23 class gnuarch_source(common.converter_source, common.commandline):
24 class gnuarch_source(common.converter_source, common.commandline):
24
25
25 class gnuarch_rev(object):
26 class gnuarch_rev(object):
26 def __init__(self, rev):
27 def __init__(self, rev):
27 self.rev = rev
28 self.rev = rev
28 self.summary = ''
29 self.summary = ''
29 self.date = None
30 self.date = None
30 self.author = ''
31 self.author = ''
31 self.continuationof = None
32 self.continuationof = None
32 self.add_files = []
33 self.add_files = []
33 self.mod_files = []
34 self.mod_files = []
34 self.del_files = []
35 self.del_files = []
35 self.ren_files = {}
36 self.ren_files = {}
36 self.ren_dirs = {}
37 self.ren_dirs = {}
37
38
38 def __init__(self, ui, path, revs=None):
39 def __init__(self, ui, path, revs=None):
39 super(gnuarch_source, self).__init__(ui, path, revs=revs)
40 super(gnuarch_source, self).__init__(ui, path, revs=revs)
40
41
41 if not os.path.exists(os.path.join(path, '{arch}')):
42 if not os.path.exists(os.path.join(path, '{arch}')):
42 raise common.NoRepo(_("%s does not look like a GNU Arch repository")
43 raise common.NoRepo(_("%s does not look like a GNU Arch repository")
43 % path)
44 % path)
44
45
45 # Could use checktool, but we want to check for baz or tla.
46 # Could use checktool, but we want to check for baz or tla.
46 self.execmd = None
47 self.execmd = None
47 if util.findexe('baz'):
48 if util.findexe('baz'):
48 self.execmd = 'baz'
49 self.execmd = 'baz'
49 else:
50 else:
50 if util.findexe('tla'):
51 if util.findexe('tla'):
51 self.execmd = 'tla'
52 self.execmd = 'tla'
52 else:
53 else:
53 raise error.Abort(_('cannot find a GNU Arch tool'))
54 raise error.Abort(_('cannot find a GNU Arch tool'))
54
55
55 common.commandline.__init__(self, ui, self.execmd)
56 common.commandline.__init__(self, ui, self.execmd)
56
57
57 self.path = os.path.realpath(path)
58 self.path = os.path.realpath(path)
58 self.tmppath = None
59 self.tmppath = None
59
60
60 self.treeversion = None
61 self.treeversion = None
61 self.lastrev = None
62 self.lastrev = None
62 self.changes = {}
63 self.changes = {}
63 self.parents = {}
64 self.parents = {}
64 self.tags = {}
65 self.tags = {}
65 self.catlogparser = email.Parser.Parser()
66 self.catlogparser = email.Parser.Parser()
66 self.encoding = encoding.encoding
67 self.encoding = encoding.encoding
67 self.archives = []
68 self.archives = []
68
69
69 def before(self):
70 def before(self):
70 # Get registered archives
71 # Get registered archives
71 self.archives = [i.rstrip('\n')
72 self.archives = [i.rstrip('\n')
72 for i in self.runlines0('archives', '-n')]
73 for i in self.runlines0('archives', '-n')]
73
74
74 if self.execmd == 'tla':
75 if self.execmd == 'tla':
75 output = self.run0('tree-version', self.path)
76 output = self.run0('tree-version', self.path)
76 else:
77 else:
77 output = self.run0('tree-version', '-d', self.path)
78 output = self.run0('tree-version', '-d', self.path)
78 self.treeversion = output.strip()
79 self.treeversion = output.strip()
79
80
80 # Get name of temporary directory
81 # Get name of temporary directory
81 version = self.treeversion.split('/')
82 version = self.treeversion.split('/')
82 self.tmppath = os.path.join(tempfile.gettempdir(),
83 self.tmppath = os.path.join(tempfile.gettempdir(),
83 'hg-%s' % version[1])
84 'hg-%s' % version[1])
84
85
85 # Generate parents dictionary
86 # Generate parents dictionary
86 self.parents[None] = []
87 self.parents[None] = []
87 treeversion = self.treeversion
88 treeversion = self.treeversion
88 child = None
89 child = None
89 while treeversion:
90 while treeversion:
90 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
91 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
91
92
92 archive = treeversion.split('/')[0]
93 archive = treeversion.split('/')[0]
93 if archive not in self.archives:
94 if archive not in self.archives:
94 self.ui.status(_('tree analysis stopped because it points to '
95 self.ui.status(_('tree analysis stopped because it points to '
95 'an unregistered archive %s...\n') % archive)
96 'an unregistered archive %s...\n') % archive)
96 break
97 break
97
98
98 # Get the complete list of revisions for that tree version
99 # Get the complete list of revisions for that tree version
99 output, status = self.runlines('revisions', '-r', '-f', treeversion)
100 output, status = self.runlines('revisions', '-r', '-f', treeversion)
100 self.checkexit(status, 'failed retrieving revisions for %s'
101 self.checkexit(status, 'failed retrieving revisions for %s'
101 % treeversion)
102 % treeversion)
102
103
103 # No new iteration unless a revision has a continuation-of header
104 # No new iteration unless a revision has a continuation-of header
104 treeversion = None
105 treeversion = None
105
106
106 for l in output:
107 for l in output:
107 rev = l.strip()
108 rev = l.strip()
108 self.changes[rev] = self.gnuarch_rev(rev)
109 self.changes[rev] = self.gnuarch_rev(rev)
109 self.parents[rev] = []
110 self.parents[rev] = []
110
111
111 # Read author, date and summary
112 # Read author, date and summary
112 catlog, status = self.run('cat-log', '-d', self.path, rev)
113 catlog, status = self.run('cat-log', '-d', self.path, rev)
113 if status:
114 if status:
114 catlog = self.run0('cat-archive-log', rev)
115 catlog = self.run0('cat-archive-log', rev)
115 self._parsecatlog(catlog, rev)
116 self._parsecatlog(catlog, rev)
116
117
117 # Populate the parents map
118 # Populate the parents map
118 self.parents[child].append(rev)
119 self.parents[child].append(rev)
119
120
120 # Keep track of the current revision as the child of the next
121 # Keep track of the current revision as the child of the next
121 # revision scanned
122 # revision scanned
122 child = rev
123 child = rev
123
124
124 # Check if we have to follow the usual incremental history
125 # Check if we have to follow the usual incremental history
125 # or if we have to 'jump' to a different treeversion given
126 # or if we have to 'jump' to a different treeversion given
126 # by the continuation-of header.
127 # by the continuation-of header.
127 if self.changes[rev].continuationof:
128 if self.changes[rev].continuationof:
128 treeversion = '--'.join(
129 treeversion = '--'.join(
129 self.changes[rev].continuationof.split('--')[:-1])
130 self.changes[rev].continuationof.split('--')[:-1])
130 break
131 break
131
132
132 # If we reached a base-0 revision w/o any continuation-of
133 # If we reached a base-0 revision w/o any continuation-of
133 # header, it means the tree history ends here.
134 # header, it means the tree history ends here.
134 if rev[-6:] == 'base-0':
135 if rev[-6:] == 'base-0':
135 break
136 break
136
137
137 def after(self):
138 def after(self):
138 self.ui.debug('cleaning up %s\n' % self.tmppath)
139 self.ui.debug('cleaning up %s\n' % self.tmppath)
139 shutil.rmtree(self.tmppath, ignore_errors=True)
140 shutil.rmtree(self.tmppath, ignore_errors=True)
140
141
141 def getheads(self):
142 def getheads(self):
142 return self.parents[None]
143 return self.parents[None]
143
144
144 def getfile(self, name, rev):
145 def getfile(self, name, rev):
145 if rev != self.lastrev:
146 if rev != self.lastrev:
146 raise error.Abort(_('internal calling inconsistency'))
147 raise error.Abort(_('internal calling inconsistency'))
147
148
148 if not os.path.lexists(os.path.join(self.tmppath, name)):
149 if not os.path.lexists(os.path.join(self.tmppath, name)):
149 return None, None
150 return None, None
150
151
151 return self._getfile(name, rev)
152 return self._getfile(name, rev)
152
153
153 def getchanges(self, rev, full):
154 def getchanges(self, rev, full):
154 if full:
155 if full:
155 raise error.Abort(_("convert from arch does not support --full"))
156 raise error.Abort(_("convert from arch does not support --full"))
156 self._update(rev)
157 self._update(rev)
157 changes = []
158 changes = []
158 copies = {}
159 copies = {}
159
160
160 for f in self.changes[rev].add_files:
161 for f in self.changes[rev].add_files:
161 changes.append((f, rev))
162 changes.append((f, rev))
162
163
163 for f in self.changes[rev].mod_files:
164 for f in self.changes[rev].mod_files:
164 changes.append((f, rev))
165 changes.append((f, rev))
165
166
166 for f in self.changes[rev].del_files:
167 for f in self.changes[rev].del_files:
167 changes.append((f, rev))
168 changes.append((f, rev))
168
169
169 for src in self.changes[rev].ren_files:
170 for src in self.changes[rev].ren_files:
170 to = self.changes[rev].ren_files[src]
171 to = self.changes[rev].ren_files[src]
171 changes.append((src, rev))
172 changes.append((src, rev))
172 changes.append((to, rev))
173 changes.append((to, rev))
173 copies[to] = src
174 copies[to] = src
174
175
175 for src in self.changes[rev].ren_dirs:
176 for src in self.changes[rev].ren_dirs:
176 to = self.changes[rev].ren_dirs[src]
177 to = self.changes[rev].ren_dirs[src]
177 chgs, cps = self._rendirchanges(src, to)
178 chgs, cps = self._rendirchanges(src, to)
178 changes += [(f, rev) for f in chgs]
179 changes += [(f, rev) for f in chgs]
179 copies.update(cps)
180 copies.update(cps)
180
181
181 self.lastrev = rev
182 self.lastrev = rev
182 return sorted(set(changes)), copies, set()
183 return sorted(set(changes)), copies, set()
183
184
184 def getcommit(self, rev):
185 def getcommit(self, rev):
185 changes = self.changes[rev]
186 changes = self.changes[rev]
186 return common.commit(author=changes.author, date=changes.date,
187 return common.commit(author=changes.author, date=changes.date,
187 desc=changes.summary, parents=self.parents[rev],
188 desc=changes.summary, parents=self.parents[rev],
188 rev=rev)
189 rev=rev)
189
190
190 def gettags(self):
191 def gettags(self):
191 return self.tags
192 return self.tags
192
193
193 def _execute(self, cmd, *args, **kwargs):
194 def _execute(self, cmd, *args, **kwargs):
194 cmdline = [self.execmd, cmd]
195 cmdline = [self.execmd, cmd]
195 cmdline += args
196 cmdline += args
196 cmdline = [util.shellquote(arg) for arg in cmdline]
197 cmdline = [util.shellquote(arg) for arg in cmdline]
197 cmdline += ['>', os.devnull, '2>', os.devnull]
198 cmdline += ['>', os.devnull, '2>', os.devnull]
198 cmdline = util.quotecommand(' '.join(cmdline))
199 cmdline = util.quotecommand(' '.join(cmdline))
199 self.ui.debug(cmdline, '\n')
200 self.ui.debug(cmdline, '\n')
200 return os.system(cmdline)
201 return os.system(cmdline)
201
202
202 def _update(self, rev):
203 def _update(self, rev):
203 self.ui.debug('applying revision %s...\n' % rev)
204 self.ui.debug('applying revision %s...\n' % rev)
204 changeset, status = self.runlines('replay', '-d', self.tmppath,
205 changeset, status = self.runlines('replay', '-d', self.tmppath,
205 rev)
206 rev)
206 if status:
207 if status:
207 # Something went wrong while merging (baz or tla
208 # Something went wrong while merging (baz or tla
208 # issue?), get latest revision and try from there
209 # issue?), get latest revision and try from there
209 shutil.rmtree(self.tmppath, ignore_errors=True)
210 shutil.rmtree(self.tmppath, ignore_errors=True)
210 self._obtainrevision(rev)
211 self._obtainrevision(rev)
211 else:
212 else:
212 old_rev = self.parents[rev][0]
213 old_rev = self.parents[rev][0]
213 self.ui.debug('computing changeset between %s and %s...\n'
214 self.ui.debug('computing changeset between %s and %s...\n'
214 % (old_rev, rev))
215 % (old_rev, rev))
215 self._parsechangeset(changeset, rev)
216 self._parsechangeset(changeset, rev)
216
217
217 def _getfile(self, name, rev):
218 def _getfile(self, name, rev):
218 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
219 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
219 if stat.S_ISLNK(mode):
220 if stat.S_ISLNK(mode):
220 data = os.readlink(os.path.join(self.tmppath, name))
221 data = os.readlink(os.path.join(self.tmppath, name))
221 if mode:
222 if mode:
222 mode = 'l'
223 mode = 'l'
223 else:
224 else:
224 mode = ''
225 mode = ''
225 else:
226 else:
226 data = open(os.path.join(self.tmppath, name), 'rb').read()
227 data = open(os.path.join(self.tmppath, name), 'rb').read()
227 mode = (mode & 0o111) and 'x' or ''
228 mode = (mode & 0o111) and 'x' or ''
228 return data, mode
229 return data, mode
229
230
230 def _exclude(self, name):
231 def _exclude(self, name):
231 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
232 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
232 for exc in exclude:
233 for exc in exclude:
233 if name.find(exc) != -1:
234 if name.find(exc) != -1:
234 return True
235 return True
235 return False
236 return False
236
237
237 def _readcontents(self, path):
238 def _readcontents(self, path):
238 files = []
239 files = []
239 contents = os.listdir(path)
240 contents = os.listdir(path)
240 while len(contents) > 0:
241 while len(contents) > 0:
241 c = contents.pop()
242 c = contents.pop()
242 p = os.path.join(path, c)
243 p = os.path.join(path, c)
243 # os.walk could be used, but here we avoid internal GNU
244 # os.walk could be used, but here we avoid internal GNU
244 # Arch files and directories, thus saving a lot time.
245 # Arch files and directories, thus saving a lot time.
245 if not self._exclude(p):
246 if not self._exclude(p):
246 if os.path.isdir(p):
247 if os.path.isdir(p):
247 contents += [os.path.join(c, f) for f in os.listdir(p)]
248 contents += [os.path.join(c, f) for f in os.listdir(p)]
248 else:
249 else:
249 files.append(c)
250 files.append(c)
250 return files
251 return files
251
252
252 def _rendirchanges(self, src, dest):
253 def _rendirchanges(self, src, dest):
253 changes = []
254 changes = []
254 copies = {}
255 copies = {}
255 files = self._readcontents(os.path.join(self.tmppath, dest))
256 files = self._readcontents(os.path.join(self.tmppath, dest))
256 for f in files:
257 for f in files:
257 s = os.path.join(src, f)
258 s = os.path.join(src, f)
258 d = os.path.join(dest, f)
259 d = os.path.join(dest, f)
259 changes.append(s)
260 changes.append(s)
260 changes.append(d)
261 changes.append(d)
261 copies[d] = s
262 copies[d] = s
262 return changes, copies
263 return changes, copies
263
264
264 def _obtainrevision(self, rev):
265 def _obtainrevision(self, rev):
265 self.ui.debug('obtaining revision %s...\n' % rev)
266 self.ui.debug('obtaining revision %s...\n' % rev)
266 output = self._execute('get', rev, self.tmppath)
267 output = self._execute('get', rev, self.tmppath)
267 self.checkexit(output)
268 self.checkexit(output)
268 self.ui.debug('analyzing revision %s...\n' % rev)
269 self.ui.debug('analyzing revision %s...\n' % rev)
269 files = self._readcontents(self.tmppath)
270 files = self._readcontents(self.tmppath)
270 self.changes[rev].add_files += files
271 self.changes[rev].add_files += files
271
272
272 def _stripbasepath(self, path):
273 def _stripbasepath(self, path):
273 if path.startswith('./'):
274 if path.startswith('./'):
274 return path[2:]
275 return path[2:]
275 return path
276 return path
276
277
277 def _parsecatlog(self, data, rev):
278 def _parsecatlog(self, data, rev):
278 try:
279 try:
279 catlog = self.catlogparser.parsestr(data)
280 catlog = self.catlogparser.parsestr(data)
280
281
281 # Commit date
282 # Commit date
282 self.changes[rev].date = util.datestr(
283 self.changes[rev].date = util.datestr(
283 util.strdate(catlog['Standard-date'],
284 util.strdate(catlog['Standard-date'],
284 '%Y-%m-%d %H:%M:%S'))
285 '%Y-%m-%d %H:%M:%S'))
285
286
286 # Commit author
287 # Commit author
287 self.changes[rev].author = self.recode(catlog['Creator'])
288 self.changes[rev].author = self.recode(catlog['Creator'])
288
289
289 # Commit description
290 # Commit description
290 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
291 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
291 catlog.get_payload()))
292 catlog.get_payload()))
292 self.changes[rev].summary = self.recode(self.changes[rev].summary)
293 self.changes[rev].summary = self.recode(self.changes[rev].summary)
293
294
294 # Commit revision origin when dealing with a branch or tag
295 # Commit revision origin when dealing with a branch or tag
295 if 'Continuation-of' in catlog:
296 if 'Continuation-of' in catlog:
296 self.changes[rev].continuationof = self.recode(
297 self.changes[rev].continuationof = self.recode(
297 catlog['Continuation-of'])
298 catlog['Continuation-of'])
298 except Exception:
299 except Exception:
299 raise error.Abort(_('could not parse cat-log of %s') % rev)
300 raise error.Abort(_('could not parse cat-log of %s') % rev)
300
301
301 def _parsechangeset(self, data, rev):
302 def _parsechangeset(self, data, rev):
302 for l in data:
303 for l in data:
303 l = l.strip()
304 l = l.strip()
304 # Added file (ignore added directory)
305 # Added file (ignore added directory)
305 if l.startswith('A') and not l.startswith('A/'):
306 if l.startswith('A') and not l.startswith('A/'):
306 file = self._stripbasepath(l[1:].strip())
307 file = self._stripbasepath(l[1:].strip())
307 if not self._exclude(file):
308 if not self._exclude(file):
308 self.changes[rev].add_files.append(file)
309 self.changes[rev].add_files.append(file)
309 # Deleted file (ignore deleted directory)
310 # Deleted file (ignore deleted directory)
310 elif l.startswith('D') and not l.startswith('D/'):
311 elif l.startswith('D') and not l.startswith('D/'):
311 file = self._stripbasepath(l[1:].strip())
312 file = self._stripbasepath(l[1:].strip())
312 if not self._exclude(file):
313 if not self._exclude(file):
313 self.changes[rev].del_files.append(file)
314 self.changes[rev].del_files.append(file)
314 # Modified binary file
315 # Modified binary file
315 elif l.startswith('Mb'):
316 elif l.startswith('Mb'):
316 file = self._stripbasepath(l[2:].strip())
317 file = self._stripbasepath(l[2:].strip())
317 if not self._exclude(file):
318 if not self._exclude(file):
318 self.changes[rev].mod_files.append(file)
319 self.changes[rev].mod_files.append(file)
319 # Modified link
320 # Modified link
320 elif l.startswith('M->'):
321 elif l.startswith('M->'):
321 file = self._stripbasepath(l[3:].strip())
322 file = self._stripbasepath(l[3:].strip())
322 if not self._exclude(file):
323 if not self._exclude(file):
323 self.changes[rev].mod_files.append(file)
324 self.changes[rev].mod_files.append(file)
324 # Modified file
325 # Modified file
325 elif l.startswith('M'):
326 elif l.startswith('M'):
326 file = self._stripbasepath(l[1:].strip())
327 file = self._stripbasepath(l[1:].strip())
327 if not self._exclude(file):
328 if not self._exclude(file):
328 self.changes[rev].mod_files.append(file)
329 self.changes[rev].mod_files.append(file)
329 # Renamed file (or link)
330 # Renamed file (or link)
330 elif l.startswith('=>'):
331 elif l.startswith('=>'):
331 files = l[2:].strip().split(' ')
332 files = l[2:].strip().split(' ')
332 if len(files) == 1:
333 if len(files) == 1:
333 files = l[2:].strip().split('\t')
334 files = l[2:].strip().split('\t')
334 src = self._stripbasepath(files[0])
335 src = self._stripbasepath(files[0])
335 dst = self._stripbasepath(files[1])
336 dst = self._stripbasepath(files[1])
336 if not self._exclude(src) and not self._exclude(dst):
337 if not self._exclude(src) and not self._exclude(dst):
337 self.changes[rev].ren_files[src] = dst
338 self.changes[rev].ren_files[src] = dst
338 # Conversion from file to link or from link to file (modified)
339 # Conversion from file to link or from link to file (modified)
339 elif l.startswith('ch'):
340 elif l.startswith('ch'):
340 file = self._stripbasepath(l[2:].strip())
341 file = self._stripbasepath(l[2:].strip())
341 if not self._exclude(file):
342 if not self._exclude(file):
342 self.changes[rev].mod_files.append(file)
343 self.changes[rev].mod_files.append(file)
343 # Renamed directory
344 # Renamed directory
344 elif l.startswith('/>'):
345 elif l.startswith('/>'):
345 dirs = l[2:].strip().split(' ')
346 dirs = l[2:].strip().split(' ')
346 if len(dirs) == 1:
347 if len(dirs) == 1:
347 dirs = l[2:].strip().split('\t')
348 dirs = l[2:].strip().split('\t')
348 src = self._stripbasepath(dirs[0])
349 src = self._stripbasepath(dirs[0])
349 dst = self._stripbasepath(dirs[1])
350 dst = self._stripbasepath(dirs[1])
350 if not self._exclude(src) and not self._exclude(dst):
351 if not self._exclude(src) and not self._exclude(dst):
351 self.changes[rev].ren_dirs[src] = dst
352 self.changes[rev].ren_dirs[src] = dst
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: 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 hgext/largefiles/basestore.py to hgext/largefiles/storefactory.py
NO CONTENT: file copied from hgext/largefiles/basestore.py to hgext/largefiles/storefactory.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: 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 renamed from tests/test-update-renames.t to tests/test-update-names.t
NO CONTENT: file renamed from tests/test-update-renames.t to tests/test-update-names.t
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: 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 was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
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