##// END OF EJS Templates
cleanup: update references to /help/ that should now be /helptext/...
Augie Fackler -
r44135:640bae94 default
parent child Browse files
Show More
@@ -1,258 +1,258 b''
1 1 # If you want to change PREFIX, do not just edit it below. The changed
2 2 # value wont get passed on to recursive make calls. You should instead
3 3 # override the variable on the command like:
4 4 #
5 5 # % make PREFIX=/opt/ install
6 6
7 7 export PREFIX=/usr/local
8 8 PYTHON?=python
9 9 $(eval HGROOT := $(shell pwd))
10 10 HGPYTHONS ?= $(HGROOT)/build/pythons
11 11 PURE=
12 12 PYFILESCMD=find mercurial hgext doc -name '*.py'
13 13 PYFILES:=$(shell $(PYFILESCMD))
14 DOCFILES=mercurial/help/*.txt
14 DOCFILES=mercurial/helptext/*.txt
15 15 export LANGUAGE=C
16 16 export LC_ALL=C
17 17 TESTFLAGS ?= $(shell echo $$HGTESTFLAGS)
18 18 OSXVERSIONFLAGS ?= $(shell echo $$OSXVERSIONFLAGS)
19 19 CARGO = cargo
20 20
21 21 # Set this to e.g. "mingw32" to use a non-default compiler.
22 22 COMPILER=
23 23
24 24 COMPILERFLAG_tmp_ =
25 25 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
26 26 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
27 27
28 28 help:
29 29 @echo 'Commonly used make targets:'
30 30 @echo ' all - build program and documentation'
31 31 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
32 32 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
33 33 @echo ' local - build for inplace usage'
34 34 @echo ' tests - run all tests in the automatic test suite'
35 35 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
36 36 @echo ' dist - run all tests and create a source tarball in dist/'
37 37 @echo ' clean - remove files created by other targets'
38 38 @echo ' (except installed files or dist source tarball)'
39 39 @echo ' update-pot - update i18n/hg.pot'
40 40 @echo
41 41 @echo 'Example for a system-wide installation under /usr/local:'
42 42 @echo ' make all && su -c "make install" && hg version'
43 43 @echo
44 44 @echo 'Example for a local installation (usable in this directory):'
45 45 @echo ' make local && ./hg version'
46 46
47 47 all: build doc
48 48
49 49 local:
50 50 $(PYTHON) setup.py $(PURE) \
51 51 build_py -c -d . \
52 52 build_ext $(COMPILERFLAG) -i \
53 53 build_hgexe $(COMPILERFLAG) -i \
54 54 build_mo
55 55 env HGRCPATH= $(PYTHON) hg version
56 56
57 57 build:
58 58 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
59 59
60 60 wheel:
61 61 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
62 62
63 63 doc:
64 64 $(MAKE) -C doc
65 65
66 66 cleanbutpackages:
67 67 -$(PYTHON) setup.py clean --all # ignore errors from this command
68 68 find contrib doc hgext hgext3rd i18n mercurial tests hgdemandimport \
69 69 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
70 70 rm -f MANIFEST MANIFEST.in hgext/__index__.py tests/*.err
71 71 rm -f mercurial/__modulepolicy__.py
72 72 if test -d .hg; then rm -f mercurial/__version__.py; fi
73 73 rm -rf build mercurial/locale
74 74 $(MAKE) -C doc clean
75 75 $(MAKE) -C contrib/chg distclean
76 76 rm -rf rust/target
77 77 rm -f mercurial/rustext.so
78 78
79 79 clean: cleanbutpackages
80 80 rm -rf packages
81 81
82 82 install: install-bin install-doc
83 83
84 84 install-bin: build
85 85 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
86 86
87 87 install-doc: doc
88 88 cd doc && $(MAKE) $(MFLAGS) install
89 89
90 90 install-home: install-home-bin install-home-doc
91 91
92 92 install-home-bin: build
93 93 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
94 94
95 95 install-home-doc: doc
96 96 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
97 97
98 98 MANIFEST-doc:
99 99 $(MAKE) -C doc MANIFEST
100 100
101 101 MANIFEST.in: MANIFEST-doc
102 102 hg manifest | sed -e 's/^/include /' > MANIFEST.in
103 103 echo include mercurial/__version__.py >> MANIFEST.in
104 104 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
105 105
106 106 dist: tests dist-notests
107 107
108 108 dist-notests: doc MANIFEST.in
109 109 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
110 110
111 111 check: tests
112 112
113 113 tests:
114 114 # Run Rust tests if cargo is installed
115 115 if command -v $(CARGO) >/dev/null 2>&1; then \
116 116 $(MAKE) rust-tests; \
117 117 fi
118 118 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
119 119
120 120 test-%:
121 121 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
122 122
123 123 testpy-%:
124 124 @echo Looking for Python $* in $(HGPYTHONS)
125 125 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
126 126 cd $$(mktemp --directory --tmpdir) && \
127 127 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
128 128 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
129 129
130 130 rust-tests: py_feature = $(shell $(PYTHON) -c \
131 131 'import sys; print(["python27-bin", "python3-bin"][sys.version_info[0] >= 3])')
132 132 rust-tests:
133 133 cd $(HGROOT)/rust/hg-cpython \
134 134 && $(CARGO) test --quiet --all \
135 135 --no-default-features --features "$(py_feature)"
136 136
137 137 check-code:
138 138 hg manifest | xargs python contrib/check-code.py
139 139
140 140 format-c:
141 141 clang-format --style file -i \
142 142 `hg files 'set:(**.c or **.cc or **.h) and not "listfile:contrib/clang-format-ignorelist"'`
143 143
144 144 update-pot: i18n/hg.pot
145 145
146 146 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
147 147 $(PYTHON) i18n/hggettext mercurial/commands.py \
148 148 hgext/*.py hgext/*/__init__.py \
149 149 mercurial/fileset.py mercurial/revset.py \
150 150 mercurial/templatefilters.py \
151 151 mercurial/templatefuncs.py \
152 152 mercurial/templatekw.py \
153 153 mercurial/filemerge.py \
154 154 mercurial/hgweb/webcommands.py \
155 155 mercurial/util.py \
156 156 $(DOCFILES) > i18n/hg.pot.tmp
157 157 # All strings marked for translation in Mercurial contain
158 158 # ASCII characters only. But some files contain string
159 159 # literals like this '\037\213'. xgettext thinks it has to
160 160 # parse them even though they are not marked for translation.
161 161 # Extracting with an explicit encoding of ISO-8859-1 will make
162 162 # xgettext "parse" and ignore them.
163 163 $(PYFILESCMD) | xargs \
164 164 xgettext --package-name "Mercurial" \
165 165 --msgid-bugs-address "<mercurial-devel@mercurial-scm.org>" \
166 166 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
167 167 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
168 168 -d hg -p i18n -o hg.pot.tmp
169 169 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
170 170 # The target file is not created before the last step. So it never is in
171 171 # an intermediate state.
172 172 mv -f i18n/hg.pot.tmp i18n/hg.pot
173 173
174 174 %.po: i18n/hg.pot
175 175 # work on a temporary copy for never having a half completed target
176 176 cp $@ $@.tmp
177 177 msgmerge --no-location --update $@.tmp $^
178 178 mv -f $@.tmp $@
179 179
180 180 # Packaging targets
181 181
182 182 packaging_targets := \
183 183 centos5 \
184 184 centos6 \
185 185 centos7 \
186 186 centos8 \
187 187 deb \
188 188 docker-centos5 \
189 189 docker-centos6 \
190 190 docker-centos7 \
191 191 docker-centos8 \
192 192 docker-debian-bullseye \
193 193 docker-debian-buster \
194 194 docker-debian-stretch \
195 195 docker-fedora \
196 196 docker-ubuntu-trusty \
197 197 docker-ubuntu-trusty-ppa \
198 198 docker-ubuntu-xenial \
199 199 docker-ubuntu-xenial-ppa \
200 200 docker-ubuntu-artful \
201 201 docker-ubuntu-artful-ppa \
202 202 docker-ubuntu-bionic \
203 203 docker-ubuntu-bionic-ppa \
204 204 fedora \
205 205 linux-wheels \
206 206 linux-wheels-x86_64 \
207 207 linux-wheels-i686 \
208 208 ppa
209 209
210 210 # Forward packaging targets for convenience.
211 211 $(packaging_targets):
212 212 $(MAKE) -C contrib/packaging $@
213 213
214 214 osx:
215 215 rm -rf build/mercurial
216 216 /usr/bin/python2.7 setup.py install --optimize=1 \
217 217 --root=build/mercurial/ --prefix=/usr/local/ \
218 218 --install-lib=/Library/Python/2.7/site-packages/
219 219 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
220 220 # Place a bogon .DS_Store file in the target dir so we can be
221 221 # sure it doesn't get included in the final package.
222 222 touch build/mercurial/.DS_Store
223 223 # install zsh completions - this location appears to be
224 224 # searched by default as of macOS Sierra.
225 225 install -d build/mercurial/usr/local/share/zsh/site-functions/
226 226 install -m 0644 contrib/zsh_completion build/mercurial/usr/local/share/zsh/site-functions/_hg
227 227 # install bash completions - there doesn't appear to be a
228 228 # place that's searched by default for bash, so we'll follow
229 229 # the lead of Apple's git install and just put it in a
230 230 # location of our own.
231 231 install -d build/mercurial/usr/local/hg/contrib/
232 232 install -m 0644 contrib/bash_completion build/mercurial/usr/local/hg/contrib/hg-completion.bash
233 233 make -C contrib/chg \
234 234 HGPATH=/usr/local/bin/hg \
235 235 PYTHON=/usr/bin/python2.7 \
236 236 HGEXTDIR=/Library/Python/2.7/site-packages/hgext \
237 237 DESTDIR=../../build/mercurial \
238 238 PREFIX=/usr/local \
239 239 clean install
240 240 mkdir -p $${OUTPUTDIR:-dist}
241 241 HGVER=$$(python contrib/genosxversion.py $(OSXVERSIONFLAGS) build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py) && \
242 242 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
243 243 pkgbuild --filter \\.DS_Store --root build/mercurial/ \
244 244 --identifier org.mercurial-scm.mercurial \
245 245 --version "$${HGVER}" \
246 246 build/mercurial.pkg && \
247 247 productbuild --distribution contrib/packaging/macosx/distribution.xml \
248 248 --package-path build/ \
249 249 --version "$${HGVER}" \
250 250 --resources contrib/packaging/macosx/ \
251 251 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
252 252
253 253 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
254 254 install-doc install-home install-home-bin install-home-doc \
255 255 dist dist-notests check tests rust-tests check-code format-c \
256 256 update-pot \
257 257 $(packaging_targets) \
258 258 osx
@@ -1,245 +1,245 b''
1 1 # py2exe.py - Functionality for performing py2exe builds.
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import os
11 11 import pathlib
12 12 import subprocess
13 13
14 14 from .downloads import download_entry
15 15 from .util import (
16 16 extract_tar_to_directory,
17 17 extract_zip_to_directory,
18 18 process_install_rules,
19 19 python_exe_info,
20 20 )
21 21
22 22
23 23 STAGING_RULES = [
24 24 ('contrib/bash_completion', 'Contrib/'),
25 25 ('contrib/hgk', 'Contrib/hgk.tcl'),
26 26 ('contrib/hgweb.fcgi', 'Contrib/'),
27 27 ('contrib/hgweb.wsgi', 'Contrib/'),
28 28 ('contrib/logo-droplets.svg', 'Contrib/'),
29 29 ('contrib/mercurial.el', 'Contrib/'),
30 30 ('contrib/mq.el', 'Contrib/'),
31 31 ('contrib/tcsh_completion', 'Contrib/'),
32 32 ('contrib/tcsh_completion_build.sh', 'Contrib/'),
33 33 ('contrib/vim/*', 'Contrib/Vim/'),
34 34 ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
35 35 ('contrib/win32/ReadMe.html', 'ReadMe.html'),
36 36 ('contrib/xml.rnc', 'Contrib/'),
37 37 ('contrib/zsh_completion', 'Contrib/'),
38 38 ('dist/hg.exe', './'),
39 39 ('dist/lib/*.dll', 'lib/'),
40 40 ('dist/lib/*.pyd', 'lib/'),
41 41 ('dist/lib/library.zip', 'lib/'),
42 42 ('dist/Microsoft.VC*.CRT.manifest', './'),
43 43 ('dist/msvc*.dll', './'),
44 44 ('dist/python*.dll', './'),
45 45 ('doc/*.html', 'doc/'),
46 46 ('doc/style.css', 'doc/'),
47 ('mercurial/help/**/*.txt', 'help/'),
47 ('mercurial/helptext/**/*.txt', 'helptext/'),
48 48 ('mercurial/default.d/*.rc', 'hgrc.d/'),
49 49 ('mercurial/locale/**/*', 'locale/'),
50 50 ('mercurial/templates/**/*', 'Templates/'),
51 51 ('COPYING', 'Copying.txt'),
52 52 ]
53 53
54 54 # List of paths to exclude from the staging area.
55 55 STAGING_EXCLUDES = [
56 56 'doc/hg-ssh.8.html',
57 57 ]
58 58
59 59
60 60 def build_py2exe(
61 61 source_dir: pathlib.Path,
62 62 build_dir: pathlib.Path,
63 63 python_exe: pathlib.Path,
64 64 build_name: str,
65 65 venv_requirements_txt: pathlib.Path,
66 66 extra_packages=None,
67 67 extra_excludes=None,
68 68 extra_dll_excludes=None,
69 69 extra_packages_script=None,
70 70 ):
71 71 """Build Mercurial with py2exe.
72 72
73 73 Build files will be placed in ``build_dir``.
74 74
75 75 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
76 76 for finding the Python 2.7 toolchain. So, we require the environment
77 77 to already be configured with an active toolchain.
78 78 """
79 79 if 'VCINSTALLDIR' not in os.environ:
80 80 raise Exception(
81 81 'not running from a Visual C++ build environment; '
82 82 'execute the "Visual C++ <version> Command Prompt" '
83 83 'application shortcut or a vcsvarsall.bat file'
84 84 )
85 85
86 86 # Identity x86/x64 and validate the environment matches the Python
87 87 # architecture.
88 88 vc_x64 = r'\x64' in os.environ['LIB']
89 89
90 90 py_info = python_exe_info(python_exe)
91 91
92 92 if vc_x64:
93 93 if py_info['arch'] != '64bit':
94 94 raise Exception(
95 95 'architecture mismatch: Visual C++ environment '
96 96 'is configured for 64-bit but Python is 32-bit'
97 97 )
98 98 else:
99 99 if py_info['arch'] != '32bit':
100 100 raise Exception(
101 101 'architecture mismatch: Visual C++ environment '
102 102 'is configured for 32-bit but Python is 64-bit'
103 103 )
104 104
105 105 if py_info['py3']:
106 106 raise Exception('Only Python 2 is currently supported')
107 107
108 108 build_dir.mkdir(exist_ok=True)
109 109
110 110 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
111 111 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
112 112 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
113 113 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
114 114
115 115 venv_path = build_dir / (
116 116 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
117 117 )
118 118
119 119 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
120 120
121 121 if not gettext_root.exists():
122 122 extract_zip_to_directory(gettext_pkg, gettext_root)
123 123 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
124 124
125 125 # This assumes Python 2. We don't need virtualenv on Python 3.
126 126 virtualenv_src_path = build_dir / (
127 127 'virtualenv-%s' % virtualenv_entry['version']
128 128 )
129 129 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
130 130
131 131 if not virtualenv_src_path.exists():
132 132 extract_tar_to_directory(virtualenv_pkg, build_dir)
133 133
134 134 py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version'])
135 135
136 136 if not py2exe_source_path.exists():
137 137 extract_zip_to_directory(py2exe_pkg, build_dir)
138 138
139 139 if not venv_path.exists():
140 140 print('creating virtualenv with dependencies')
141 141 subprocess.run(
142 142 [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
143 143 )
144 144
145 145 venv_python = venv_path / 'Scripts' / 'python.exe'
146 146 venv_pip = venv_path / 'Scripts' / 'pip.exe'
147 147
148 148 subprocess.run(
149 149 [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
150 150 )
151 151
152 152 # Force distutils to use VC++ settings from environment, which was
153 153 # validated above.
154 154 env = dict(os.environ)
155 155 env['DISTUTILS_USE_SDK'] = '1'
156 156 env['MSSdk'] = '1'
157 157
158 158 if extra_packages_script:
159 159 more_packages = set(
160 160 subprocess.check_output(extra_packages_script, cwd=build_dir)
161 161 .split(b'\0')[-1]
162 162 .strip()
163 163 .decode('utf-8')
164 164 .splitlines()
165 165 )
166 166 if more_packages:
167 167 if not extra_packages:
168 168 extra_packages = more_packages
169 169 else:
170 170 extra_packages |= more_packages
171 171
172 172 if extra_packages:
173 173 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
174 174 hgext3rd_extras = sorted(
175 175 e for e in extra_packages if e.startswith('hgext3rd.')
176 176 )
177 177 if hgext3rd_extras:
178 178 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
179 179 if extra_excludes:
180 180 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
181 181 if extra_dll_excludes:
182 182 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
183 183 sorted(extra_dll_excludes)
184 184 )
185 185
186 186 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
187 187 if not py2exe_py_path.exists():
188 188 print('building py2exe')
189 189 subprocess.run(
190 190 [str(venv_python), 'setup.py', 'install'],
191 191 cwd=py2exe_source_path,
192 192 env=env,
193 193 check=True,
194 194 )
195 195
196 196 # Register location of msgfmt and other binaries.
197 197 env['PATH'] = '%s%s%s' % (
198 198 env['PATH'],
199 199 os.pathsep,
200 200 str(gettext_root / 'bin'),
201 201 )
202 202
203 203 print('building Mercurial')
204 204 subprocess.run(
205 205 [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
206 206 cwd=str(source_dir),
207 207 env=env,
208 208 check=True,
209 209 )
210 210
211 211
212 212 def stage_install(
213 213 source_dir: pathlib.Path, staging_dir: pathlib.Path, lower_case=False
214 214 ):
215 215 """Copy all files to be installed to a directory.
216 216
217 217 This allows packaging to simply walk a directory tree to find source
218 218 files.
219 219 """
220 220 if lower_case:
221 221 rules = []
222 222 for source, dest in STAGING_RULES:
223 223 # Only lower directory names.
224 224 if '/' in dest:
225 225 parent, leaf = dest.rsplit('/', 1)
226 226 dest = '%s/%s' % (parent.lower(), leaf)
227 227 rules.append((source, dest))
228 228 else:
229 229 rules = STAGING_RULES
230 230
231 231 process_install_rules(rules, source_dir, staging_dir)
232 232
233 233 # Write out a default editor.rc file to configure notepad as the
234 234 # default editor.
235 235 with (staging_dir / 'hgrc.d' / 'editor.rc').open(
236 236 'w', encoding='utf-8'
237 237 ) as fh:
238 238 fh.write('[ui]\neditor = notepad\n')
239 239
240 240 # Purge any files we don't want to be there.
241 241 for f in STAGING_EXCLUDES:
242 242 p = staging_dir / f
243 243 if p.exists():
244 244 print('removing %s' % p)
245 245 p.unlink()
@@ -1,48 +1,48 b''
1 SOURCES=$(notdir $(wildcard ../mercurial/help/*.[0-9].txt))
1 SOURCES=$(notdir $(wildcard ../mercurial/helptext/*.[0-9].txt))
2 2 MAN=$(SOURCES:%.txt=%)
3 3 HTML=$(SOURCES:%.txt=%.html)
4 4 GENDOC=gendoc.py ../mercurial/commands.py ../mercurial/help.py \
5 ../mercurial/help/*.txt ../hgext/*.py ../hgext/*/__init__.py
5 ../mercurial/helptext/*.txt ../hgext/*.py ../hgext/*/__init__.py
6 6 PREFIX=/usr/local
7 7 MANDIR=$(PREFIX)/share/man
8 8 INSTALL=install -c -m 644
9 9 PYTHON?=python
10 10 RSTARGS=
11 11
12 12 export HGENCODING=UTF-8
13 13
14 14 all: man html
15 15
16 16 man: $(MAN)
17 17
18 18 html: $(HTML)
19 19
20 20 # This logic is duplicated in setup.py:hgbuilddoc()
21 21 common.txt $(SOURCES) $(SOURCES:%.txt=%.gendoc.txt): $(GENDOC)
22 22 ${PYTHON} gendoc.py "$(basename $@)" > $@.tmp
23 23 mv $@.tmp $@
24 24
25 25 %: %.txt %.gendoc.txt common.txt
26 26 $(PYTHON) runrst hgmanpage $(RSTARGS) --halt warning \
27 27 --strip-elements-with-class htmlonly $*.txt $*
28 28
29 29 %.html: %.txt %.gendoc.txt common.txt
30 30 $(PYTHON) runrst html $(RSTARGS) --halt warning \
31 31 --link-stylesheet --stylesheet-path style.css $*.txt $*.html
32 32
33 33 MANIFEST: man html
34 34 # tracked files are already in the main MANIFEST
35 35 $(RM) $@
36 36 for i in $(MAN) $(HTML); do \
37 37 echo "doc/$$i" >> $@ ; \
38 38 done
39 39
40 40 install: man
41 41 for i in $(MAN) ; do \
42 42 subdir=`echo $$i | sed -n 's/^.*\.\([0-9]\)$$/man\1/p'` ; \
43 43 mkdir -p "$(DESTDIR)$(MANDIR)"/$$subdir ; \
44 44 $(INSTALL) $$i "$(DESTDIR)$(MANDIR)"/$$subdir ; \
45 45 done
46 46
47 47 clean:
48 48 $(RM) $(MAN) $(HTML) common.txt $(SOURCES) $(SOURCES:%.txt=%.gendoc.txt) MANIFEST
@@ -1,467 +1,467 b''
1 1 # linelog - efficient cache for annotate data
2 2 #
3 3 # Copyright 2018 Google LLC.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """linelog is an efficient cache for annotate data inspired by SCCS Weaves.
8 8
9 9 SCCS Weaves are an implementation of
10 10 https://en.wikipedia.org/wiki/Interleaved_deltas. See
11 mercurial/help/internals/linelog.txt for an exploration of SCCS weaves
11 mercurial/helptext/internals/linelog.txt for an exploration of SCCS weaves
12 12 and how linelog works in detail.
13 13
14 14 Here's a hacker's summary: a linelog is a program which is executed in
15 15 the context of a revision. Executing the program emits information
16 16 about lines, including the revision that introduced them and the line
17 17 number in the file at the introducing revision. When an insertion or
18 18 deletion is performed on the file, a jump instruction is used to patch
19 19 in a new body of annotate information.
20 20 """
21 21 from __future__ import absolute_import, print_function
22 22
23 23 import abc
24 24 import struct
25 25
26 26 from .thirdparty import attr
27 27 from . import pycompat
28 28
29 29 _llentry = struct.Struct(b'>II')
30 30
31 31
32 32 class LineLogError(Exception):
33 33 """Error raised when something bad happens internally in linelog."""
34 34
35 35
36 36 @attr.s
37 37 class lineinfo(object):
38 38 # Introducing revision of this line.
39 39 rev = attr.ib()
40 40 # Line number for this line in its introducing revision.
41 41 linenum = attr.ib()
42 42 # Private. Offset in the linelog program of this line. Used internally.
43 43 _offset = attr.ib()
44 44
45 45
46 46 @attr.s
47 47 class annotateresult(object):
48 48 rev = attr.ib()
49 49 lines = attr.ib()
50 50 _eof = attr.ib()
51 51
52 52 def __iter__(self):
53 53 return iter(self.lines)
54 54
55 55
56 56 class _llinstruction(object): # pytype: disable=ignored-metaclass
57 57
58 58 __metaclass__ = abc.ABCMeta
59 59
60 60 @abc.abstractmethod
61 61 def __init__(self, op1, op2):
62 62 pass
63 63
64 64 @abc.abstractmethod
65 65 def __str__(self):
66 66 pass
67 67
68 68 def __repr__(self):
69 69 return str(self)
70 70
71 71 @abc.abstractmethod
72 72 def __eq__(self, other):
73 73 pass
74 74
75 75 @abc.abstractmethod
76 76 def encode(self):
77 77 """Encode this instruction to the binary linelog format."""
78 78
79 79 @abc.abstractmethod
80 80 def execute(self, rev, pc, emit):
81 81 """Execute this instruction.
82 82
83 83 Args:
84 84 rev: The revision we're annotating.
85 85 pc: The current offset in the linelog program.
86 86 emit: A function that accepts a single lineinfo object.
87 87
88 88 Returns:
89 89 The new value of pc. Returns None if exeuction should stop
90 90 (that is, we've found the end of the file.)
91 91 """
92 92
93 93
94 94 class _jge(_llinstruction):
95 95 """If the current rev is greater than or equal to op1, jump to op2."""
96 96
97 97 def __init__(self, op1, op2):
98 98 self._cmprev = op1
99 99 self._target = op2
100 100
101 101 def __str__(self):
102 102 return 'JGE %d %d' % (self._cmprev, self._target)
103 103
104 104 def __eq__(self, other):
105 105 return (
106 106 type(self) == type(other)
107 107 and self._cmprev == other._cmprev
108 108 and self._target == other._target
109 109 )
110 110
111 111 def encode(self):
112 112 return _llentry.pack(self._cmprev << 2, self._target)
113 113
114 114 def execute(self, rev, pc, emit):
115 115 if rev >= self._cmprev:
116 116 return self._target
117 117 return pc + 1
118 118
119 119
120 120 class _jump(_llinstruction):
121 121 """Unconditional jumps are expressed as a JGE with op1 set to 0."""
122 122
123 123 def __init__(self, op1, op2):
124 124 if op1 != 0:
125 125 raise LineLogError(b"malformed JUMP, op1 must be 0, got %d" % op1)
126 126 self._target = op2
127 127
128 128 def __str__(self):
129 129 return 'JUMP %d' % (self._target)
130 130
131 131 def __eq__(self, other):
132 132 return type(self) == type(other) and self._target == other._target
133 133
134 134 def encode(self):
135 135 return _llentry.pack(0, self._target)
136 136
137 137 def execute(self, rev, pc, emit):
138 138 return self._target
139 139
140 140
141 141 class _eof(_llinstruction):
142 142 """EOF is expressed as a JGE that always jumps to 0."""
143 143
144 144 def __init__(self, op1, op2):
145 145 if op1 != 0:
146 146 raise LineLogError(b"malformed EOF, op1 must be 0, got %d" % op1)
147 147 if op2 != 0:
148 148 raise LineLogError(b"malformed EOF, op2 must be 0, got %d" % op2)
149 149
150 150 def __str__(self):
151 151 return r'EOF'
152 152
153 153 def __eq__(self, other):
154 154 return type(self) == type(other)
155 155
156 156 def encode(self):
157 157 return _llentry.pack(0, 0)
158 158
159 159 def execute(self, rev, pc, emit):
160 160 return None
161 161
162 162
163 163 class _jl(_llinstruction):
164 164 """If the current rev is less than op1, jump to op2."""
165 165
166 166 def __init__(self, op1, op2):
167 167 self._cmprev = op1
168 168 self._target = op2
169 169
170 170 def __str__(self):
171 171 return 'JL %d %d' % (self._cmprev, self._target)
172 172
173 173 def __eq__(self, other):
174 174 return (
175 175 type(self) == type(other)
176 176 and self._cmprev == other._cmprev
177 177 and self._target == other._target
178 178 )
179 179
180 180 def encode(self):
181 181 return _llentry.pack(1 | (self._cmprev << 2), self._target)
182 182
183 183 def execute(self, rev, pc, emit):
184 184 if rev < self._cmprev:
185 185 return self._target
186 186 return pc + 1
187 187
188 188
189 189 class _line(_llinstruction):
190 190 """Emit a line."""
191 191
192 192 def __init__(self, op1, op2):
193 193 # This line was introduced by this revision number.
194 194 self._rev = op1
195 195 # This line had the specified line number in the introducing revision.
196 196 self._origlineno = op2
197 197
198 198 def __str__(self):
199 199 return 'LINE %d %d' % (self._rev, self._origlineno)
200 200
201 201 def __eq__(self, other):
202 202 return (
203 203 type(self) == type(other)
204 204 and self._rev == other._rev
205 205 and self._origlineno == other._origlineno
206 206 )
207 207
208 208 def encode(self):
209 209 return _llentry.pack(2 | (self._rev << 2), self._origlineno)
210 210
211 211 def execute(self, rev, pc, emit):
212 212 emit(lineinfo(self._rev, self._origlineno, pc))
213 213 return pc + 1
214 214
215 215
216 216 def _decodeone(data, offset):
217 217 """Decode a single linelog instruction from an offset in a buffer."""
218 218 try:
219 219 op1, op2 = _llentry.unpack_from(data, offset)
220 220 except struct.error as e:
221 221 raise LineLogError(b'reading an instruction failed: %r' % e)
222 222 opcode = op1 & 0b11
223 223 op1 = op1 >> 2
224 224 if opcode == 0:
225 225 if op1 == 0:
226 226 if op2 == 0:
227 227 return _eof(op1, op2)
228 228 return _jump(op1, op2)
229 229 return _jge(op1, op2)
230 230 elif opcode == 1:
231 231 return _jl(op1, op2)
232 232 elif opcode == 2:
233 233 return _line(op1, op2)
234 234 raise NotImplementedError(b'Unimplemented opcode %r' % opcode)
235 235
236 236
237 237 class linelog(object):
238 238 """Efficient cache for per-line history information."""
239 239
240 240 def __init__(self, program=None, maxrev=0):
241 241 if program is None:
242 242 # We pad the program with an extra leading EOF so that our
243 243 # offsets will match the C code exactly. This means we can
244 244 # interoperate with the C code.
245 245 program = [_eof(0, 0), _eof(0, 0)]
246 246 self._program = program
247 247 self._lastannotate = None
248 248 self._maxrev = maxrev
249 249
250 250 def __eq__(self, other):
251 251 return (
252 252 type(self) == type(other)
253 253 and self._program == other._program
254 254 and self._maxrev == other._maxrev
255 255 )
256 256
257 257 def __repr__(self):
258 258 return b'<linelog at %s: maxrev=%d size=%d>' % (
259 259 hex(id(self)),
260 260 self._maxrev,
261 261 len(self._program),
262 262 )
263 263
264 264 def debugstr(self):
265 265 fmt = '%%%dd %%s' % len(str(len(self._program)))
266 266 return pycompat.sysstr(b'\n').join(
267 267 fmt % (idx, i) for idx, i in enumerate(self._program[1:], 1)
268 268 )
269 269
270 270 @classmethod
271 271 def fromdata(cls, buf):
272 272 if len(buf) % _llentry.size != 0:
273 273 raise LineLogError(
274 274 b"invalid linelog buffer size %d (must be a multiple of %d)"
275 275 % (len(buf), _llentry.size)
276 276 )
277 277 expected = len(buf) / _llentry.size
278 278 fakejge = _decodeone(buf, 0)
279 279 if isinstance(fakejge, _jump):
280 280 maxrev = 0
281 281 elif isinstance(fakejge, (_jge, _jl)):
282 282 maxrev = fakejge._cmprev
283 283 else:
284 284 raise LineLogError(
285 285 'Expected one of _jump, _jge, or _jl. Got %s.'
286 286 % type(fakejge).__name__
287 287 )
288 288 assert isinstance(fakejge, (_jump, _jge, _jl)) # help pytype
289 289 numentries = fakejge._target
290 290 if expected != numentries:
291 291 raise LineLogError(
292 292 b"corrupt linelog data: claimed"
293 293 b" %d entries but given data for %d entries"
294 294 % (expected, numentries)
295 295 )
296 296 instructions = [_eof(0, 0)]
297 297 for offset in pycompat.xrange(1, numentries):
298 298 instructions.append(_decodeone(buf, offset * _llentry.size))
299 299 return cls(instructions, maxrev=maxrev)
300 300
301 301 def encode(self):
302 302 hdr = _jge(self._maxrev, len(self._program)).encode()
303 303 return hdr + b''.join(i.encode() for i in self._program[1:])
304 304
305 305 def clear(self):
306 306 self._program = []
307 307 self._maxrev = 0
308 308 self._lastannotate = None
309 309
310 310 def replacelines_vec(self, rev, a1, a2, blines):
311 311 return self.replacelines(
312 312 rev, a1, a2, 0, len(blines), _internal_blines=blines
313 313 )
314 314
315 315 def replacelines(self, rev, a1, a2, b1, b2, _internal_blines=None):
316 316 """Replace lines [a1, a2) with lines [b1, b2)."""
317 317 if self._lastannotate:
318 318 # TODO(augie): make replacelines() accept a revision at
319 319 # which we're editing as well as a revision to mark
320 320 # responsible for the edits. In hg-experimental it's
321 321 # stateful like this, so we're doing the same thing to
322 322 # retain compatibility with absorb until that's imported.
323 323 ar = self._lastannotate
324 324 else:
325 325 ar = self.annotate(rev)
326 326 # ar = self.annotate(self._maxrev)
327 327 if a1 > len(ar.lines):
328 328 raise LineLogError(
329 329 b'%d contains %d lines, tried to access line %d'
330 330 % (rev, len(ar.lines), a1)
331 331 )
332 332 elif a1 == len(ar.lines):
333 333 # Simulated EOF instruction since we're at EOF, which
334 334 # doesn't have a "real" line.
335 335 a1inst = _eof(0, 0)
336 336 a1info = lineinfo(0, 0, ar._eof)
337 337 else:
338 338 a1info = ar.lines[a1]
339 339 a1inst = self._program[a1info._offset]
340 340 programlen = self._program.__len__
341 341 oldproglen = programlen()
342 342 appendinst = self._program.append
343 343
344 344 # insert
345 345 blineinfos = []
346 346 bappend = blineinfos.append
347 347 if b1 < b2:
348 348 # Determine the jump target for the JGE at the start of
349 349 # the new block.
350 350 tgt = oldproglen + (b2 - b1 + 1)
351 351 # Jump to skip the insert if we're at an older revision.
352 352 appendinst(_jl(rev, tgt))
353 353 for linenum in pycompat.xrange(b1, b2):
354 354 if _internal_blines is None:
355 355 bappend(lineinfo(rev, linenum, programlen()))
356 356 appendinst(_line(rev, linenum))
357 357 else:
358 358 newrev, newlinenum = _internal_blines[linenum]
359 359 bappend(lineinfo(newrev, newlinenum, programlen()))
360 360 appendinst(_line(newrev, newlinenum))
361 361 # delete
362 362 if a1 < a2:
363 363 if a2 > len(ar.lines):
364 364 raise LineLogError(
365 365 b'%d contains %d lines, tried to access line %d'
366 366 % (rev, len(ar.lines), a2)
367 367 )
368 368 elif a2 == len(ar.lines):
369 369 endaddr = ar._eof
370 370 else:
371 371 endaddr = ar.lines[a2]._offset
372 372 if a2 > 0 and rev < self._maxrev:
373 373 # If we're here, we're deleting a chunk of an old
374 374 # commit, so we need to be careful and not touch
375 375 # invisible lines between a2-1 and a2 (IOW, lines that
376 376 # are added later).
377 377 endaddr = ar.lines[a2 - 1]._offset + 1
378 378 appendinst(_jge(rev, endaddr))
379 379 # copy instruction from a1
380 380 a1instpc = programlen()
381 381 appendinst(a1inst)
382 382 # if a1inst isn't a jump or EOF, then we need to add an unconditional
383 383 # jump back into the program here.
384 384 if not isinstance(a1inst, (_jump, _eof)):
385 385 appendinst(_jump(0, a1info._offset + 1))
386 386 # Patch instruction at a1, which makes our patch live.
387 387 self._program[a1info._offset] = _jump(0, oldproglen)
388 388
389 389 # Update self._lastannotate in place. This serves as a cache to avoid
390 390 # expensive "self.annotate" in this function, when "replacelines" is
391 391 # used continuously.
392 392 if len(self._lastannotate.lines) > a1:
393 393 self._lastannotate.lines[a1]._offset = a1instpc
394 394 else:
395 395 assert isinstance(a1inst, _eof)
396 396 self._lastannotate._eof = a1instpc
397 397 self._lastannotate.lines[a1:a2] = blineinfos
398 398 self._lastannotate.rev = max(self._lastannotate.rev, rev)
399 399
400 400 if rev > self._maxrev:
401 401 self._maxrev = rev
402 402
403 403 def annotate(self, rev):
404 404 pc = 1
405 405 lines = []
406 406 executed = 0
407 407 # Sanity check: if instructions executed exceeds len(program), we
408 408 # hit an infinite loop in the linelog program somehow and we
409 409 # should stop.
410 410 while pc is not None and executed < len(self._program):
411 411 inst = self._program[pc]
412 412 lastpc = pc
413 413 pc = inst.execute(rev, pc, lines.append)
414 414 executed += 1
415 415 if pc is not None:
416 416 raise LineLogError(
417 417 r'Probably hit an infinite loop in linelog. Program:\n'
418 418 + self.debugstr()
419 419 )
420 420 ar = annotateresult(rev, lines, lastpc)
421 421 self._lastannotate = ar
422 422 return ar
423 423
424 424 @property
425 425 def maxrev(self):
426 426 return self._maxrev
427 427
428 428 # Stateful methods which depend on the value of the last
429 429 # annotation run. This API is for compatiblity with the original
430 430 # linelog, and we should probably consider refactoring it.
431 431 @property
432 432 def annotateresult(self):
433 433 """Return the last annotation result. C linelog code exposed this."""
434 434 return [(l.rev, l.linenum) for l in self._lastannotate.lines]
435 435
436 436 def getoffset(self, line):
437 437 return self._lastannotate.lines[line]._offset
438 438
439 439 def getalllines(self, start=0, end=0):
440 440 """Get all lines that ever occurred in [start, end).
441 441
442 442 Passing start == end == 0 means "all lines ever".
443 443
444 444 This works in terms of *internal* program offsets, not line numbers.
445 445 """
446 446 pc = start or 1
447 447 lines = []
448 448 # only take as many steps as there are instructions in the
449 449 # program - if we don't find an EOF or our stop-line before
450 450 # then, something is badly broken.
451 451 for step in pycompat.xrange(len(self._program)):
452 452 inst = self._program[pc]
453 453 nextpc = pc + 1
454 454 if isinstance(inst, _jump):
455 455 nextpc = inst._target
456 456 elif isinstance(inst, _eof):
457 457 return lines
458 458 elif isinstance(inst, (_jl, _jge)):
459 459 pass
460 460 elif isinstance(inst, _line):
461 461 lines.append((inst._rev, inst._origlineno))
462 462 else:
463 463 raise LineLogError(b"Illegal instruction %r" % inst)
464 464 if nextpc == end:
465 465 return lines
466 466 pc = nextpc
467 467 raise LineLogError(b"Failed to perform getalllines")
General Comments 0
You need to be logged in to leave comments. Login now