##// END OF EJS Templates
py3: use pickle directly...
Gregory Szorc -
r50110:df56e6bd default
parent child Browse files
Show More
@@ -1,1126 +1,1124 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
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 Olivia Mackall <olivia@selenic.com>
5 # Copyright 2010 Olivia Mackall <olivia@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
29
30 if sys.version_info[0] < 3:
30 if sys.version_info[0] < 3:
31 opentext = open
31 opentext = open
32 else:
32 else:
33
33
34 def opentext(f):
34 def opentext(f):
35 return open(f, encoding='latin1')
35 return open(f, encoding='latin1')
36
36
37
37
38 try:
38 try:
39 xrange
39 xrange
40 except NameError:
40 except NameError:
41 xrange = range
41 xrange = range
42 try:
42 try:
43 import re2
43 import re2
44 except ImportError:
44 except ImportError:
45 re2 = None
45 re2 = None
46
46
47 import testparseutil
47 import testparseutil
48
48
49
49
50 def compilere(pat, multiline=False):
50 def compilere(pat, multiline=False):
51 if multiline:
51 if multiline:
52 pat = '(?m)' + pat
52 pat = '(?m)' + pat
53 if re2:
53 if re2:
54 try:
54 try:
55 return re2.compile(pat)
55 return re2.compile(pat)
56 except re2.error:
56 except re2.error:
57 pass
57 pass
58 return re.compile(pat)
58 return re.compile(pat)
59
59
60
60
61 # check "rules depending on implementation of repquote()" in each
61 # check "rules depending on implementation of repquote()" in each
62 # patterns (especially pypats), before changing around repquote()
62 # patterns (especially pypats), before changing around repquote()
63 _repquotefixedmap = {
63 _repquotefixedmap = {
64 ' ': ' ',
64 ' ': ' ',
65 '\n': '\n',
65 '\n': '\n',
66 '.': 'p',
66 '.': 'p',
67 ':': 'q',
67 ':': 'q',
68 '%': '%',
68 '%': '%',
69 '\\': 'b',
69 '\\': 'b',
70 '*': 'A',
70 '*': 'A',
71 '+': 'P',
71 '+': 'P',
72 '-': 'M',
72 '-': 'M',
73 }
73 }
74
74
75
75
76 def _repquoteencodechr(i):
76 def _repquoteencodechr(i):
77 if i > 255:
77 if i > 255:
78 return 'u'
78 return 'u'
79 c = chr(i)
79 c = chr(i)
80 if c in _repquotefixedmap:
80 if c in _repquotefixedmap:
81 return _repquotefixedmap[c]
81 return _repquotefixedmap[c]
82 if c.isalpha():
82 if c.isalpha():
83 return 'x'
83 return 'x'
84 if c.isdigit():
84 if c.isdigit():
85 return 'n'
85 return 'n'
86 return 'o'
86 return 'o'
87
87
88
88
89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
90
90
91
91
92 def repquote(m):
92 def repquote(m):
93 t = m.group('text')
93 t = m.group('text')
94 t = t.translate(_repquotett)
94 t = t.translate(_repquotett)
95 return m.group('quote') + t + m.group('quote')
95 return m.group('quote') + t + m.group('quote')
96
96
97
97
98 def reppython(m):
98 def reppython(m):
99 comment = m.group('comment')
99 comment = m.group('comment')
100 if comment:
100 if comment:
101 l = len(comment.rstrip())
101 l = len(comment.rstrip())
102 return "#" * l + comment[l:]
102 return "#" * l + comment[l:]
103 return repquote(m)
103 return repquote(m)
104
104
105
105
106 def repcomment(m):
106 def repcomment(m):
107 return m.group(1) + "#" * len(m.group(2))
107 return m.group(1) + "#" * len(m.group(2))
108
108
109
109
110 def repccomment(m):
110 def repccomment(m):
111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
112 return m.group(1) + t + "*/"
112 return m.group(1) + t + "*/"
113
113
114
114
115 def repcallspaces(m):
115 def repcallspaces(m):
116 t = re.sub(r"\n\s+", "\n", m.group(2))
116 t = re.sub(r"\n\s+", "\n", m.group(2))
117 return m.group(1) + t
117 return m.group(1) + t
118
118
119
119
120 def repinclude(m):
120 def repinclude(m):
121 return m.group(1) + "<foo>"
121 return m.group(1) + "<foo>"
122
122
123
123
124 def rephere(m):
124 def rephere(m):
125 t = re.sub(r"\S", "x", m.group(2))
125 t = re.sub(r"\S", "x", m.group(2))
126 return m.group(1) + t
126 return m.group(1) + t
127
127
128
128
129 testpats = [
129 testpats = [
130 [
130 [
131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
137 (r'echo -n', "don't use 'echo -n', use printf"),
137 (r'echo -n', "don't use 'echo -n', use printf"),
138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
139 (r'head -c', "don't use 'head -c', use 'dd'"),
139 (r'head -c', "don't use 'head -c', use 'dd'"),
140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
146 (
146 (
147 r'\[[^\]]+==',
147 r'\[[^\]]+==',
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
149 ),
149 ),
150 (
150 (
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
152 "use egrep for extended grep syntax",
152 "use egrep for extended grep syntax",
153 ),
153 ),
154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
157 (r'[^\n]\Z', "no trailing newline"),
157 (r'[^\n]\Z', "no trailing newline"),
158 (r'export .*=', "don't export and assign at once"),
158 (r'export .*=', "don't export and assign at once"),
159 (r'^source\b', "don't use 'source', use '.'"),
159 (r'^source\b', "don't use 'source', use '.'"),
160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
166 (r'^alias\b.*=', "don't use alias, use a function"),
166 (r'^alias\b.*=', "don't use alias, use a function"),
167 (r'if\s*!', "don't use '!' to negate exit status"),
167 (r'if\s*!', "don't use '!' to negate exit status"),
168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
170 (
170 (
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
172 "put a backslash-escaped newline after sed 'i' command",
172 "put a backslash-escaped newline after sed 'i' command",
173 ),
173 ),
174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
178 (r'\butil\.Abort\b', "directly use error.Abort"),
178 (r'\butil\.Abort\b', "directly use error.Abort"),
179 (r'\|&', "don't use |&, use 2>&1"),
179 (r'\|&', "don't use |&, use 2>&1"),
180 (r'\w = +\w', "only one space after = allowed"),
180 (r'\w = +\w', "only one space after = allowed"),
181 (
181 (
182 r'\bsed\b.*[^\\]\\n',
182 r'\bsed\b.*[^\\]\\n',
183 "don't use 'sed ... \\n', use a \\ and a newline",
183 "don't use 'sed ... \\n', use a \\ and a newline",
184 ),
184 ),
185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
187 (r'grep.* -[ABC]', "don't use grep's context flags"),
187 (r'grep.* -[ABC]', "don't use grep's context flags"),
188 (
188 (
189 r'find.*-printf',
189 r'find.*-printf',
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 ),
191 ),
192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
193 ],
193 ],
194 # warnings
194 # warnings
195 [
195 [
196 (r'^function', "don't use 'function', use old style"),
196 (r'^function', "don't use 'function', use old style"),
197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`", "no-pwd-check"),
198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`", "no-pwd-check"),
199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 ],
201 ],
202 ]
202 ]
203
203
204 testfilters = [
204 testfilters = [
205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
207 ]
207 ]
208
208
209 uprefix = r"^ \$ "
209 uprefix = r"^ \$ "
210 utestpats = [
210 utestpats = [
211 [
211 [
212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
213 (
213 (
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
215 "use regex test output patterns instead of sed",
215 "use regex test output patterns instead of sed",
216 ),
216 ),
217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
218 (
218 (
219 uprefix + r'.*\|\| echo.*(fail|error)',
219 uprefix + r'.*\|\| echo.*(fail|error)',
220 "explicit exit code checks unnecessary",
220 "explicit exit code checks unnecessary",
221 ),
221 ),
222 (uprefix + r'set -e', "don't use set -e"),
222 (uprefix + r'set -e', "don't use set -e"),
223 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
223 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
224 (
224 (
225 uprefix + r'.*:\.\S*/',
225 uprefix + r'.*:\.\S*/',
226 "x:.y in a path does not work on msys, rewrite "
226 "x:.y in a path does not work on msys, rewrite "
227 "as x://.y, or see `hg log -k msys` for alternatives",
227 "as x://.y, or see `hg log -k msys` for alternatives",
228 r'-\S+:\.|' '# no-msys', # -Rxxx
228 r'-\S+:\.|' '# no-msys', # -Rxxx
229 ), # in test-pull.t which is skipped on windows
229 ), # in test-pull.t which is skipped on windows
230 (
230 (
231 r'^ [^$>].*27\.0\.0\.1',
231 r'^ [^$>].*27\.0\.0\.1',
232 'use $LOCALIP not an explicit loopback address',
232 'use $LOCALIP not an explicit loopback address',
233 ),
233 ),
234 (
234 (
235 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
235 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
236 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
236 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
237 ),
237 ),
238 (
238 (
239 r'^ (cat|find): .*: \$ENOENT\$',
239 r'^ (cat|find): .*: \$ENOENT\$',
240 'use test -f to test for file existence',
240 'use test -f to test for file existence',
241 ),
241 ),
242 (
242 (
243 r'^ diff -[^ -]*p',
243 r'^ diff -[^ -]*p',
244 "don't use (external) diff with -p for portability",
244 "don't use (external) diff with -p for portability",
245 ),
245 ),
246 (r' readlink ', 'use readlink.py instead of readlink'),
246 (r' readlink ', 'use readlink.py instead of readlink'),
247 (
247 (
248 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
248 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
249 "glob timezone field in diff output for portability",
249 "glob timezone field in diff output for portability",
250 ),
250 ),
251 (
251 (
252 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
252 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
253 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
253 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
254 ),
254 ),
255 (
255 (
256 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
256 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
257 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
257 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
258 ),
258 ),
259 (
259 (
260 r'^ @@ -[0-9]+ [+][0-9]+ @@',
260 r'^ @@ -[0-9]+ [+][0-9]+ @@',
261 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
261 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
262 ),
262 ),
263 (
263 (
264 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
264 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
265 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
265 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
266 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
266 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
267 ),
267 ),
268 ],
268 ],
269 # warnings
269 # warnings
270 [
270 [
271 (
271 (
272 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
272 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
273 "glob match with no glob string (?, *, /, and $LOCALIP)",
273 "glob match with no glob string (?, *, /, and $LOCALIP)",
274 ),
274 ),
275 ],
275 ],
276 ]
276 ]
277
277
278 # transform plain test rules to unified test's
278 # transform plain test rules to unified test's
279 for i in [0, 1]:
279 for i in [0, 1]:
280 for tp in testpats[i]:
280 for tp in testpats[i]:
281 p = tp[0]
281 p = tp[0]
282 m = tp[1]
282 m = tp[1]
283 if p.startswith('^'):
283 if p.startswith('^'):
284 p = "^ [$>] (%s)" % p[1:]
284 p = "^ [$>] (%s)" % p[1:]
285 else:
285 else:
286 p = "^ [$>] .*(%s)" % p
286 p = "^ [$>] .*(%s)" % p
287 utestpats[i].append((p, m) + tp[2:])
287 utestpats[i].append((p, m) + tp[2:])
288
288
289 # don't transform the following rules:
289 # don't transform the following rules:
290 # " > \t" and " \t" should be allowed in unified tests
290 # " > \t" and " \t" should be allowed in unified tests
291 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
291 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
292 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
292 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
293
293
294 utestfilters = [
294 utestfilters = [
295 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
295 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
296 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
296 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
297 ]
297 ]
298
298
299 # common patterns to check *.py
299 # common patterns to check *.py
300 commonpypats = [
300 commonpypats = [
301 [
301 [
302 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
302 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
303 (
303 (
304 r'^\s*def\s*\w+\s*\(.*,\s*\(',
304 r'^\s*def\s*\w+\s*\(.*,\s*\(',
305 "tuple parameter unpacking not available in Python 3+",
305 "tuple parameter unpacking not available in Python 3+",
306 ),
306 ),
307 (
307 (
308 r'lambda\s*\(.*,.*\)',
308 r'lambda\s*\(.*,.*\)',
309 "tuple parameter unpacking not available in Python 3+",
309 "tuple parameter unpacking not available in Python 3+",
310 ),
310 ),
311 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
311 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
312 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
312 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
313 (
313 (
314 r'\bdict\(.*=',
314 r'\bdict\(.*=',
315 'dict() is different in Py2 and 3 and is slower than {}',
315 'dict() is different in Py2 and 3 and is slower than {}',
316 'dict-from-generator',
316 'dict-from-generator',
317 ),
317 ),
318 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
318 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
319 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
319 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
320 (r'^\s*\t', "don't use tabs"),
320 (r'^\s*\t', "don't use tabs"),
321 (r'\S;\s*\n', "semicolon"),
321 (r'\S;\s*\n', "semicolon"),
322 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
322 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
323 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
323 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
324 (r'(\w|\)),\w', "missing whitespace after ,"),
324 (r'(\w|\)),\w', "missing whitespace after ,"),
325 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
325 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
326 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
326 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
327 (
327 (
328 (
328 (
329 # a line ending with a colon, potentially with trailing comments
329 # a line ending with a colon, potentially with trailing comments
330 r':([ \t]*#[^\n]*)?\n'
330 r':([ \t]*#[^\n]*)?\n'
331 # one that is not a pass and not only a comment
331 # one that is not a pass and not only a comment
332 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
332 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
333 # more lines at the same indent level
333 # more lines at the same indent level
334 r'((?P=indent)[^\n]+\n)*'
334 r'((?P=indent)[^\n]+\n)*'
335 # a pass at the same indent level, which is bogus
335 # a pass at the same indent level, which is bogus
336 r'(?P=indent)pass[ \t\n#]'
336 r'(?P=indent)pass[ \t\n#]'
337 ),
337 ),
338 'omit superfluous pass',
338 'omit superfluous pass',
339 ),
339 ),
340 (r'[^\n]\Z', "no trailing newline"),
340 (r'[^\n]\Z', "no trailing newline"),
341 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
341 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
342 (
342 (
343 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
343 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
344 "linebreak after :",
344 "linebreak after :",
345 ),
345 ),
346 (
346 (
347 r'class\s[^( \n]+:',
347 r'class\s[^( \n]+:',
348 "old-style class, use class foo(object)",
348 "old-style class, use class foo(object)",
349 r'#.*old-style',
349 r'#.*old-style',
350 ),
350 ),
351 (
351 (
352 r'class\s[^( \n]+\(\):',
352 r'class\s[^( \n]+\(\):',
353 "class foo() creates old style object, use class foo(object)",
353 "class foo() creates old style object, use class foo(object)",
354 r'#.*old-style',
354 r'#.*old-style',
355 ),
355 ),
356 (
356 (
357 r'\b(%s)\('
357 r'\b(%s)\('
358 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
358 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
359 "Python keyword is not a function",
359 "Python keyword is not a function",
360 ),
360 ),
361 # (r'class\s[A-Z][^\(]*\((?!Exception)',
361 # (r'class\s[A-Z][^\(]*\((?!Exception)',
362 # "don't capitalize non-exception classes"),
362 # "don't capitalize non-exception classes"),
363 # (r'in range\(', "use xrange"),
363 # (r'in range\(', "use xrange"),
364 # (r'^\s*print\s+', "avoid using print in core and extensions"),
364 # (r'^\s*print\s+', "avoid using print in core and extensions"),
365 (r'[\x80-\xff]', "non-ASCII character literal"),
365 (r'[\x80-\xff]', "non-ASCII character literal"),
366 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
366 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
367 (
367 (
368 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
368 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
369 "gratuitous whitespace in () or []",
369 "gratuitous whitespace in () or []",
370 ),
370 ),
371 # (r'\s\s=', "gratuitous whitespace before ="),
371 # (r'\s\s=', "gratuitous whitespace before ="),
372 (
372 (
373 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
373 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
374 "missing whitespace around operator",
374 "missing whitespace around operator",
375 ),
375 ),
376 (
376 (
377 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
377 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
378 "missing whitespace around operator",
378 "missing whitespace around operator",
379 ),
379 ),
380 (
380 (
381 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
381 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
382 "missing whitespace around operator",
382 "missing whitespace around operator",
383 ),
383 ),
384 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
384 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
385 (
385 (
386 r'\([^()]*( =[^=]|[^<>!=]= )',
386 r'\([^()]*( =[^=]|[^<>!=]= )',
387 "no whitespace around = for named parameters",
387 "no whitespace around = for named parameters",
388 ),
388 ),
389 (
389 (
390 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
390 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
391 "don't use old-style two-argument raise, use Exception(message)",
391 "don't use old-style two-argument raise, use Exception(message)",
392 ),
392 ),
393 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
393 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
394 (
394 (
395 r' [=!]=\s+(True|False|None)',
395 r' [=!]=\s+(True|False|None)',
396 "comparison with singleton, use 'is' or 'is not' instead",
396 "comparison with singleton, use 'is' or 'is not' instead",
397 ),
397 ),
398 (
398 (
399 r'^\s*(while|if) [01]:',
399 r'^\s*(while|if) [01]:',
400 "use True/False for constant Boolean expression",
400 "use True/False for constant Boolean expression",
401 ),
401 ),
402 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
402 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
403 (
403 (
404 r'(?:(?<!def)\s+|\()hasattr\(',
404 r'(?:(?<!def)\s+|\()hasattr\(',
405 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
405 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
406 'instead',
406 'instead',
407 r'#.*hasattr-py3-only',
407 r'#.*hasattr-py3-only',
408 ),
408 ),
409 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
409 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
410 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
410 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
411 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
411 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
412 (r'\.debug\(\_', "don't mark debug messages for translation"),
412 (r'\.debug\(\_', "don't mark debug messages for translation"),
413 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
413 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
414 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
414 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
415 (
415 (
416 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
416 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
417 'legacy exception syntax; use "as" instead of ","',
417 'legacy exception syntax; use "as" instead of ","',
418 ),
418 ),
419 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
419 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
420 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
420 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
421 (
421 (
422 r'os\.path\.join\(.*, *(""|\'\')\)',
422 r'os\.path\.join\(.*, *(""|\'\')\)',
423 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
423 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
424 ),
424 ),
425 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
425 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
426 # XXX only catch mutable arguments on the first line of the definition
426 # XXX only catch mutable arguments on the first line of the definition
427 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
427 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
428 (r'\butil\.Abort\b', "directly use error.Abort"),
428 (r'\butil\.Abort\b', "directly use error.Abort"),
429 (
429 (
430 r'^@(\w*\.)?cachefunc',
430 r'^@(\w*\.)?cachefunc',
431 "module-level @cachefunc is risky, please avoid",
431 "module-level @cachefunc is risky, please avoid",
432 ),
432 ),
433 (
433 (
434 r'^import Queue',
434 r'^import Queue',
435 "don't use Queue, use pycompat.queue.Queue + "
435 "don't use Queue, use pycompat.queue.Queue + "
436 "pycompat.queue.Empty",
436 "pycompat.queue.Empty",
437 ),
437 ),
438 (
438 (
439 r'^import cStringIO',
439 r'^import cStringIO',
440 "don't use cStringIO.StringIO, use util.stringio",
440 "don't use cStringIO.StringIO, use util.stringio",
441 ),
441 ),
442 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
442 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
443 (
443 (
444 r'^import SocketServer',
444 r'^import SocketServer',
445 "don't use SockerServer, use util.socketserver",
445 "don't use SockerServer, use util.socketserver",
446 ),
446 ),
447 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
447 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
448 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
448 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
449 (r'^import cPickle', "don't use cPickle, use util.pickle"),
450 (r'^import pickle', "don't use pickle, use util.pickle"),
451 (r'^import httplib', "don't use httplib, use util.httplib"),
449 (r'^import httplib', "don't use httplib, use util.httplib"),
452 (r'^import BaseHTTPServer', "use util.httpserver instead"),
450 (r'^import BaseHTTPServer', "use util.httpserver instead"),
453 (
451 (
454 r'^(from|import) mercurial\.(cext|pure|cffi)',
452 r'^(from|import) mercurial\.(cext|pure|cffi)',
455 "use mercurial.policy.importmod instead",
453 "use mercurial.policy.importmod instead",
456 ),
454 ),
457 (r'\.next\(\)', "don't use .next(), use next(...)"),
455 (r'\.next\(\)', "don't use .next(), use next(...)"),
458 (
456 (
459 r'([a-z]*).revision\(\1\.node\(',
457 r'([a-z]*).revision\(\1\.node\(',
460 "don't convert rev to node before passing to revision(nodeorrev)",
458 "don't convert rev to node before passing to revision(nodeorrev)",
461 ),
459 ),
462 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
460 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
463 ],
461 ],
464 # warnings
462 # warnings
465 [],
463 [],
466 ]
464 ]
467
465
468 # patterns to check normal *.py files
466 # patterns to check normal *.py files
469 pypats = [
467 pypats = [
470 [
468 [
471 # Ideally, these should be placed in "commonpypats" for
469 # Ideally, these should be placed in "commonpypats" for
472 # consistency of coding rules in Mercurial source tree.
470 # consistency of coding rules in Mercurial source tree.
473 # But on the other hand, these are not so seriously required for
471 # But on the other hand, these are not so seriously required for
474 # python code fragments embedded in test scripts. Fixing test
472 # python code fragments embedded in test scripts. Fixing test
475 # scripts for these patterns requires many changes, and has less
473 # scripts for these patterns requires many changes, and has less
476 # profit than effort.
474 # profit than effort.
477 (r'raise Exception', "don't raise generic exceptions"),
475 (r'raise Exception', "don't raise generic exceptions"),
478 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
476 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
479 (
477 (
480 r'[\s\(](open|file)\([^)]*\)\.write\(',
478 r'[\s\(](open|file)\([^)]*\)\.write\(',
481 "use util.writefile() instead",
479 "use util.writefile() instead",
482 ),
480 ),
483 (
481 (
484 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
482 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
485 "always assign an opened file to a variable, and close it afterwards",
483 "always assign an opened file to a variable, and close it afterwards",
486 ),
484 ),
487 (
485 (
488 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
486 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
489 "always assign an opened file to a variable, and close it afterwards",
487 "always assign an opened file to a variable, and close it afterwards",
490 ),
488 ),
491 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
489 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
492 (r'^import atexit', "don't use atexit, use ui.atexit"),
490 (r'^import atexit', "don't use atexit, use ui.atexit"),
493 # rules depending on implementation of repquote()
491 # rules depending on implementation of repquote()
494 (
492 (
495 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
493 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
496 'string join across lines with no space',
494 'string join across lines with no space',
497 ),
495 ),
498 (
496 (
499 r'''(?x)ui\.(status|progress|write|note|warn)\(
497 r'''(?x)ui\.(status|progress|write|note|warn)\(
500 [ \t\n#]*
498 [ \t\n#]*
501 (?# any strings/comments might precede a string, which
499 (?# any strings/comments might precede a string, which
502 # contains translatable message)
500 # contains translatable message)
503 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
501 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
504 (?# sequence consisting of below might precede translatable message
502 (?# sequence consisting of below might precede translatable message
505 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
503 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
506 # - escaped character: "\\", "\n", "\0" ...
504 # - escaped character: "\\", "\n", "\0" ...
507 # - character other than '%', 'b' as '\', and 'x' as alphabet)
505 # - character other than '%', 'b' as '\', and 'x' as alphabet)
508 (['"]|\'\'\'|""")
506 (['"]|\'\'\'|""")
509 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
507 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
510 (?# this regexp can't use [^...] style,
508 (?# this regexp can't use [^...] style,
511 # because _preparepats forcibly adds "\n" into [^...],
509 # because _preparepats forcibly adds "\n" into [^...],
512 # even though this regexp wants match it against "\n")''',
510 # even though this regexp wants match it against "\n")''',
513 "missing _() in ui message (use () to hide false-positives)",
511 "missing _() in ui message (use () to hide false-positives)",
514 ),
512 ),
515 ]
513 ]
516 + commonpypats[0],
514 + commonpypats[0],
517 # warnings
515 # warnings
518 [
516 [
519 # rules depending on implementation of repquote()
517 # rules depending on implementation of repquote()
520 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
518 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
521 ]
519 ]
522 + commonpypats[1],
520 + commonpypats[1],
523 ]
521 ]
524
522
525 # patterns to check *.py for embedded ones in test script
523 # patterns to check *.py for embedded ones in test script
526 embeddedpypats = [
524 embeddedpypats = [
527 [] + commonpypats[0],
525 [] + commonpypats[0],
528 # warnings
526 # warnings
529 [] + commonpypats[1],
527 [] + commonpypats[1],
530 ]
528 ]
531
529
532 # common filters to convert *.py
530 # common filters to convert *.py
533 commonpyfilters = [
531 commonpyfilters = [
534 (
532 (
535 r"""(?msx)(?P<comment>\#.*?$)|
533 r"""(?msx)(?P<comment>\#.*?$)|
536 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
534 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
537 (?P<text>(([^\\]|\\.)*?))
535 (?P<text>(([^\\]|\\.)*?))
538 (?P=quote))""",
536 (?P=quote))""",
539 reppython,
537 reppython,
540 ),
538 ),
541 ]
539 ]
542
540
543 # pattern only for mercurial and extensions
541 # pattern only for mercurial and extensions
544 core_py_pats = [
542 core_py_pats = [
545 [
543 [
546 # Windows tend to get confused about capitalization of the drive letter
544 # Windows tend to get confused about capitalization of the drive letter
547 #
545 #
548 # see mercurial.windows.abspath for details
546 # see mercurial.windows.abspath for details
549 (
547 (
550 r'os\.path\.abspath',
548 r'os\.path\.abspath',
551 "use util.abspath instead (windows)",
549 "use util.abspath instead (windows)",
552 r'#.*re-exports',
550 r'#.*re-exports',
553 ),
551 ),
554 ],
552 ],
555 # warnings
553 # warnings
556 [],
554 [],
557 ]
555 ]
558
556
559 # filters to convert normal *.py files
557 # filters to convert normal *.py files
560 pyfilters = [] + commonpyfilters
558 pyfilters = [] + commonpyfilters
561
559
562 # non-filter patterns
560 # non-filter patterns
563 pynfpats = [
561 pynfpats = [
564 [
562 [
565 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
563 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
566 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
564 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
567 (
565 (
568 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
566 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
569 "use pycompat.isdarwin",
567 "use pycompat.isdarwin",
570 ),
568 ),
571 ],
569 ],
572 # warnings
570 # warnings
573 [],
571 [],
574 ]
572 ]
575
573
576 # filters to convert *.py for embedded ones in test script
574 # filters to convert *.py for embedded ones in test script
577 embeddedpyfilters = [] + commonpyfilters
575 embeddedpyfilters = [] + commonpyfilters
578
576
579 # extension non-filter patterns
577 # extension non-filter patterns
580 pyextnfpats = [
578 pyextnfpats = [
581 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
579 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
582 # warnings
580 # warnings
583 [],
581 [],
584 ]
582 ]
585
583
586 txtfilters = []
584 txtfilters = []
587
585
588 txtpats = [
586 txtpats = [
589 [
587 [
590 (r'\s$', 'trailing whitespace'),
588 (r'\s$', 'trailing whitespace'),
591 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
589 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
592 ],
590 ],
593 [],
591 [],
594 ]
592 ]
595
593
596 cpats = [
594 cpats = [
597 [
595 [
598 (r'//', "don't use //-style comments"),
596 (r'//', "don't use //-style comments"),
599 (r'\S\t', "don't use tabs except for indent"),
597 (r'\S\t', "don't use tabs except for indent"),
600 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
598 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
601 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
599 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
602 (r'return\(', "return is not a function"),
600 (r'return\(', "return is not a function"),
603 (r' ;', "no space before ;"),
601 (r' ;', "no space before ;"),
604 (r'[^;] \)', "no space before )"),
602 (r'[^;] \)', "no space before )"),
605 (r'[)][{]', "space between ) and {"),
603 (r'[)][{]', "space between ) and {"),
606 (r'\w+\* \w+', "use int *foo, not int* foo"),
604 (r'\w+\* \w+', "use int *foo, not int* foo"),
607 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
605 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
608 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
606 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
609 (r'\w,\w', "missing whitespace after ,"),
607 (r'\w,\w', "missing whitespace after ,"),
610 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
608 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
611 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
609 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
612 (r'^#\s+\w', "use #foo, not # foo"),
610 (r'^#\s+\w', "use #foo, not # foo"),
613 (r'[^\n]\Z', "no trailing newline"),
611 (r'[^\n]\Z', "no trailing newline"),
614 (r'^\s*#import\b', "use only #include in standard C code"),
612 (r'^\s*#import\b', "use only #include in standard C code"),
615 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
613 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
616 (r'strcat\(', "don't use strcat"),
614 (r'strcat\(', "don't use strcat"),
617 # rules depending on implementation of repquote()
615 # rules depending on implementation of repquote()
618 ],
616 ],
619 # warnings
617 # warnings
620 [
618 [
621 # rules depending on implementation of repquote()
619 # rules depending on implementation of repquote()
622 ],
620 ],
623 ]
621 ]
624
622
625 cfilters = [
623 cfilters = [
626 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
624 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
627 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
625 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
628 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
626 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
629 (r'(\()([^)]+\))', repcallspaces),
627 (r'(\()([^)]+\))', repcallspaces),
630 ]
628 ]
631
629
632 inutilpats = [
630 inutilpats = [
633 [
631 [
634 (r'\bui\.', "don't use ui in util"),
632 (r'\bui\.', "don't use ui in util"),
635 ],
633 ],
636 # warnings
634 # warnings
637 [],
635 [],
638 ]
636 ]
639
637
640 inrevlogpats = [
638 inrevlogpats = [
641 [
639 [
642 (r'\brepo\.', "don't use repo in revlog"),
640 (r'\brepo\.', "don't use repo in revlog"),
643 ],
641 ],
644 # warnings
642 # warnings
645 [],
643 [],
646 ]
644 ]
647
645
648 webtemplatefilters = []
646 webtemplatefilters = []
649
647
650 webtemplatepats = [
648 webtemplatepats = [
651 [],
649 [],
652 [
650 [
653 (
651 (
654 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
652 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
655 'follow desc keyword with either firstline or websub',
653 'follow desc keyword with either firstline or websub',
656 ),
654 ),
657 ],
655 ],
658 ]
656 ]
659
657
660 allfilesfilters = []
658 allfilesfilters = []
661
659
662 allfilespats = [
660 allfilespats = [
663 [
661 [
664 (
662 (
665 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
663 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
666 'use mercurial-scm.org domain URL',
664 'use mercurial-scm.org domain URL',
667 ),
665 ),
668 (
666 (
669 r'mercurial@selenic\.com',
667 r'mercurial@selenic\.com',
670 'use mercurial-scm.org domain for mercurial ML address',
668 'use mercurial-scm.org domain for mercurial ML address',
671 ),
669 ),
672 (
670 (
673 r'mercurial-devel@selenic\.com',
671 r'mercurial-devel@selenic\.com',
674 'use mercurial-scm.org domain for mercurial-devel ML address',
672 'use mercurial-scm.org domain for mercurial-devel ML address',
675 ),
673 ),
676 ],
674 ],
677 # warnings
675 # warnings
678 [],
676 [],
679 ]
677 ]
680
678
681 py3pats = [
679 py3pats = [
682 [
680 [
683 (
681 (
684 r'os\.environ',
682 r'os\.environ',
685 "use encoding.environ instead (py3)",
683 "use encoding.environ instead (py3)",
686 r'#.*re-exports',
684 r'#.*re-exports',
687 ),
685 ),
688 (r'os\.name', "use pycompat.osname instead (py3)"),
686 (r'os\.name', "use pycompat.osname instead (py3)"),
689 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
687 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
690 (r'os\.sep', "use pycompat.ossep instead (py3)"),
688 (r'os\.sep', "use pycompat.ossep instead (py3)"),
691 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
689 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
692 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
690 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
693 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
691 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
694 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
692 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
695 (r'os\.getenv', "use encoding.environ.get instead"),
693 (r'os\.getenv', "use encoding.environ.get instead"),
696 (r'os\.setenv', "modifying the environ dict is not preferred"),
694 (r'os\.setenv', "modifying the environ dict is not preferred"),
697 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
695 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
698 ],
696 ],
699 # warnings
697 # warnings
700 [],
698 [],
701 ]
699 ]
702
700
703 checks = [
701 checks = [
704 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
702 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
705 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
703 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
706 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
704 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
707 (
705 (
708 'python 3',
706 'python 3',
709 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
707 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
710 '',
708 '',
711 pyfilters,
709 pyfilters,
712 py3pats,
710 py3pats,
713 ),
711 ),
714 (
712 (
715 'core files',
713 'core files',
716 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
714 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
717 '',
715 '',
718 pyfilters,
716 pyfilters,
719 core_py_pats,
717 core_py_pats,
720 ),
718 ),
721 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
719 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
722 ('c', r'.*\.[ch]$', '', cfilters, cpats),
720 ('c', r'.*\.[ch]$', '', cfilters, cpats),
723 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
721 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
724 (
722 (
725 'layering violation repo in revlog',
723 'layering violation repo in revlog',
726 r'mercurial/revlog\.py',
724 r'mercurial/revlog\.py',
727 '',
725 '',
728 pyfilters,
726 pyfilters,
729 inrevlogpats,
727 inrevlogpats,
730 ),
728 ),
731 (
729 (
732 'layering violation ui in util',
730 'layering violation ui in util',
733 r'mercurial/util\.py',
731 r'mercurial/util\.py',
734 '',
732 '',
735 pyfilters,
733 pyfilters,
736 inutilpats,
734 inutilpats,
737 ),
735 ),
738 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
736 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
739 (
737 (
740 'web template',
738 'web template',
741 r'mercurial/templates/.*\.tmpl',
739 r'mercurial/templates/.*\.tmpl',
742 '',
740 '',
743 webtemplatefilters,
741 webtemplatefilters,
744 webtemplatepats,
742 webtemplatepats,
745 ),
743 ),
746 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
744 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
747 ]
745 ]
748
746
749 # (desc,
747 # (desc,
750 # func to pick up embedded code fragments,
748 # func to pick up embedded code fragments,
751 # list of patterns to convert target files
749 # list of patterns to convert target files
752 # list of patterns to detect errors/warnings)
750 # list of patterns to detect errors/warnings)
753 embeddedchecks = [
751 embeddedchecks = [
754 (
752 (
755 'embedded python',
753 'embedded python',
756 testparseutil.pyembedded,
754 testparseutil.pyembedded,
757 embeddedpyfilters,
755 embeddedpyfilters,
758 embeddedpypats,
756 embeddedpypats,
759 )
757 )
760 ]
758 ]
761
759
762
760
763 def _preparepats():
761 def _preparepats():
764 def preparefailandwarn(failandwarn):
762 def preparefailandwarn(failandwarn):
765 for pats in failandwarn:
763 for pats in failandwarn:
766 for i, pseq in enumerate(pats):
764 for i, pseq in enumerate(pats):
767 # fix-up regexes for multi-line searches
765 # fix-up regexes for multi-line searches
768 p = pseq[0]
766 p = pseq[0]
769 # \s doesn't match \n (done in two steps)
767 # \s doesn't match \n (done in two steps)
770 # first, we replace \s that appears in a set already
768 # first, we replace \s that appears in a set already
771 p = re.sub(r'\[\\s', r'[ \\t', p)
769 p = re.sub(r'\[\\s', r'[ \\t', p)
772 # now we replace other \s instances.
770 # now we replace other \s instances.
773 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
771 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
774 # [^...] doesn't match newline
772 # [^...] doesn't match newline
775 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
773 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
776
774
777 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
775 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
778
776
779 def preparefilters(filters):
777 def preparefilters(filters):
780 for i, flt in enumerate(filters):
778 for i, flt in enumerate(filters):
781 filters[i] = re.compile(flt[0]), flt[1]
779 filters[i] = re.compile(flt[0]), flt[1]
782
780
783 for cs in (checks, embeddedchecks):
781 for cs in (checks, embeddedchecks):
784 for c in cs:
782 for c in cs:
785 failandwarn = c[-1]
783 failandwarn = c[-1]
786 preparefailandwarn(failandwarn)
784 preparefailandwarn(failandwarn)
787
785
788 filters = c[-2]
786 filters = c[-2]
789 preparefilters(filters)
787 preparefilters(filters)
790
788
791
789
792 class norepeatlogger(object):
790 class norepeatlogger(object):
793 def __init__(self):
791 def __init__(self):
794 self._lastseen = None
792 self._lastseen = None
795
793
796 def log(self, fname, lineno, line, msg, blame):
794 def log(self, fname, lineno, line, msg, blame):
797 """print error related a to given line of a given file.
795 """print error related a to given line of a given file.
798
796
799 The faulty line will also be printed but only once in the case
797 The faulty line will also be printed but only once in the case
800 of multiple errors.
798 of multiple errors.
801
799
802 :fname: filename
800 :fname: filename
803 :lineno: line number
801 :lineno: line number
804 :line: actual content of the line
802 :line: actual content of the line
805 :msg: error message
803 :msg: error message
806 """
804 """
807 msgid = fname, lineno, line
805 msgid = fname, lineno, line
808 if msgid != self._lastseen:
806 if msgid != self._lastseen:
809 if blame:
807 if blame:
810 print("%s:%d (%s):" % (fname, lineno, blame))
808 print("%s:%d (%s):" % (fname, lineno, blame))
811 else:
809 else:
812 print("%s:%d:" % (fname, lineno))
810 print("%s:%d:" % (fname, lineno))
813 print(" > %s" % line)
811 print(" > %s" % line)
814 self._lastseen = msgid
812 self._lastseen = msgid
815 print(" " + msg)
813 print(" " + msg)
816
814
817
815
818 _defaultlogger = norepeatlogger()
816 _defaultlogger = norepeatlogger()
819
817
820
818
821 def getblame(f):
819 def getblame(f):
822 lines = []
820 lines = []
823 for l in os.popen('hg annotate -un %s' % f):
821 for l in os.popen('hg annotate -un %s' % f):
824 start, line = l.split(':', 1)
822 start, line = l.split(':', 1)
825 user, rev = start.split()
823 user, rev = start.split()
826 lines.append((line[1:-1], user, rev))
824 lines.append((line[1:-1], user, rev))
827 return lines
825 return lines
828
826
829
827
830 def checkfile(
828 def checkfile(
831 f,
829 f,
832 logfunc=_defaultlogger.log,
830 logfunc=_defaultlogger.log,
833 maxerr=None,
831 maxerr=None,
834 warnings=False,
832 warnings=False,
835 blame=False,
833 blame=False,
836 debug=False,
834 debug=False,
837 lineno=True,
835 lineno=True,
838 ):
836 ):
839 """checks style and portability of a given file
837 """checks style and portability of a given file
840
838
841 :f: filepath
839 :f: filepath
842 :logfunc: function used to report error
840 :logfunc: function used to report error
843 logfunc(filename, linenumber, linecontent, errormessage)
841 logfunc(filename, linenumber, linecontent, errormessage)
844 :maxerr: number of error to display before aborting.
842 :maxerr: number of error to display before aborting.
845 Set to false (default) to report all errors
843 Set to false (default) to report all errors
846
844
847 return True if no error is found, False otherwise.
845 return True if no error is found, False otherwise.
848 """
846 """
849 result = True
847 result = True
850
848
851 try:
849 try:
852 with opentext(f) as fp:
850 with opentext(f) as fp:
853 try:
851 try:
854 pre = fp.read()
852 pre = fp.read()
855 except UnicodeDecodeError as e:
853 except UnicodeDecodeError as e:
856 print("%s while reading %s" % (e, f))
854 print("%s while reading %s" % (e, f))
857 return result
855 return result
858 except IOError as e:
856 except IOError as e:
859 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
857 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
860 return result
858 return result
861
859
862 # context information shared while single checkfile() invocation
860 # context information shared while single checkfile() invocation
863 context = {'blamecache': None}
861 context = {'blamecache': None}
864
862
865 for name, match, magic, filters, pats in checks:
863 for name, match, magic, filters, pats in checks:
866 if debug:
864 if debug:
867 print(name, f)
865 print(name, f)
868 if not (re.match(match, f) or (magic and re.search(magic, pre))):
866 if not (re.match(match, f) or (magic and re.search(magic, pre))):
869 if debug:
867 if debug:
870 print(
868 print(
871 "Skipping %s for %s it doesn't match %s" % (name, match, f)
869 "Skipping %s for %s it doesn't match %s" % (name, match, f)
872 )
870 )
873 continue
871 continue
874 if "no-" "check-code" in pre:
872 if "no-" "check-code" in pre:
875 # If you're looking at this line, it's because a file has:
873 # If you're looking at this line, it's because a file has:
876 # no- check- code
874 # no- check- code
877 # but the reason to output skipping is to make life for
875 # but the reason to output skipping is to make life for
878 # tests easier. So, instead of writing it with a normal
876 # tests easier. So, instead of writing it with a normal
879 # spelling, we write it with the expected spelling from
877 # spelling, we write it with the expected spelling from
880 # tests/test-check-code.t
878 # tests/test-check-code.t
881 print("Skipping %s it has no-che?k-code (glob)" % f)
879 print("Skipping %s it has no-che?k-code (glob)" % f)
882 return "Skip" # skip checking this file
880 return "Skip" # skip checking this file
883
881
884 fc = _checkfiledata(
882 fc = _checkfiledata(
885 name,
883 name,
886 f,
884 f,
887 pre,
885 pre,
888 filters,
886 filters,
889 pats,
887 pats,
890 context,
888 context,
891 logfunc,
889 logfunc,
892 maxerr,
890 maxerr,
893 warnings,
891 warnings,
894 blame,
892 blame,
895 debug,
893 debug,
896 lineno,
894 lineno,
897 )
895 )
898 if fc:
896 if fc:
899 result = False
897 result = False
900
898
901 if f.endswith('.t') and "no-" "check-code" not in pre:
899 if f.endswith('.t') and "no-" "check-code" not in pre:
902 if debug:
900 if debug:
903 print("Checking embedded code in %s" % f)
901 print("Checking embedded code in %s" % f)
904
902
905 prelines = pre.splitlines()
903 prelines = pre.splitlines()
906 embeddederros = []
904 embeddederros = []
907 for name, embedded, filters, pats in embeddedchecks:
905 for name, embedded, filters, pats in embeddedchecks:
908 # "reset curmax at each repetition" treats maxerr as "max
906 # "reset curmax at each repetition" treats maxerr as "max
909 # nubmer of errors in an actual file per entry of
907 # nubmer of errors in an actual file per entry of
910 # (embedded)checks"
908 # (embedded)checks"
911 curmaxerr = maxerr
909 curmaxerr = maxerr
912
910
913 for found in embedded(f, prelines, embeddederros):
911 for found in embedded(f, prelines, embeddederros):
914 filename, starts, ends, code = found
912 filename, starts, ends, code = found
915 fc = _checkfiledata(
913 fc = _checkfiledata(
916 name,
914 name,
917 f,
915 f,
918 code,
916 code,
919 filters,
917 filters,
920 pats,
918 pats,
921 context,
919 context,
922 logfunc,
920 logfunc,
923 curmaxerr,
921 curmaxerr,
924 warnings,
922 warnings,
925 blame,
923 blame,
926 debug,
924 debug,
927 lineno,
925 lineno,
928 offset=starts - 1,
926 offset=starts - 1,
929 )
927 )
930 if fc:
928 if fc:
931 result = False
929 result = False
932 if curmaxerr:
930 if curmaxerr:
933 if fc >= curmaxerr:
931 if fc >= curmaxerr:
934 break
932 break
935 curmaxerr -= fc
933 curmaxerr -= fc
936
934
937 return result
935 return result
938
936
939
937
940 def _checkfiledata(
938 def _checkfiledata(
941 name,
939 name,
942 f,
940 f,
943 filedata,
941 filedata,
944 filters,
942 filters,
945 pats,
943 pats,
946 context,
944 context,
947 logfunc,
945 logfunc,
948 maxerr,
946 maxerr,
949 warnings,
947 warnings,
950 blame,
948 blame,
951 debug,
949 debug,
952 lineno,
950 lineno,
953 offset=None,
951 offset=None,
954 ):
952 ):
955 """Execute actual error check for file data
953 """Execute actual error check for file data
956
954
957 :name: of the checking category
955 :name: of the checking category
958 :f: filepath
956 :f: filepath
959 :filedata: content of a file
957 :filedata: content of a file
960 :filters: to be applied before checking
958 :filters: to be applied before checking
961 :pats: to detect errors
959 :pats: to detect errors
962 :context: a dict of information shared while single checkfile() invocation
960 :context: a dict of information shared while single checkfile() invocation
963 Valid keys: 'blamecache'.
961 Valid keys: 'blamecache'.
964 :logfunc: function used to report error
962 :logfunc: function used to report error
965 logfunc(filename, linenumber, linecontent, errormessage)
963 logfunc(filename, linenumber, linecontent, errormessage)
966 :maxerr: number of error to display before aborting, or False to
964 :maxerr: number of error to display before aborting, or False to
967 report all errors
965 report all errors
968 :warnings: whether warning level checks should be applied
966 :warnings: whether warning level checks should be applied
969 :blame: whether blame information should be displayed at error reporting
967 :blame: whether blame information should be displayed at error reporting
970 :debug: whether debug information should be displayed
968 :debug: whether debug information should be displayed
971 :lineno: whether lineno should be displayed at error reporting
969 :lineno: whether lineno should be displayed at error reporting
972 :offset: line number offset of 'filedata' in 'f' for checking
970 :offset: line number offset of 'filedata' in 'f' for checking
973 an embedded code fragment, or None (offset=0 is different
971 an embedded code fragment, or None (offset=0 is different
974 from offset=None)
972 from offset=None)
975
973
976 returns number of detected errors.
974 returns number of detected errors.
977 """
975 """
978 blamecache = context['blamecache']
976 blamecache = context['blamecache']
979 if offset is None:
977 if offset is None:
980 lineoffset = 0
978 lineoffset = 0
981 else:
979 else:
982 lineoffset = offset
980 lineoffset = offset
983
981
984 fc = 0
982 fc = 0
985 pre = post = filedata
983 pre = post = filedata
986
984
987 if True: # TODO: get rid of this redundant 'if' block
985 if True: # TODO: get rid of this redundant 'if' block
988 for p, r in filters:
986 for p, r in filters:
989 post = re.sub(p, r, post)
987 post = re.sub(p, r, post)
990 nerrs = len(pats[0]) # nerr elements are errors
988 nerrs = len(pats[0]) # nerr elements are errors
991 if warnings:
989 if warnings:
992 pats = pats[0] + pats[1]
990 pats = pats[0] + pats[1]
993 else:
991 else:
994 pats = pats[0]
992 pats = pats[0]
995 # print post # uncomment to show filtered version
993 # print post # uncomment to show filtered version
996
994
997 if debug:
995 if debug:
998 print("Checking %s for %s" % (name, f))
996 print("Checking %s for %s" % (name, f))
999
997
1000 prelines = None
998 prelines = None
1001 errors = []
999 errors = []
1002 for i, pat in enumerate(pats):
1000 for i, pat in enumerate(pats):
1003 if len(pat) == 3:
1001 if len(pat) == 3:
1004 p, msg, ignore = pat
1002 p, msg, ignore = pat
1005 else:
1003 else:
1006 p, msg = pat
1004 p, msg = pat
1007 ignore = None
1005 ignore = None
1008 if i >= nerrs:
1006 if i >= nerrs:
1009 msg = "warning: " + msg
1007 msg = "warning: " + msg
1010
1008
1011 pos = 0
1009 pos = 0
1012 n = 0
1010 n = 0
1013 for m in p.finditer(post):
1011 for m in p.finditer(post):
1014 if prelines is None:
1012 if prelines is None:
1015 prelines = pre.splitlines()
1013 prelines = pre.splitlines()
1016 postlines = post.splitlines(True)
1014 postlines = post.splitlines(True)
1017
1015
1018 start = m.start()
1016 start = m.start()
1019 while n < len(postlines):
1017 while n < len(postlines):
1020 step = len(postlines[n])
1018 step = len(postlines[n])
1021 if pos + step > start:
1019 if pos + step > start:
1022 break
1020 break
1023 pos += step
1021 pos += step
1024 n += 1
1022 n += 1
1025 l = prelines[n]
1023 l = prelines[n]
1026
1024
1027 if ignore and re.search(ignore, l, re.MULTILINE):
1025 if ignore and re.search(ignore, l, re.MULTILINE):
1028 if debug:
1026 if debug:
1029 print(
1027 print(
1030 "Skipping %s for %s:%s (ignore pattern)"
1028 "Skipping %s for %s:%s (ignore pattern)"
1031 % (name, f, (n + lineoffset))
1029 % (name, f, (n + lineoffset))
1032 )
1030 )
1033 continue
1031 continue
1034 bd = ""
1032 bd = ""
1035 if blame:
1033 if blame:
1036 bd = 'working directory'
1034 bd = 'working directory'
1037 if blamecache is None:
1035 if blamecache is None:
1038 blamecache = getblame(f)
1036 blamecache = getblame(f)
1039 context['blamecache'] = blamecache
1037 context['blamecache'] = blamecache
1040 if (n + lineoffset) < len(blamecache):
1038 if (n + lineoffset) < len(blamecache):
1041 bl, bu, br = blamecache[(n + lineoffset)]
1039 bl, bu, br = blamecache[(n + lineoffset)]
1042 if offset is None and bl == l:
1040 if offset is None and bl == l:
1043 bd = '%s@%s' % (bu, br)
1041 bd = '%s@%s' % (bu, br)
1044 elif offset is not None and bl.endswith(l):
1042 elif offset is not None and bl.endswith(l):
1045 # "offset is not None" means "checking
1043 # "offset is not None" means "checking
1046 # embedded code fragment". In this case,
1044 # embedded code fragment". In this case,
1047 # "l" does not have information about the
1045 # "l" does not have information about the
1048 # beginning of an *original* line in the
1046 # beginning of an *original* line in the
1049 # file (e.g. ' > ').
1047 # file (e.g. ' > ').
1050 # Therefore, use "str.endswith()", and
1048 # Therefore, use "str.endswith()", and
1051 # show "maybe" for a little loose
1049 # show "maybe" for a little loose
1052 # examination.
1050 # examination.
1053 bd = '%s@%s, maybe' % (bu, br)
1051 bd = '%s@%s, maybe' % (bu, br)
1054
1052
1055 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1053 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1056
1054
1057 errors.sort()
1055 errors.sort()
1058 for e in errors:
1056 for e in errors:
1059 logfunc(*e)
1057 logfunc(*e)
1060 fc += 1
1058 fc += 1
1061 if maxerr and fc >= maxerr:
1059 if maxerr and fc >= maxerr:
1062 print(" (too many errors, giving up)")
1060 print(" (too many errors, giving up)")
1063 break
1061 break
1064
1062
1065 return fc
1063 return fc
1066
1064
1067
1065
1068 def main():
1066 def main():
1069 parser = optparse.OptionParser("%prog [options] [files | -]")
1067 parser = optparse.OptionParser("%prog [options] [files | -]")
1070 parser.add_option(
1068 parser.add_option(
1071 "-w",
1069 "-w",
1072 "--warnings",
1070 "--warnings",
1073 action="store_true",
1071 action="store_true",
1074 help="include warning-level checks",
1072 help="include warning-level checks",
1075 )
1073 )
1076 parser.add_option(
1074 parser.add_option(
1077 "-p", "--per-file", type="int", help="max warnings per file"
1075 "-p", "--per-file", type="int", help="max warnings per file"
1078 )
1076 )
1079 parser.add_option(
1077 parser.add_option(
1080 "-b",
1078 "-b",
1081 "--blame",
1079 "--blame",
1082 action="store_true",
1080 action="store_true",
1083 help="use annotate to generate blame info",
1081 help="use annotate to generate blame info",
1084 )
1082 )
1085 parser.add_option(
1083 parser.add_option(
1086 "", "--debug", action="store_true", help="show debug information"
1084 "", "--debug", action="store_true", help="show debug information"
1087 )
1085 )
1088 parser.add_option(
1086 parser.add_option(
1089 "",
1087 "",
1090 "--nolineno",
1088 "--nolineno",
1091 action="store_false",
1089 action="store_false",
1092 dest='lineno',
1090 dest='lineno',
1093 help="don't show line numbers",
1091 help="don't show line numbers",
1094 )
1092 )
1095
1093
1096 parser.set_defaults(
1094 parser.set_defaults(
1097 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1095 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1098 )
1096 )
1099 (options, args) = parser.parse_args()
1097 (options, args) = parser.parse_args()
1100
1098
1101 if len(args) == 0:
1099 if len(args) == 0:
1102 check = glob.glob("*")
1100 check = glob.glob("*")
1103 elif args == ['-']:
1101 elif args == ['-']:
1104 # read file list from stdin
1102 # read file list from stdin
1105 check = sys.stdin.read().splitlines()
1103 check = sys.stdin.read().splitlines()
1106 else:
1104 else:
1107 check = args
1105 check = args
1108
1106
1109 _preparepats()
1107 _preparepats()
1110
1108
1111 ret = 0
1109 ret = 0
1112 for f in check:
1110 for f in check:
1113 if not checkfile(
1111 if not checkfile(
1114 f,
1112 f,
1115 maxerr=options.per_file,
1113 maxerr=options.per_file,
1116 warnings=options.warnings,
1114 warnings=options.warnings,
1117 blame=options.blame,
1115 blame=options.blame,
1118 debug=options.debug,
1116 debug=options.debug,
1119 lineno=options.lineno,
1117 lineno=options.lineno,
1120 ):
1118 ):
1121 ret = 1
1119 ret = 1
1122 return ret
1120 return ret
1123
1121
1124
1122
1125 if __name__ == "__main__":
1123 if __name__ == "__main__":
1126 sys.exit(main())
1124 sys.exit(main())
@@ -1,598 +1,598 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@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 datetime
10 import datetime
11 import errno
11 import errno
12 import os
12 import os
13 import pickle
13 import re
14 import re
14 import shlex
15 import shlex
15 import subprocess
16 import subprocess
16
17
17 from mercurial.i18n import _
18 from mercurial.i18n import _
18 from mercurial.pycompat import open
19 from mercurial.pycompat import open
19 from mercurial import (
20 from mercurial import (
20 encoding,
21 encoding,
21 error,
22 error,
22 phases,
23 phases,
23 pycompat,
24 pycompat,
24 util,
25 util,
25 )
26 )
26 from mercurial.utils import procutil
27 from mercurial.utils import procutil
27
28
28 pickle = util.pickle
29 propertycache = util.propertycache
29 propertycache = util.propertycache
30
30
31
31
32 def _encodeornone(d):
32 def _encodeornone(d):
33 if d is None:
33 if d is None:
34 return
34 return
35 return d.encode('latin1')
35 return d.encode('latin1')
36
36
37
37
38 class _shlexpy3proxy(object):
38 class _shlexpy3proxy(object):
39 def __init__(self, l):
39 def __init__(self, l):
40 self._l = l
40 self._l = l
41
41
42 def __iter__(self):
42 def __iter__(self):
43 return (_encodeornone(v) for v in self._l)
43 return (_encodeornone(v) for v in self._l)
44
44
45 def get_token(self):
45 def get_token(self):
46 return _encodeornone(self._l.get_token())
46 return _encodeornone(self._l.get_token())
47
47
48 @property
48 @property
49 def infile(self):
49 def infile(self):
50 return self._l.infile or b'<unknown>'
50 return self._l.infile or b'<unknown>'
51
51
52 @property
52 @property
53 def lineno(self):
53 def lineno(self):
54 return self._l.lineno
54 return self._l.lineno
55
55
56
56
57 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
57 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
58 if data is None:
58 if data is None:
59 if pycompat.ispy3:
59 if pycompat.ispy3:
60 data = open(filepath, b'r', encoding='latin1')
60 data = open(filepath, b'r', encoding='latin1')
61 else:
61 else:
62 data = open(filepath, b'r')
62 data = open(filepath, b'r')
63 else:
63 else:
64 if filepath is not None:
64 if filepath is not None:
65 raise error.ProgrammingError(
65 raise error.ProgrammingError(
66 b'shlexer only accepts data or filepath, not both'
66 b'shlexer only accepts data or filepath, not both'
67 )
67 )
68 if pycompat.ispy3:
68 if pycompat.ispy3:
69 data = data.decode('latin1')
69 data = data.decode('latin1')
70 l = shlex.shlex(data, infile=filepath, posix=True)
70 l = shlex.shlex(data, infile=filepath, posix=True)
71 if whitespace is not None:
71 if whitespace is not None:
72 l.whitespace_split = True
72 l.whitespace_split = True
73 if pycompat.ispy3:
73 if pycompat.ispy3:
74 l.whitespace += whitespace.decode('latin1')
74 l.whitespace += whitespace.decode('latin1')
75 else:
75 else:
76 l.whitespace += whitespace
76 l.whitespace += whitespace
77 if wordchars is not None:
77 if wordchars is not None:
78 if pycompat.ispy3:
78 if pycompat.ispy3:
79 l.wordchars += wordchars.decode('latin1')
79 l.wordchars += wordchars.decode('latin1')
80 else:
80 else:
81 l.wordchars += wordchars
81 l.wordchars += wordchars
82 if pycompat.ispy3:
82 if pycompat.ispy3:
83 return _shlexpy3proxy(l)
83 return _shlexpy3proxy(l)
84 return l
84 return l
85
85
86
86
87 if pycompat.ispy3:
87 if pycompat.ispy3:
88 base64_encodebytes = base64.encodebytes
88 base64_encodebytes = base64.encodebytes
89 base64_decodebytes = base64.decodebytes
89 base64_decodebytes = base64.decodebytes
90 else:
90 else:
91 base64_encodebytes = base64.encodestring
91 base64_encodebytes = base64.encodestring
92 base64_decodebytes = base64.decodestring
92 base64_decodebytes = base64.decodestring
93
93
94
94
95 def encodeargs(args):
95 def encodeargs(args):
96 def encodearg(s):
96 def encodearg(s):
97 lines = base64_encodebytes(s)
97 lines = base64_encodebytes(s)
98 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
98 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
99 return b''.join(lines)
99 return b''.join(lines)
100
100
101 s = pickle.dumps(args)
101 s = pickle.dumps(args)
102 return encodearg(s)
102 return encodearg(s)
103
103
104
104
105 def decodeargs(s):
105 def decodeargs(s):
106 s = base64_decodebytes(s)
106 s = base64_decodebytes(s)
107 return pickle.loads(s)
107 return pickle.loads(s)
108
108
109
109
110 class MissingTool(Exception):
110 class MissingTool(Exception):
111 pass
111 pass
112
112
113
113
114 def checktool(exe, name=None, abort=True):
114 def checktool(exe, name=None, abort=True):
115 name = name or exe
115 name = name or exe
116 if not procutil.findexe(exe):
116 if not procutil.findexe(exe):
117 if abort:
117 if abort:
118 exc = error.Abort
118 exc = error.Abort
119 else:
119 else:
120 exc = MissingTool
120 exc = MissingTool
121 raise exc(_(b'cannot find required "%s" tool') % name)
121 raise exc(_(b'cannot find required "%s" tool') % name)
122
122
123
123
124 class NoRepo(Exception):
124 class NoRepo(Exception):
125 pass
125 pass
126
126
127
127
128 SKIPREV = b'SKIP'
128 SKIPREV = b'SKIP'
129
129
130
130
131 class commit(object):
131 class commit(object):
132 def __init__(
132 def __init__(
133 self,
133 self,
134 author,
134 author,
135 date,
135 date,
136 desc,
136 desc,
137 parents,
137 parents,
138 branch=None,
138 branch=None,
139 rev=None,
139 rev=None,
140 extra=None,
140 extra=None,
141 sortkey=None,
141 sortkey=None,
142 saverev=True,
142 saverev=True,
143 phase=phases.draft,
143 phase=phases.draft,
144 optparents=None,
144 optparents=None,
145 ctx=None,
145 ctx=None,
146 ):
146 ):
147 self.author = author or b'unknown'
147 self.author = author or b'unknown'
148 self.date = date or b'0 0'
148 self.date = date or b'0 0'
149 self.desc = desc
149 self.desc = desc
150 self.parents = parents # will be converted and used as parents
150 self.parents = parents # will be converted and used as parents
151 self.optparents = optparents or [] # will be used if already converted
151 self.optparents = optparents or [] # will be used if already converted
152 self.branch = branch
152 self.branch = branch
153 self.rev = rev
153 self.rev = rev
154 self.extra = extra or {}
154 self.extra = extra or {}
155 self.sortkey = sortkey
155 self.sortkey = sortkey
156 self.saverev = saverev
156 self.saverev = saverev
157 self.phase = phase
157 self.phase = phase
158 self.ctx = ctx # for hg to hg conversions
158 self.ctx = ctx # for hg to hg conversions
159
159
160
160
161 class converter_source(object):
161 class converter_source(object):
162 """Conversion source interface"""
162 """Conversion source interface"""
163
163
164 def __init__(self, ui, repotype, path=None, revs=None):
164 def __init__(self, ui, repotype, path=None, revs=None):
165 """Initialize conversion source (or raise NoRepo("message")
165 """Initialize conversion source (or raise NoRepo("message")
166 exception if path is not a valid repository)"""
166 exception if path is not a valid repository)"""
167 self.ui = ui
167 self.ui = ui
168 self.path = path
168 self.path = path
169 self.revs = revs
169 self.revs = revs
170 self.repotype = repotype
170 self.repotype = repotype
171
171
172 self.encoding = b'utf-8'
172 self.encoding = b'utf-8'
173
173
174 def checkhexformat(self, revstr, mapname=b'splicemap'):
174 def checkhexformat(self, revstr, mapname=b'splicemap'):
175 """fails if revstr is not a 40 byte hex. mercurial and git both uses
175 """fails if revstr is not a 40 byte hex. mercurial and git both uses
176 such format for their revision numbering
176 such format for their revision numbering
177 """
177 """
178 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
178 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
179 raise error.Abort(
179 raise error.Abort(
180 _(b'%s entry %s is not a valid revision identifier')
180 _(b'%s entry %s is not a valid revision identifier')
181 % (mapname, revstr)
181 % (mapname, revstr)
182 )
182 )
183
183
184 def before(self):
184 def before(self):
185 pass
185 pass
186
186
187 def after(self):
187 def after(self):
188 pass
188 pass
189
189
190 def targetfilebelongstosource(self, targetfilename):
190 def targetfilebelongstosource(self, targetfilename):
191 """Returns true if the given targetfile belongs to the source repo. This
191 """Returns true if the given targetfile belongs to the source repo. This
192 is useful when only a subdirectory of the target belongs to the source
192 is useful when only a subdirectory of the target belongs to the source
193 repo."""
193 repo."""
194 # For normal full repo converts, this is always True.
194 # For normal full repo converts, this is always True.
195 return True
195 return True
196
196
197 def setrevmap(self, revmap):
197 def setrevmap(self, revmap):
198 """set the map of already-converted revisions"""
198 """set the map of already-converted revisions"""
199
199
200 def getheads(self):
200 def getheads(self):
201 """Return a list of this repository's heads"""
201 """Return a list of this repository's heads"""
202 raise NotImplementedError
202 raise NotImplementedError
203
203
204 def getfile(self, name, rev):
204 def getfile(self, name, rev):
205 """Return a pair (data, mode) where data is the file content
205 """Return a pair (data, mode) where data is the file content
206 as a string and mode one of '', 'x' or 'l'. rev is the
206 as a string and mode one of '', 'x' or 'l'. rev is the
207 identifier returned by a previous call to getchanges().
207 identifier returned by a previous call to getchanges().
208 Data is None if file is missing/deleted in rev.
208 Data is None if file is missing/deleted in rev.
209 """
209 """
210 raise NotImplementedError
210 raise NotImplementedError
211
211
212 def getchanges(self, version, full):
212 def getchanges(self, version, full):
213 """Returns a tuple of (files, copies, cleanp2).
213 """Returns a tuple of (files, copies, cleanp2).
214
214
215 files is a sorted list of (filename, id) tuples for all files
215 files is a sorted list of (filename, id) tuples for all files
216 changed between version and its first parent returned by
216 changed between version and its first parent returned by
217 getcommit(). If full, all files in that revision is returned.
217 getcommit(). If full, all files in that revision is returned.
218 id is the source revision id of the file.
218 id is the source revision id of the file.
219
219
220 copies is a dictionary of dest: source
220 copies is a dictionary of dest: source
221
221
222 cleanp2 is the set of files filenames that are clean against p2.
222 cleanp2 is the set of files filenames that are clean against p2.
223 (Files that are clean against p1 are already not in files (unless
223 (Files that are clean against p1 are already not in files (unless
224 full). This makes it possible to handle p2 clean files similarly.)
224 full). This makes it possible to handle p2 clean files similarly.)
225 """
225 """
226 raise NotImplementedError
226 raise NotImplementedError
227
227
228 def getcommit(self, version):
228 def getcommit(self, version):
229 """Return the commit object for version"""
229 """Return the commit object for version"""
230 raise NotImplementedError
230 raise NotImplementedError
231
231
232 def numcommits(self):
232 def numcommits(self):
233 """Return the number of commits in this source.
233 """Return the number of commits in this source.
234
234
235 If unknown, return None.
235 If unknown, return None.
236 """
236 """
237 return None
237 return None
238
238
239 def gettags(self):
239 def gettags(self):
240 """Return the tags as a dictionary of name: revision
240 """Return the tags as a dictionary of name: revision
241
241
242 Tag names must be UTF-8 strings.
242 Tag names must be UTF-8 strings.
243 """
243 """
244 raise NotImplementedError
244 raise NotImplementedError
245
245
246 def recode(self, s, encoding=None):
246 def recode(self, s, encoding=None):
247 if not encoding:
247 if not encoding:
248 encoding = self.encoding or b'utf-8'
248 encoding = self.encoding or b'utf-8'
249
249
250 if isinstance(s, pycompat.unicode):
250 if isinstance(s, pycompat.unicode):
251 return s.encode("utf-8")
251 return s.encode("utf-8")
252 try:
252 try:
253 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
253 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
254 except UnicodeError:
254 except UnicodeError:
255 try:
255 try:
256 return s.decode("latin-1").encode("utf-8")
256 return s.decode("latin-1").encode("utf-8")
257 except UnicodeError:
257 except UnicodeError:
258 return s.decode(pycompat.sysstr(encoding), "replace").encode(
258 return s.decode(pycompat.sysstr(encoding), "replace").encode(
259 "utf-8"
259 "utf-8"
260 )
260 )
261
261
262 def getchangedfiles(self, rev, i):
262 def getchangedfiles(self, rev, i):
263 """Return the files changed by rev compared to parent[i].
263 """Return the files changed by rev compared to parent[i].
264
264
265 i is an index selecting one of the parents of rev. The return
265 i is an index selecting one of the parents of rev. The return
266 value should be the list of files that are different in rev and
266 value should be the list of files that are different in rev and
267 this parent.
267 this parent.
268
268
269 If rev has no parents, i is None.
269 If rev has no parents, i is None.
270
270
271 This function is only needed to support --filemap
271 This function is only needed to support --filemap
272 """
272 """
273 raise NotImplementedError
273 raise NotImplementedError
274
274
275 def converted(self, rev, sinkrev):
275 def converted(self, rev, sinkrev):
276 '''Notify the source that a revision has been converted.'''
276 '''Notify the source that a revision has been converted.'''
277
277
278 def hasnativeorder(self):
278 def hasnativeorder(self):
279 """Return true if this source has a meaningful, native revision
279 """Return true if this source has a meaningful, native revision
280 order. For instance, Mercurial revisions are store sequentially
280 order. For instance, Mercurial revisions are store sequentially
281 while there is no such global ordering with Darcs.
281 while there is no such global ordering with Darcs.
282 """
282 """
283 return False
283 return False
284
284
285 def hasnativeclose(self):
285 def hasnativeclose(self):
286 """Return true if this source has ability to close branch."""
286 """Return true if this source has ability to close branch."""
287 return False
287 return False
288
288
289 def lookuprev(self, rev):
289 def lookuprev(self, rev):
290 """If rev is a meaningful revision reference in source, return
290 """If rev is a meaningful revision reference in source, return
291 the referenced identifier in the same format used by getcommit().
291 the referenced identifier in the same format used by getcommit().
292 return None otherwise.
292 return None otherwise.
293 """
293 """
294 return None
294 return None
295
295
296 def getbookmarks(self):
296 def getbookmarks(self):
297 """Return the bookmarks as a dictionary of name: revision
297 """Return the bookmarks as a dictionary of name: revision
298
298
299 Bookmark names are to be UTF-8 strings.
299 Bookmark names are to be UTF-8 strings.
300 """
300 """
301 return {}
301 return {}
302
302
303 def checkrevformat(self, revstr, mapname=b'splicemap'):
303 def checkrevformat(self, revstr, mapname=b'splicemap'):
304 """revstr is a string that describes a revision in the given
304 """revstr is a string that describes a revision in the given
305 source control system. Return true if revstr has correct
305 source control system. Return true if revstr has correct
306 format.
306 format.
307 """
307 """
308 return True
308 return True
309
309
310
310
311 class converter_sink(object):
311 class converter_sink(object):
312 """Conversion sink (target) interface"""
312 """Conversion sink (target) interface"""
313
313
314 def __init__(self, ui, repotype, path):
314 def __init__(self, ui, repotype, path):
315 """Initialize conversion sink (or raise NoRepo("message")
315 """Initialize conversion sink (or raise NoRepo("message")
316 exception if path is not a valid repository)
316 exception if path is not a valid repository)
317
317
318 created is a list of paths to remove if a fatal error occurs
318 created is a list of paths to remove if a fatal error occurs
319 later"""
319 later"""
320 self.ui = ui
320 self.ui = ui
321 self.path = path
321 self.path = path
322 self.created = []
322 self.created = []
323 self.repotype = repotype
323 self.repotype = repotype
324
324
325 def revmapfile(self):
325 def revmapfile(self):
326 """Path to a file that will contain lines
326 """Path to a file that will contain lines
327 source_rev_id sink_rev_id
327 source_rev_id sink_rev_id
328 mapping equivalent revision identifiers for each system."""
328 mapping equivalent revision identifiers for each system."""
329 raise NotImplementedError
329 raise NotImplementedError
330
330
331 def authorfile(self):
331 def authorfile(self):
332 """Path to a file that will contain lines
332 """Path to a file that will contain lines
333 srcauthor=dstauthor
333 srcauthor=dstauthor
334 mapping equivalent authors identifiers for each system."""
334 mapping equivalent authors identifiers for each system."""
335 return None
335 return None
336
336
337 def putcommit(
337 def putcommit(
338 self, files, copies, parents, commit, source, revmap, full, cleanp2
338 self, files, copies, parents, commit, source, revmap, full, cleanp2
339 ):
339 ):
340 """Create a revision with all changed files listed in 'files'
340 """Create a revision with all changed files listed in 'files'
341 and having listed parents. 'commit' is a commit object
341 and having listed parents. 'commit' is a commit object
342 containing at a minimum the author, date, and message for this
342 containing at a minimum the author, date, and message for this
343 changeset. 'files' is a list of (path, version) tuples,
343 changeset. 'files' is a list of (path, version) tuples,
344 'copies' is a dictionary mapping destinations to sources,
344 'copies' is a dictionary mapping destinations to sources,
345 'source' is the source repository, and 'revmap' is a mapfile
345 'source' is the source repository, and 'revmap' is a mapfile
346 of source revisions to converted revisions. Only getfile() and
346 of source revisions to converted revisions. Only getfile() and
347 lookuprev() should be called on 'source'. 'full' means that 'files'
347 lookuprev() should be called on 'source'. 'full' means that 'files'
348 is complete and all other files should be removed.
348 is complete and all other files should be removed.
349 'cleanp2' is a set of the filenames that are unchanged from p2
349 'cleanp2' is a set of the filenames that are unchanged from p2
350 (only in the common merge case where there two parents).
350 (only in the common merge case where there two parents).
351
351
352 Note that the sink repository is not told to update itself to
352 Note that the sink repository is not told to update itself to
353 a particular revision (or even what that revision would be)
353 a particular revision (or even what that revision would be)
354 before it receives the file data.
354 before it receives the file data.
355 """
355 """
356 raise NotImplementedError
356 raise NotImplementedError
357
357
358 def puttags(self, tags):
358 def puttags(self, tags):
359 """Put tags into sink.
359 """Put tags into sink.
360
360
361 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
361 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
362 Return a pair (tag_revision, tag_parent_revision), or (None, None)
362 Return a pair (tag_revision, tag_parent_revision), or (None, None)
363 if nothing was changed.
363 if nothing was changed.
364 """
364 """
365 raise NotImplementedError
365 raise NotImplementedError
366
366
367 def setbranch(self, branch, pbranches):
367 def setbranch(self, branch, pbranches):
368 """Set the current branch name. Called before the first putcommit
368 """Set the current branch name. Called before the first putcommit
369 on the branch.
369 on the branch.
370 branch: branch name for subsequent commits
370 branch: branch name for subsequent commits
371 pbranches: (converted parent revision, parent branch) tuples"""
371 pbranches: (converted parent revision, parent branch) tuples"""
372
372
373 def setfilemapmode(self, active):
373 def setfilemapmode(self, active):
374 """Tell the destination that we're using a filemap
374 """Tell the destination that we're using a filemap
375
375
376 Some converter_sources (svn in particular) can claim that a file
376 Some converter_sources (svn in particular) can claim that a file
377 was changed in a revision, even if there was no change. This method
377 was changed in a revision, even if there was no change. This method
378 tells the destination that we're using a filemap and that it should
378 tells the destination that we're using a filemap and that it should
379 filter empty revisions.
379 filter empty revisions.
380 """
380 """
381
381
382 def before(self):
382 def before(self):
383 pass
383 pass
384
384
385 def after(self):
385 def after(self):
386 pass
386 pass
387
387
388 def putbookmarks(self, bookmarks):
388 def putbookmarks(self, bookmarks):
389 """Put bookmarks into sink.
389 """Put bookmarks into sink.
390
390
391 bookmarks: {bookmarkname: sink_rev_id, ...}
391 bookmarks: {bookmarkname: sink_rev_id, ...}
392 where bookmarkname is an UTF-8 string.
392 where bookmarkname is an UTF-8 string.
393 """
393 """
394
394
395 def hascommitfrommap(self, rev):
395 def hascommitfrommap(self, rev):
396 """Return False if a rev mentioned in a filemap is known to not be
396 """Return False if a rev mentioned in a filemap is known to not be
397 present."""
397 present."""
398 raise NotImplementedError
398 raise NotImplementedError
399
399
400 def hascommitforsplicemap(self, rev):
400 def hascommitforsplicemap(self, rev):
401 """This method is for the special needs for splicemap handling and not
401 """This method is for the special needs for splicemap handling and not
402 for general use. Returns True if the sink contains rev, aborts on some
402 for general use. Returns True if the sink contains rev, aborts on some
403 special cases."""
403 special cases."""
404 raise NotImplementedError
404 raise NotImplementedError
405
405
406
406
407 class commandline(object):
407 class commandline(object):
408 def __init__(self, ui, command):
408 def __init__(self, ui, command):
409 self.ui = ui
409 self.ui = ui
410 self.command = command
410 self.command = command
411
411
412 def prerun(self):
412 def prerun(self):
413 pass
413 pass
414
414
415 def postrun(self):
415 def postrun(self):
416 pass
416 pass
417
417
418 def _cmdline(self, cmd, *args, **kwargs):
418 def _cmdline(self, cmd, *args, **kwargs):
419 kwargs = pycompat.byteskwargs(kwargs)
419 kwargs = pycompat.byteskwargs(kwargs)
420 cmdline = [self.command, cmd] + list(args)
420 cmdline = [self.command, cmd] + list(args)
421 for k, v in pycompat.iteritems(kwargs):
421 for k, v in pycompat.iteritems(kwargs):
422 if len(k) == 1:
422 if len(k) == 1:
423 cmdline.append(b'-' + k)
423 cmdline.append(b'-' + k)
424 else:
424 else:
425 cmdline.append(b'--' + k.replace(b'_', b'-'))
425 cmdline.append(b'--' + k.replace(b'_', b'-'))
426 try:
426 try:
427 if len(k) == 1:
427 if len(k) == 1:
428 cmdline.append(b'' + v)
428 cmdline.append(b'' + v)
429 else:
429 else:
430 cmdline[-1] += b'=' + v
430 cmdline[-1] += b'=' + v
431 except TypeError:
431 except TypeError:
432 pass
432 pass
433 cmdline = [procutil.shellquote(arg) for arg in cmdline]
433 cmdline = [procutil.shellquote(arg) for arg in cmdline]
434 if not self.ui.debugflag:
434 if not self.ui.debugflag:
435 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
435 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
436 cmdline = b' '.join(cmdline)
436 cmdline = b' '.join(cmdline)
437 return cmdline
437 return cmdline
438
438
439 def _run(self, cmd, *args, **kwargs):
439 def _run(self, cmd, *args, **kwargs):
440 def popen(cmdline):
440 def popen(cmdline):
441 p = subprocess.Popen(
441 p = subprocess.Popen(
442 procutil.tonativestr(cmdline),
442 procutil.tonativestr(cmdline),
443 shell=True,
443 shell=True,
444 bufsize=-1,
444 bufsize=-1,
445 close_fds=procutil.closefds,
445 close_fds=procutil.closefds,
446 stdout=subprocess.PIPE,
446 stdout=subprocess.PIPE,
447 )
447 )
448 return p
448 return p
449
449
450 return self._dorun(popen, cmd, *args, **kwargs)
450 return self._dorun(popen, cmd, *args, **kwargs)
451
451
452 def _run2(self, cmd, *args, **kwargs):
452 def _run2(self, cmd, *args, **kwargs):
453 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
453 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
454
454
455 def _run3(self, cmd, *args, **kwargs):
455 def _run3(self, cmd, *args, **kwargs):
456 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
456 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
457
457
458 def _dorun(self, openfunc, cmd, *args, **kwargs):
458 def _dorun(self, openfunc, cmd, *args, **kwargs):
459 cmdline = self._cmdline(cmd, *args, **kwargs)
459 cmdline = self._cmdline(cmd, *args, **kwargs)
460 self.ui.debug(b'running: %s\n' % (cmdline,))
460 self.ui.debug(b'running: %s\n' % (cmdline,))
461 self.prerun()
461 self.prerun()
462 try:
462 try:
463 return openfunc(cmdline)
463 return openfunc(cmdline)
464 finally:
464 finally:
465 self.postrun()
465 self.postrun()
466
466
467 def run(self, cmd, *args, **kwargs):
467 def run(self, cmd, *args, **kwargs):
468 p = self._run(cmd, *args, **kwargs)
468 p = self._run(cmd, *args, **kwargs)
469 output = p.communicate()[0]
469 output = p.communicate()[0]
470 self.ui.debug(output)
470 self.ui.debug(output)
471 return output, p.returncode
471 return output, p.returncode
472
472
473 def runlines(self, cmd, *args, **kwargs):
473 def runlines(self, cmd, *args, **kwargs):
474 p = self._run(cmd, *args, **kwargs)
474 p = self._run(cmd, *args, **kwargs)
475 output = p.stdout.readlines()
475 output = p.stdout.readlines()
476 p.wait()
476 p.wait()
477 self.ui.debug(b''.join(output))
477 self.ui.debug(b''.join(output))
478 return output, p.returncode
478 return output, p.returncode
479
479
480 def checkexit(self, status, output=b''):
480 def checkexit(self, status, output=b''):
481 if status:
481 if status:
482 if output:
482 if output:
483 self.ui.warn(_(b'%s error:\n') % self.command)
483 self.ui.warn(_(b'%s error:\n') % self.command)
484 self.ui.warn(output)
484 self.ui.warn(output)
485 msg = procutil.explainexit(status)
485 msg = procutil.explainexit(status)
486 raise error.Abort(b'%s %s' % (self.command, msg))
486 raise error.Abort(b'%s %s' % (self.command, msg))
487
487
488 def run0(self, cmd, *args, **kwargs):
488 def run0(self, cmd, *args, **kwargs):
489 output, status = self.run(cmd, *args, **kwargs)
489 output, status = self.run(cmd, *args, **kwargs)
490 self.checkexit(status, output)
490 self.checkexit(status, output)
491 return output
491 return output
492
492
493 def runlines0(self, cmd, *args, **kwargs):
493 def runlines0(self, cmd, *args, **kwargs):
494 output, status = self.runlines(cmd, *args, **kwargs)
494 output, status = self.runlines(cmd, *args, **kwargs)
495 self.checkexit(status, b''.join(output))
495 self.checkexit(status, b''.join(output))
496 return output
496 return output
497
497
498 @propertycache
498 @propertycache
499 def argmax(self):
499 def argmax(self):
500 # POSIX requires at least 4096 bytes for ARG_MAX
500 # POSIX requires at least 4096 bytes for ARG_MAX
501 argmax = 4096
501 argmax = 4096
502 try:
502 try:
503 argmax = os.sysconf("SC_ARG_MAX")
503 argmax = os.sysconf("SC_ARG_MAX")
504 except (AttributeError, ValueError):
504 except (AttributeError, ValueError):
505 pass
505 pass
506
506
507 # Windows shells impose their own limits on command line length,
507 # Windows shells impose their own limits on command line length,
508 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
508 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
509 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
509 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
510 # details about cmd.exe limitations.
510 # details about cmd.exe limitations.
511
511
512 # Since ARG_MAX is for command line _and_ environment, lower our limit
512 # Since ARG_MAX is for command line _and_ environment, lower our limit
513 # (and make happy Windows shells while doing this).
513 # (and make happy Windows shells while doing this).
514 return argmax // 2 - 1
514 return argmax // 2 - 1
515
515
516 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
516 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
517 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
517 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
518 limit = self.argmax - cmdlen
518 limit = self.argmax - cmdlen
519 numbytes = 0
519 numbytes = 0
520 fl = []
520 fl = []
521 for fn in arglist:
521 for fn in arglist:
522 b = len(fn) + 3
522 b = len(fn) + 3
523 if numbytes + b < limit or len(fl) == 0:
523 if numbytes + b < limit or len(fl) == 0:
524 fl.append(fn)
524 fl.append(fn)
525 numbytes += b
525 numbytes += b
526 else:
526 else:
527 yield fl
527 yield fl
528 fl = [fn]
528 fl = [fn]
529 numbytes = b
529 numbytes = b
530 if fl:
530 if fl:
531 yield fl
531 yield fl
532
532
533 def xargs(self, arglist, cmd, *args, **kwargs):
533 def xargs(self, arglist, cmd, *args, **kwargs):
534 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
534 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
535 self.run0(cmd, *(list(args) + l), **kwargs)
535 self.run0(cmd, *(list(args) + l), **kwargs)
536
536
537
537
538 class mapfile(dict):
538 class mapfile(dict):
539 def __init__(self, ui, path):
539 def __init__(self, ui, path):
540 super(mapfile, self).__init__()
540 super(mapfile, self).__init__()
541 self.ui = ui
541 self.ui = ui
542 self.path = path
542 self.path = path
543 self.fp = None
543 self.fp = None
544 self.order = []
544 self.order = []
545 self._read()
545 self._read()
546
546
547 def _read(self):
547 def _read(self):
548 if not self.path:
548 if not self.path:
549 return
549 return
550 try:
550 try:
551 fp = open(self.path, b'rb')
551 fp = open(self.path, b'rb')
552 except IOError as err:
552 except IOError as err:
553 if err.errno != errno.ENOENT:
553 if err.errno != errno.ENOENT:
554 raise
554 raise
555 return
555 return
556 for i, line in enumerate(util.iterfile(fp)):
556 for i, line in enumerate(util.iterfile(fp)):
557 line = line.splitlines()[0].rstrip()
557 line = line.splitlines()[0].rstrip()
558 if not line:
558 if not line:
559 # Ignore blank lines
559 # Ignore blank lines
560 continue
560 continue
561 try:
561 try:
562 key, value = line.rsplit(b' ', 1)
562 key, value = line.rsplit(b' ', 1)
563 except ValueError:
563 except ValueError:
564 raise error.Abort(
564 raise error.Abort(
565 _(b'syntax error in %s(%d): key/value pair expected')
565 _(b'syntax error in %s(%d): key/value pair expected')
566 % (self.path, i + 1)
566 % (self.path, i + 1)
567 )
567 )
568 if key not in self:
568 if key not in self:
569 self.order.append(key)
569 self.order.append(key)
570 super(mapfile, self).__setitem__(key, value)
570 super(mapfile, self).__setitem__(key, value)
571 fp.close()
571 fp.close()
572
572
573 def __setitem__(self, key, value):
573 def __setitem__(self, key, value):
574 if self.fp is None:
574 if self.fp is None:
575 try:
575 try:
576 self.fp = open(self.path, b'ab')
576 self.fp = open(self.path, b'ab')
577 except IOError as err:
577 except IOError as err:
578 raise error.Abort(
578 raise error.Abort(
579 _(b'could not open map file %r: %s')
579 _(b'could not open map file %r: %s')
580 % (self.path, encoding.strtolocal(err.strerror))
580 % (self.path, encoding.strtolocal(err.strerror))
581 )
581 )
582 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
582 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
583 self.fp.flush()
583 self.fp.flush()
584 super(mapfile, self).__setitem__(key, value)
584 super(mapfile, self).__setitem__(key, value)
585
585
586 def close(self):
586 def close(self):
587 if self.fp:
587 if self.fp:
588 self.fp.close()
588 self.fp.close()
589 self.fp = None
589 self.fp = None
590
590
591
591
592 def makedatetimestamp(t):
592 def makedatetimestamp(t):
593 """Like dateutil.makedate() but for time t instead of current time"""
593 """Like dateutil.makedate() but for time t instead of current time"""
594 delta = datetime.datetime.utcfromtimestamp(
594 delta = datetime.datetime.utcfromtimestamp(
595 t
595 t
596 ) - datetime.datetime.fromtimestamp(t)
596 ) - datetime.datetime.fromtimestamp(t)
597 tz = delta.days * 86400 + delta.seconds
597 tz = delta.days * 86400 + delta.seconds
598 return t, tz
598 return t, tz
@@ -1,1070 +1,1069 b''
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 functools
9 import functools
10 import os
10 import os
11 import pickle
11 import re
12 import re
12
13
13 from mercurial.i18n import _
14 from mercurial.i18n import _
14 from mercurial.pycompat import open
15 from mercurial.pycompat import open
15 from mercurial import (
16 from mercurial import (
16 encoding,
17 encoding,
17 error,
18 error,
18 hook,
19 hook,
19 pycompat,
20 pycompat,
20 util,
21 util,
21 )
22 )
22 from mercurial.utils import (
23 from mercurial.utils import (
23 dateutil,
24 dateutil,
24 procutil,
25 procutil,
25 stringutil,
26 stringutil,
26 )
27 )
27
28
28 pickle = util.pickle
29
30
29
31 class logentry(object):
30 class logentry(object):
32 """Class logentry has the following attributes:
31 """Class logentry has the following attributes:
33 .author - author name as CVS knows it
32 .author - author name as CVS knows it
34 .branch - name of branch this revision is on
33 .branch - name of branch this revision is on
35 .branches - revision tuple of branches starting at this revision
34 .branches - revision tuple of branches starting at this revision
36 .comment - commit message
35 .comment - commit message
37 .commitid - CVS commitid or None
36 .commitid - CVS commitid or None
38 .date - the commit date as a (time, tz) tuple
37 .date - the commit date as a (time, tz) tuple
39 .dead - true if file revision is dead
38 .dead - true if file revision is dead
40 .file - Name of file
39 .file - Name of file
41 .lines - a tuple (+lines, -lines) or None
40 .lines - a tuple (+lines, -lines) or None
42 .parent - Previous revision of this entry
41 .parent - Previous revision of this entry
43 .rcs - name of file as returned from CVS
42 .rcs - name of file as returned from CVS
44 .revision - revision number as tuple
43 .revision - revision number as tuple
45 .tags - list of tags on the file
44 .tags - list of tags on the file
46 .synthetic - is this a synthetic "file ... added on ..." revision?
45 .synthetic - is this a synthetic "file ... added on ..." revision?
47 .mergepoint - the branch that has been merged from (if present in
46 .mergepoint - the branch that has been merged from (if present in
48 rlog output) or None
47 rlog output) or None
49 .branchpoints - the branches that start at the current entry or empty
48 .branchpoints - the branches that start at the current entry or empty
50 """
49 """
51
50
52 def __init__(self, **entries):
51 def __init__(self, **entries):
53 self.synthetic = False
52 self.synthetic = False
54 self.__dict__.update(entries)
53 self.__dict__.update(entries)
55
54
56 def __repr__(self):
55 def __repr__(self):
57 items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__))
56 items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__))
58 return "%s(%s)" % (type(self).__name__, ", ".join(items))
57 return "%s(%s)" % (type(self).__name__, ", ".join(items))
59
58
60
59
61 class logerror(Exception):
60 class logerror(Exception):
62 pass
61 pass
63
62
64
63
65 def getrepopath(cvspath):
64 def getrepopath(cvspath):
66 """Return the repository path from a CVS path.
65 """Return the repository path from a CVS path.
67
66
68 >>> getrepopath(b'/foo/bar')
67 >>> getrepopath(b'/foo/bar')
69 '/foo/bar'
68 '/foo/bar'
70 >>> getrepopath(b'c:/foo/bar')
69 >>> getrepopath(b'c:/foo/bar')
71 '/foo/bar'
70 '/foo/bar'
72 >>> getrepopath(b':pserver:10/foo/bar')
71 >>> getrepopath(b':pserver:10/foo/bar')
73 '/foo/bar'
72 '/foo/bar'
74 >>> getrepopath(b':pserver:10c:/foo/bar')
73 >>> getrepopath(b':pserver:10c:/foo/bar')
75 '/foo/bar'
74 '/foo/bar'
76 >>> getrepopath(b':pserver:/foo/bar')
75 >>> getrepopath(b':pserver:/foo/bar')
77 '/foo/bar'
76 '/foo/bar'
78 >>> getrepopath(b':pserver:c:/foo/bar')
77 >>> getrepopath(b':pserver:c:/foo/bar')
79 '/foo/bar'
78 '/foo/bar'
80 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
79 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
81 '/foo/bar'
80 '/foo/bar'
82 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
81 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
83 '/foo/bar'
82 '/foo/bar'
84 >>> getrepopath(b'user@server/path/to/repository')
83 >>> getrepopath(b'user@server/path/to/repository')
85 '/path/to/repository'
84 '/path/to/repository'
86 """
85 """
87 # According to CVS manual, CVS paths are expressed like:
86 # According to CVS manual, CVS paths are expressed like:
88 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
87 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
89 #
88 #
90 # CVSpath is splitted into parts and then position of the first occurrence
89 # CVSpath is splitted into parts and then position of the first occurrence
91 # of the '/' char after the '@' is located. The solution is the rest of the
90 # of the '/' char after the '@' is located. The solution is the rest of the
92 # string after that '/' sign including it
91 # string after that '/' sign including it
93
92
94 parts = cvspath.split(b':')
93 parts = cvspath.split(b':')
95 atposition = parts[-1].find(b'@')
94 atposition = parts[-1].find(b'@')
96 start = 0
95 start = 0
97
96
98 if atposition != -1:
97 if atposition != -1:
99 start = atposition
98 start = atposition
100
99
101 repopath = parts[-1][parts[-1].find(b'/', start) :]
100 repopath = parts[-1][parts[-1].find(b'/', start) :]
102 return repopath
101 return repopath
103
102
104
103
105 def createlog(ui, directory=None, root=b"", rlog=True, cache=None):
104 def createlog(ui, directory=None, root=b"", rlog=True, cache=None):
106 '''Collect the CVS rlog'''
105 '''Collect the CVS rlog'''
107
106
108 # Because we store many duplicate commit log messages, reusing strings
107 # Because we store many duplicate commit log messages, reusing strings
109 # saves a lot of memory and pickle storage space.
108 # saves a lot of memory and pickle storage space.
110 _scache = {}
109 _scache = {}
111
110
112 def scache(s):
111 def scache(s):
113 """return a shared version of a string"""
112 """return a shared version of a string"""
114 return _scache.setdefault(s, s)
113 return _scache.setdefault(s, s)
115
114
116 ui.status(_(b'collecting CVS rlog\n'))
115 ui.status(_(b'collecting CVS rlog\n'))
117
116
118 log = [] # list of logentry objects containing the CVS state
117 log = [] # list of logentry objects containing the CVS state
119
118
120 # patterns to match in CVS (r)log output, by state of use
119 # patterns to match in CVS (r)log output, by state of use
121 re_00 = re.compile(b'RCS file: (.+)$')
120 re_00 = re.compile(b'RCS file: (.+)$')
122 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
121 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
123 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
122 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
124 re_03 = re.compile(
123 re_03 = re.compile(
125 b"(Cannot access.+CVSROOT)|(can't create temporary directory.+)$"
124 b"(Cannot access.+CVSROOT)|(can't create temporary directory.+)$"
126 )
125 )
127 re_10 = re.compile(b'Working file: (.+)$')
126 re_10 = re.compile(b'Working file: (.+)$')
128 re_20 = re.compile(b'symbolic names:')
127 re_20 = re.compile(b'symbolic names:')
129 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
128 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
130 re_31 = re.compile(b'----------------------------$')
129 re_31 = re.compile(b'----------------------------$')
131 re_32 = re.compile(
130 re_32 = re.compile(
132 b'======================================='
131 b'======================================='
133 b'======================================$'
132 b'======================================$'
134 )
133 )
135 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
134 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
136 re_60 = re.compile(
135 re_60 = re.compile(
137 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
136 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
138 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
137 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
139 br'(\s+commitid:\s+([^;]+);)?'
138 br'(\s+commitid:\s+([^;]+);)?'
140 br'(.*mergepoint:\s+([^;]+);)?'
139 br'(.*mergepoint:\s+([^;]+);)?'
141 )
140 )
142 re_70 = re.compile(b'branches: (.+);$')
141 re_70 = re.compile(b'branches: (.+);$')
143
142
144 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
143 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
145
144
146 prefix = b'' # leading path to strip of what we get from CVS
145 prefix = b'' # leading path to strip of what we get from CVS
147
146
148 if directory is None:
147 if directory is None:
149 # Current working directory
148 # Current working directory
150
149
151 # Get the real directory in the repository
150 # Get the real directory in the repository
152 try:
151 try:
153 with open(os.path.join(b'CVS', b'Repository'), b'rb') as f:
152 with open(os.path.join(b'CVS', b'Repository'), b'rb') as f:
154 prefix = f.read().strip()
153 prefix = f.read().strip()
155 directory = prefix
154 directory = prefix
156 if prefix == b".":
155 if prefix == b".":
157 prefix = b""
156 prefix = b""
158 except IOError:
157 except IOError:
159 raise logerror(_(b'not a CVS sandbox'))
158 raise logerror(_(b'not a CVS sandbox'))
160
159
161 if prefix and not prefix.endswith(pycompat.ossep):
160 if prefix and not prefix.endswith(pycompat.ossep):
162 prefix += pycompat.ossep
161 prefix += pycompat.ossep
163
162
164 # Use the Root file in the sandbox, if it exists
163 # Use the Root file in the sandbox, if it exists
165 try:
164 try:
166 root = open(os.path.join(b'CVS', b'Root'), b'rb').read().strip()
165 root = open(os.path.join(b'CVS', b'Root'), b'rb').read().strip()
167 except IOError:
166 except IOError:
168 pass
167 pass
169
168
170 if not root:
169 if not root:
171 root = encoding.environ.get(b'CVSROOT', b'')
170 root = encoding.environ.get(b'CVSROOT', b'')
172
171
173 # read log cache if one exists
172 # read log cache if one exists
174 oldlog = []
173 oldlog = []
175 date = None
174 date = None
176
175
177 if cache:
176 if cache:
178 cachedir = os.path.expanduser(b'~/.hg.cvsps')
177 cachedir = os.path.expanduser(b'~/.hg.cvsps')
179 if not os.path.exists(cachedir):
178 if not os.path.exists(cachedir):
180 os.mkdir(cachedir)
179 os.mkdir(cachedir)
181
180
182 # The cvsps cache pickle needs a uniquified name, based on the
181 # The cvsps cache pickle needs a uniquified name, based on the
183 # repository location. The address may have all sort of nasties
182 # repository location. The address may have all sort of nasties
184 # in it, slashes, colons and such. So here we take just the
183 # in it, slashes, colons and such. So here we take just the
185 # alphanumeric characters, concatenated in a way that does not
184 # alphanumeric characters, concatenated in a way that does not
186 # mix up the various components, so that
185 # mix up the various components, so that
187 # :pserver:user@server:/path
186 # :pserver:user@server:/path
188 # and
187 # and
189 # /pserver/user/server/path
188 # /pserver/user/server/path
190 # are mapped to different cache file names.
189 # are mapped to different cache file names.
191 cachefile = root.split(b":") + [directory, b"cache"]
190 cachefile = root.split(b":") + [directory, b"cache"]
192 cachefile = [b'-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
191 cachefile = [b'-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
193 cachefile = os.path.join(
192 cachefile = os.path.join(
194 cachedir, b'.'.join([s for s in cachefile if s])
193 cachedir, b'.'.join([s for s in cachefile if s])
195 )
194 )
196
195
197 if cache == b'update':
196 if cache == b'update':
198 try:
197 try:
199 ui.note(_(b'reading cvs log cache %s\n') % cachefile)
198 ui.note(_(b'reading cvs log cache %s\n') % cachefile)
200 oldlog = pickle.load(open(cachefile, b'rb'))
199 oldlog = pickle.load(open(cachefile, b'rb'))
201 for e in oldlog:
200 for e in oldlog:
202 if not (
201 if not (
203 util.safehasattr(e, b'branchpoints')
202 util.safehasattr(e, b'branchpoints')
204 and util.safehasattr(e, b'commitid')
203 and util.safehasattr(e, b'commitid')
205 and util.safehasattr(e, b'mergepoint')
204 and util.safehasattr(e, b'mergepoint')
206 ):
205 ):
207 ui.status(_(b'ignoring old cache\n'))
206 ui.status(_(b'ignoring old cache\n'))
208 oldlog = []
207 oldlog = []
209 break
208 break
210
209
211 ui.note(_(b'cache has %d log entries\n') % len(oldlog))
210 ui.note(_(b'cache has %d log entries\n') % len(oldlog))
212 except Exception as e:
211 except Exception as e:
213 ui.note(_(b'error reading cache: %r\n') % e)
212 ui.note(_(b'error reading cache: %r\n') % e)
214
213
215 if oldlog:
214 if oldlog:
216 date = oldlog[-1].date # last commit date as a (time,tz) tuple
215 date = oldlog[-1].date # last commit date as a (time,tz) tuple
217 date = dateutil.datestr(date, b'%Y/%m/%d %H:%M:%S %1%2')
216 date = dateutil.datestr(date, b'%Y/%m/%d %H:%M:%S %1%2')
218
217
219 # build the CVS commandline
218 # build the CVS commandline
220 cmd = [b'cvs', b'-q']
219 cmd = [b'cvs', b'-q']
221 if root:
220 if root:
222 cmd.append(b'-d%s' % root)
221 cmd.append(b'-d%s' % root)
223 p = util.normpath(getrepopath(root))
222 p = util.normpath(getrepopath(root))
224 if not p.endswith(b'/'):
223 if not p.endswith(b'/'):
225 p += b'/'
224 p += b'/'
226 if prefix:
225 if prefix:
227 # looks like normpath replaces "" by "."
226 # looks like normpath replaces "" by "."
228 prefix = p + util.normpath(prefix)
227 prefix = p + util.normpath(prefix)
229 else:
228 else:
230 prefix = p
229 prefix = p
231 cmd.append([b'log', b'rlog'][rlog])
230 cmd.append([b'log', b'rlog'][rlog])
232 if date:
231 if date:
233 # no space between option and date string
232 # no space between option and date string
234 cmd.append(b'-d>%s' % date)
233 cmd.append(b'-d>%s' % date)
235 cmd.append(directory)
234 cmd.append(directory)
236
235
237 # state machine begins here
236 # state machine begins here
238 tags = {} # dictionary of revisions on current file with their tags
237 tags = {} # dictionary of revisions on current file with their tags
239 branchmap = {} # mapping between branch names and revision numbers
238 branchmap = {} # mapping between branch names and revision numbers
240 rcsmap = {}
239 rcsmap = {}
241 state = 0
240 state = 0
242 store = False # set when a new record can be appended
241 store = False # set when a new record can be appended
243
242
244 cmd = [procutil.shellquote(arg) for arg in cmd]
243 cmd = [procutil.shellquote(arg) for arg in cmd]
245 ui.note(_(b"running %s\n") % (b' '.join(cmd)))
244 ui.note(_(b"running %s\n") % (b' '.join(cmd)))
246 ui.debug(b"prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
245 ui.debug(b"prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
247
246
248 pfp = procutil.popen(b' '.join(cmd), b'rb')
247 pfp = procutil.popen(b' '.join(cmd), b'rb')
249 peek = util.fromnativeeol(pfp.readline())
248 peek = util.fromnativeeol(pfp.readline())
250 while True:
249 while True:
251 line = peek
250 line = peek
252 if line == b'':
251 if line == b'':
253 break
252 break
254 peek = util.fromnativeeol(pfp.readline())
253 peek = util.fromnativeeol(pfp.readline())
255 if line.endswith(b'\n'):
254 if line.endswith(b'\n'):
256 line = line[:-1]
255 line = line[:-1]
257 # ui.debug('state=%d line=%r\n' % (state, line))
256 # ui.debug('state=%d line=%r\n' % (state, line))
258
257
259 if state == 0:
258 if state == 0:
260 # initial state, consume input until we see 'RCS file'
259 # initial state, consume input until we see 'RCS file'
261 match = re_00.match(line)
260 match = re_00.match(line)
262 if match:
261 if match:
263 rcs = match.group(1)
262 rcs = match.group(1)
264 tags = {}
263 tags = {}
265 if rlog:
264 if rlog:
266 filename = util.normpath(rcs[:-2])
265 filename = util.normpath(rcs[:-2])
267 if filename.startswith(prefix):
266 if filename.startswith(prefix):
268 filename = filename[len(prefix) :]
267 filename = filename[len(prefix) :]
269 if filename.startswith(b'/'):
268 if filename.startswith(b'/'):
270 filename = filename[1:]
269 filename = filename[1:]
271 if filename.startswith(b'Attic/'):
270 if filename.startswith(b'Attic/'):
272 filename = filename[6:]
271 filename = filename[6:]
273 else:
272 else:
274 filename = filename.replace(b'/Attic/', b'/')
273 filename = filename.replace(b'/Attic/', b'/')
275 state = 2
274 state = 2
276 continue
275 continue
277 state = 1
276 state = 1
278 continue
277 continue
279 match = re_01.match(line)
278 match = re_01.match(line)
280 if match:
279 if match:
281 raise logerror(match.group(1))
280 raise logerror(match.group(1))
282 match = re_02.match(line)
281 match = re_02.match(line)
283 if match:
282 if match:
284 raise logerror(match.group(2))
283 raise logerror(match.group(2))
285 if re_03.match(line):
284 if re_03.match(line):
286 raise logerror(line)
285 raise logerror(line)
287
286
288 elif state == 1:
287 elif state == 1:
289 # expect 'Working file' (only when using log instead of rlog)
288 # expect 'Working file' (only when using log instead of rlog)
290 match = re_10.match(line)
289 match = re_10.match(line)
291 assert match, _(b'RCS file must be followed by working file')
290 assert match, _(b'RCS file must be followed by working file')
292 filename = util.normpath(match.group(1))
291 filename = util.normpath(match.group(1))
293 state = 2
292 state = 2
294
293
295 elif state == 2:
294 elif state == 2:
296 # expect 'symbolic names'
295 # expect 'symbolic names'
297 if re_20.match(line):
296 if re_20.match(line):
298 branchmap = {}
297 branchmap = {}
299 state = 3
298 state = 3
300
299
301 elif state == 3:
300 elif state == 3:
302 # read the symbolic names and store as tags
301 # read the symbolic names and store as tags
303 match = re_30.match(line)
302 match = re_30.match(line)
304 if match:
303 if match:
305 rev = [int(x) for x in match.group(2).split(b'.')]
304 rev = [int(x) for x in match.group(2).split(b'.')]
306
305
307 # Convert magic branch number to an odd-numbered one
306 # Convert magic branch number to an odd-numbered one
308 revn = len(rev)
307 revn = len(rev)
309 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
308 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
310 rev = rev[:-2] + rev[-1:]
309 rev = rev[:-2] + rev[-1:]
311 rev = tuple(rev)
310 rev = tuple(rev)
312
311
313 if rev not in tags:
312 if rev not in tags:
314 tags[rev] = []
313 tags[rev] = []
315 tags[rev].append(match.group(1))
314 tags[rev].append(match.group(1))
316 branchmap[match.group(1)] = match.group(2)
315 branchmap[match.group(1)] = match.group(2)
317
316
318 elif re_31.match(line):
317 elif re_31.match(line):
319 state = 5
318 state = 5
320 elif re_32.match(line):
319 elif re_32.match(line):
321 state = 0
320 state = 0
322
321
323 elif state == 4:
322 elif state == 4:
324 # expecting '------' separator before first revision
323 # expecting '------' separator before first revision
325 if re_31.match(line):
324 if re_31.match(line):
326 state = 5
325 state = 5
327 else:
326 else:
328 assert not re_32.match(line), _(
327 assert not re_32.match(line), _(
329 b'must have at least some revisions'
328 b'must have at least some revisions'
330 )
329 )
331
330
332 elif state == 5:
331 elif state == 5:
333 # expecting revision number and possibly (ignored) lock indication
332 # expecting revision number and possibly (ignored) lock indication
334 # we create the logentry here from values stored in states 0 to 4,
333 # we create the logentry here from values stored in states 0 to 4,
335 # as this state is re-entered for subsequent revisions of a file.
334 # as this state is re-entered for subsequent revisions of a file.
336 match = re_50.match(line)
335 match = re_50.match(line)
337 assert match, _(b'expected revision number')
336 assert match, _(b'expected revision number')
338 e = logentry(
337 e = logentry(
339 rcs=scache(rcs),
338 rcs=scache(rcs),
340 file=scache(filename),
339 file=scache(filename),
341 revision=tuple([int(x) for x in match.group(1).split(b'.')]),
340 revision=tuple([int(x) for x in match.group(1).split(b'.')]),
342 branches=[],
341 branches=[],
343 parent=None,
342 parent=None,
344 commitid=None,
343 commitid=None,
345 mergepoint=None,
344 mergepoint=None,
346 branchpoints=set(),
345 branchpoints=set(),
347 )
346 )
348
347
349 state = 6
348 state = 6
350
349
351 elif state == 6:
350 elif state == 6:
352 # expecting date, author, state, lines changed
351 # expecting date, author, state, lines changed
353 match = re_60.match(line)
352 match = re_60.match(line)
354 assert match, _(b'revision must be followed by date line')
353 assert match, _(b'revision must be followed by date line')
355 d = match.group(1)
354 d = match.group(1)
356 if d[2] == b'/':
355 if d[2] == b'/':
357 # Y2K
356 # Y2K
358 d = b'19' + d
357 d = b'19' + d
359
358
360 if len(d.split()) != 3:
359 if len(d.split()) != 3:
361 # cvs log dates always in GMT
360 # cvs log dates always in GMT
362 d = d + b' UTC'
361 d = d + b' UTC'
363 e.date = dateutil.parsedate(
362 e.date = dateutil.parsedate(
364 d,
363 d,
365 [
364 [
366 b'%y/%m/%d %H:%M:%S',
365 b'%y/%m/%d %H:%M:%S',
367 b'%Y/%m/%d %H:%M:%S',
366 b'%Y/%m/%d %H:%M:%S',
368 b'%Y-%m-%d %H:%M:%S',
367 b'%Y-%m-%d %H:%M:%S',
369 ],
368 ],
370 )
369 )
371 e.author = scache(match.group(2))
370 e.author = scache(match.group(2))
372 e.dead = match.group(3).lower() == b'dead'
371 e.dead = match.group(3).lower() == b'dead'
373
372
374 if match.group(5):
373 if match.group(5):
375 if match.group(6):
374 if match.group(6):
376 e.lines = (int(match.group(5)), int(match.group(6)))
375 e.lines = (int(match.group(5)), int(match.group(6)))
377 else:
376 else:
378 e.lines = (int(match.group(5)), 0)
377 e.lines = (int(match.group(5)), 0)
379 elif match.group(6):
378 elif match.group(6):
380 e.lines = (0, int(match.group(6)))
379 e.lines = (0, int(match.group(6)))
381 else:
380 else:
382 e.lines = None
381 e.lines = None
383
382
384 if match.group(7): # cvs 1.12 commitid
383 if match.group(7): # cvs 1.12 commitid
385 e.commitid = match.group(8)
384 e.commitid = match.group(8)
386
385
387 if match.group(9): # cvsnt mergepoint
386 if match.group(9): # cvsnt mergepoint
388 myrev = match.group(10).split(b'.')
387 myrev = match.group(10).split(b'.')
389 if len(myrev) == 2: # head
388 if len(myrev) == 2: # head
390 e.mergepoint = b'HEAD'
389 e.mergepoint = b'HEAD'
391 else:
390 else:
392 myrev = b'.'.join(myrev[:-2] + [b'0', myrev[-2]])
391 myrev = b'.'.join(myrev[:-2] + [b'0', myrev[-2]])
393 branches = [b for b in branchmap if branchmap[b] == myrev]
392 branches = [b for b in branchmap if branchmap[b] == myrev]
394 assert len(branches) == 1, (
393 assert len(branches) == 1, (
395 b'unknown branch: %s' % e.mergepoint
394 b'unknown branch: %s' % e.mergepoint
396 )
395 )
397 e.mergepoint = branches[0]
396 e.mergepoint = branches[0]
398
397
399 e.comment = []
398 e.comment = []
400 state = 7
399 state = 7
401
400
402 elif state == 7:
401 elif state == 7:
403 # read the revision numbers of branches that start at this revision
402 # read the revision numbers of branches that start at this revision
404 # or store the commit log message otherwise
403 # or store the commit log message otherwise
405 m = re_70.match(line)
404 m = re_70.match(line)
406 if m:
405 if m:
407 e.branches = [
406 e.branches = [
408 tuple([int(y) for y in x.strip().split(b'.')])
407 tuple([int(y) for y in x.strip().split(b'.')])
409 for x in m.group(1).split(b';')
408 for x in m.group(1).split(b';')
410 ]
409 ]
411 state = 8
410 state = 8
412 elif re_31.match(line) and re_50.match(peek):
411 elif re_31.match(line) and re_50.match(peek):
413 state = 5
412 state = 5
414 store = True
413 store = True
415 elif re_32.match(line):
414 elif re_32.match(line):
416 state = 0
415 state = 0
417 store = True
416 store = True
418 else:
417 else:
419 e.comment.append(line)
418 e.comment.append(line)
420
419
421 elif state == 8:
420 elif state == 8:
422 # store commit log message
421 # store commit log message
423 if re_31.match(line):
422 if re_31.match(line):
424 cpeek = peek
423 cpeek = peek
425 if cpeek.endswith(b'\n'):
424 if cpeek.endswith(b'\n'):
426 cpeek = cpeek[:-1]
425 cpeek = cpeek[:-1]
427 if re_50.match(cpeek):
426 if re_50.match(cpeek):
428 state = 5
427 state = 5
429 store = True
428 store = True
430 else:
429 else:
431 e.comment.append(line)
430 e.comment.append(line)
432 elif re_32.match(line):
431 elif re_32.match(line):
433 state = 0
432 state = 0
434 store = True
433 store = True
435 else:
434 else:
436 e.comment.append(line)
435 e.comment.append(line)
437
436
438 # When a file is added on a branch B1, CVS creates a synthetic
437 # When a file is added on a branch B1, CVS creates a synthetic
439 # dead trunk revision 1.1 so that the branch has a root.
438 # dead trunk revision 1.1 so that the branch has a root.
440 # Likewise, if you merge such a file to a later branch B2 (one
439 # Likewise, if you merge such a file to a later branch B2 (one
441 # that already existed when the file was added on B1), CVS
440 # that already existed when the file was added on B1), CVS
442 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
441 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
443 # these revisions now, but mark them synthetic so
442 # these revisions now, but mark them synthetic so
444 # createchangeset() can take care of them.
443 # createchangeset() can take care of them.
445 if (
444 if (
446 store
445 store
447 and e.dead
446 and e.dead
448 and e.revision[-1] == 1
447 and e.revision[-1] == 1
449 and len(e.comment) == 1 # 1.1 or 1.1.x.1
448 and len(e.comment) == 1 # 1.1 or 1.1.x.1
450 and file_added_re.match(e.comment[0])
449 and file_added_re.match(e.comment[0])
451 ):
450 ):
452 ui.debug(
451 ui.debug(
453 b'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
452 b'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
454 )
453 )
455 e.synthetic = True
454 e.synthetic = True
456
455
457 if store:
456 if store:
458 # clean up the results and save in the log.
457 # clean up the results and save in the log.
459 store = False
458 store = False
460 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
459 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
461 e.comment = scache(b'\n'.join(e.comment))
460 e.comment = scache(b'\n'.join(e.comment))
462
461
463 revn = len(e.revision)
462 revn = len(e.revision)
464 if revn > 3 and (revn % 2) == 0:
463 if revn > 3 and (revn % 2) == 0:
465 e.branch = tags.get(e.revision[:-1], [None])[0]
464 e.branch = tags.get(e.revision[:-1], [None])[0]
466 else:
465 else:
467 e.branch = None
466 e.branch = None
468
467
469 # find the branches starting from this revision
468 # find the branches starting from this revision
470 branchpoints = set()
469 branchpoints = set()
471 for branch, revision in pycompat.iteritems(branchmap):
470 for branch, revision in pycompat.iteritems(branchmap):
472 revparts = tuple([int(i) for i in revision.split(b'.')])
471 revparts = tuple([int(i) for i in revision.split(b'.')])
473 if len(revparts) < 2: # bad tags
472 if len(revparts) < 2: # bad tags
474 continue
473 continue
475 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
474 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
476 # normal branch
475 # normal branch
477 if revparts[:-2] == e.revision:
476 if revparts[:-2] == e.revision:
478 branchpoints.add(branch)
477 branchpoints.add(branch)
479 elif revparts == (1, 1, 1): # vendor branch
478 elif revparts == (1, 1, 1): # vendor branch
480 if revparts in e.branches:
479 if revparts in e.branches:
481 branchpoints.add(branch)
480 branchpoints.add(branch)
482 e.branchpoints = branchpoints
481 e.branchpoints = branchpoints
483
482
484 log.append(e)
483 log.append(e)
485
484
486 rcsmap[e.rcs.replace(b'/Attic/', b'/')] = e.rcs
485 rcsmap[e.rcs.replace(b'/Attic/', b'/')] = e.rcs
487
486
488 if len(log) % 100 == 0:
487 if len(log) % 100 == 0:
489 ui.status(
488 ui.status(
490 stringutil.ellipsis(b'%d %s' % (len(log), e.file), 80)
489 stringutil.ellipsis(b'%d %s' % (len(log), e.file), 80)
491 + b'\n'
490 + b'\n'
492 )
491 )
493
492
494 log.sort(key=lambda x: (x.rcs, x.revision))
493 log.sort(key=lambda x: (x.rcs, x.revision))
495
494
496 # find parent revisions of individual files
495 # find parent revisions of individual files
497 versions = {}
496 versions = {}
498 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
497 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
499 rcs = e.rcs.replace(b'/Attic/', b'/')
498 rcs = e.rcs.replace(b'/Attic/', b'/')
500 if rcs in rcsmap:
499 if rcs in rcsmap:
501 e.rcs = rcsmap[rcs]
500 e.rcs = rcsmap[rcs]
502 branch = e.revision[:-1]
501 branch = e.revision[:-1]
503 versions[(e.rcs, branch)] = e.revision
502 versions[(e.rcs, branch)] = e.revision
504
503
505 for e in log:
504 for e in log:
506 branch = e.revision[:-1]
505 branch = e.revision[:-1]
507 p = versions.get((e.rcs, branch), None)
506 p = versions.get((e.rcs, branch), None)
508 if p is None:
507 if p is None:
509 p = e.revision[:-2]
508 p = e.revision[:-2]
510 e.parent = p
509 e.parent = p
511 versions[(e.rcs, branch)] = e.revision
510 versions[(e.rcs, branch)] = e.revision
512
511
513 # update the log cache
512 # update the log cache
514 if cache:
513 if cache:
515 if log:
514 if log:
516 # join up the old and new logs
515 # join up the old and new logs
517 log.sort(key=lambda x: x.date)
516 log.sort(key=lambda x: x.date)
518
517
519 if oldlog and oldlog[-1].date >= log[0].date:
518 if oldlog and oldlog[-1].date >= log[0].date:
520 raise logerror(
519 raise logerror(
521 _(
520 _(
522 b'log cache overlaps with new log entries,'
521 b'log cache overlaps with new log entries,'
523 b' re-run without cache.'
522 b' re-run without cache.'
524 )
523 )
525 )
524 )
526
525
527 log = oldlog + log
526 log = oldlog + log
528
527
529 # write the new cachefile
528 # write the new cachefile
530 ui.note(_(b'writing cvs log cache %s\n') % cachefile)
529 ui.note(_(b'writing cvs log cache %s\n') % cachefile)
531 pickle.dump(log, open(cachefile, b'wb'))
530 pickle.dump(log, open(cachefile, b'wb'))
532 else:
531 else:
533 log = oldlog
532 log = oldlog
534
533
535 ui.status(_(b'%d log entries\n') % len(log))
534 ui.status(_(b'%d log entries\n') % len(log))
536
535
537 encodings = ui.configlist(b'convert', b'cvsps.logencoding')
536 encodings = ui.configlist(b'convert', b'cvsps.logencoding')
538 if encodings:
537 if encodings:
539
538
540 def revstr(r):
539 def revstr(r):
541 # this is needed, because logentry.revision is a tuple of "int"
540 # this is needed, because logentry.revision is a tuple of "int"
542 # (e.g. (1, 2) for "1.2")
541 # (e.g. (1, 2) for "1.2")
543 return b'.'.join(pycompat.maplist(pycompat.bytestr, r))
542 return b'.'.join(pycompat.maplist(pycompat.bytestr, r))
544
543
545 for entry in log:
544 for entry in log:
546 comment = entry.comment
545 comment = entry.comment
547 for e in encodings:
546 for e in encodings:
548 try:
547 try:
549 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
548 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
550 'utf-8'
549 'utf-8'
551 )
550 )
552 if ui.debugflag:
551 if ui.debugflag:
553 ui.debug(
552 ui.debug(
554 b"transcoding by %s: %s of %s\n"
553 b"transcoding by %s: %s of %s\n"
555 % (e, revstr(entry.revision), entry.file)
554 % (e, revstr(entry.revision), entry.file)
556 )
555 )
557 break
556 break
558 except UnicodeDecodeError:
557 except UnicodeDecodeError:
559 pass # try next encoding
558 pass # try next encoding
560 except LookupError as inst: # unknown encoding, maybe
559 except LookupError as inst: # unknown encoding, maybe
561 raise error.Abort(
560 raise error.Abort(
562 pycompat.bytestr(inst),
561 pycompat.bytestr(inst),
563 hint=_(
562 hint=_(
564 b'check convert.cvsps.logencoding configuration'
563 b'check convert.cvsps.logencoding configuration'
565 ),
564 ),
566 )
565 )
567 else:
566 else:
568 raise error.Abort(
567 raise error.Abort(
569 _(
568 _(
570 b"no encoding can transcode"
569 b"no encoding can transcode"
571 b" CVS log message for %s of %s"
570 b" CVS log message for %s of %s"
572 )
571 )
573 % (revstr(entry.revision), entry.file),
572 % (revstr(entry.revision), entry.file),
574 hint=_(b'check convert.cvsps.logencoding configuration'),
573 hint=_(b'check convert.cvsps.logencoding configuration'),
575 )
574 )
576
575
577 hook.hook(ui, None, b"cvslog", True, log=log)
576 hook.hook(ui, None, b"cvslog", True, log=log)
578
577
579 return log
578 return log
580
579
581
580
582 class changeset(object):
581 class changeset(object):
583 """Class changeset has the following attributes:
582 """Class changeset has the following attributes:
584 .id - integer identifying this changeset (list index)
583 .id - integer identifying this changeset (list index)
585 .author - author name as CVS knows it
584 .author - author name as CVS knows it
586 .branch - name of branch this changeset is on, or None
585 .branch - name of branch this changeset is on, or None
587 .comment - commit message
586 .comment - commit message
588 .commitid - CVS commitid or None
587 .commitid - CVS commitid or None
589 .date - the commit date as a (time,tz) tuple
588 .date - the commit date as a (time,tz) tuple
590 .entries - list of logentry objects in this changeset
589 .entries - list of logentry objects in this changeset
591 .parents - list of one or two parent changesets
590 .parents - list of one or two parent changesets
592 .tags - list of tags on this changeset
591 .tags - list of tags on this changeset
593 .synthetic - from synthetic revision "file ... added on branch ..."
592 .synthetic - from synthetic revision "file ... added on branch ..."
594 .mergepoint- the branch that has been merged from or None
593 .mergepoint- the branch that has been merged from or None
595 .branchpoints- the branches that start at the current entry or empty
594 .branchpoints- the branches that start at the current entry or empty
596 """
595 """
597
596
598 def __init__(self, **entries):
597 def __init__(self, **entries):
599 self.id = None
598 self.id = None
600 self.synthetic = False
599 self.synthetic = False
601 self.__dict__.update(entries)
600 self.__dict__.update(entries)
602
601
603 def __repr__(self):
602 def __repr__(self):
604 items = (
603 items = (
605 b"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
604 b"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
606 )
605 )
607 return b"%s(%s)" % (type(self).__name__, b", ".join(items))
606 return b"%s(%s)" % (type(self).__name__, b", ".join(items))
608
607
609
608
610 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
609 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
611 '''Convert log into changesets.'''
610 '''Convert log into changesets.'''
612
611
613 ui.status(_(b'creating changesets\n'))
612 ui.status(_(b'creating changesets\n'))
614
613
615 # try to order commitids by date
614 # try to order commitids by date
616 mindate = {}
615 mindate = {}
617 for e in log:
616 for e in log:
618 if e.commitid:
617 if e.commitid:
619 if e.commitid not in mindate:
618 if e.commitid not in mindate:
620 mindate[e.commitid] = e.date
619 mindate[e.commitid] = e.date
621 else:
620 else:
622 mindate[e.commitid] = min(e.date, mindate[e.commitid])
621 mindate[e.commitid] = min(e.date, mindate[e.commitid])
623
622
624 # Merge changesets
623 # Merge changesets
625 log.sort(
624 log.sort(
626 key=lambda x: (
625 key=lambda x: (
627 mindate.get(x.commitid, (-1, 0)),
626 mindate.get(x.commitid, (-1, 0)),
628 x.commitid or b'',
627 x.commitid or b'',
629 x.comment,
628 x.comment,
630 x.author,
629 x.author,
631 x.branch or b'',
630 x.branch or b'',
632 x.date,
631 x.date,
633 x.branchpoints,
632 x.branchpoints,
634 )
633 )
635 )
634 )
636
635
637 changesets = []
636 changesets = []
638 files = set()
637 files = set()
639 c = None
638 c = None
640 for i, e in enumerate(log):
639 for i, e in enumerate(log):
641
640
642 # Check if log entry belongs to the current changeset or not.
641 # Check if log entry belongs to the current changeset or not.
643
642
644 # Since CVS is file-centric, two different file revisions with
643 # Since CVS is file-centric, two different file revisions with
645 # different branchpoints should be treated as belonging to two
644 # different branchpoints should be treated as belonging to two
646 # different changesets (and the ordering is important and not
645 # different changesets (and the ordering is important and not
647 # honoured by cvsps at this point).
646 # honoured by cvsps at this point).
648 #
647 #
649 # Consider the following case:
648 # Consider the following case:
650 # foo 1.1 branchpoints: [MYBRANCH]
649 # foo 1.1 branchpoints: [MYBRANCH]
651 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
650 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
652 #
651 #
653 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
652 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
654 # later version of foo may be in MYBRANCH2, so foo should be the
653 # later version of foo may be in MYBRANCH2, so foo should be the
655 # first changeset and bar the next and MYBRANCH and MYBRANCH2
654 # first changeset and bar the next and MYBRANCH and MYBRANCH2
656 # should both start off of the bar changeset. No provisions are
655 # should both start off of the bar changeset. No provisions are
657 # made to ensure that this is, in fact, what happens.
656 # made to ensure that this is, in fact, what happens.
658 if not (
657 if not (
659 c
658 c
660 and e.branchpoints == c.branchpoints
659 and e.branchpoints == c.branchpoints
661 and ( # cvs commitids
660 and ( # cvs commitids
662 (e.commitid is not None and e.commitid == c.commitid)
661 (e.commitid is not None and e.commitid == c.commitid)
663 or ( # no commitids, use fuzzy commit detection
662 or ( # no commitids, use fuzzy commit detection
664 (e.commitid is None or c.commitid is None)
663 (e.commitid is None or c.commitid is None)
665 and e.comment == c.comment
664 and e.comment == c.comment
666 and e.author == c.author
665 and e.author == c.author
667 and e.branch == c.branch
666 and e.branch == c.branch
668 and (
667 and (
669 (c.date[0] + c.date[1])
668 (c.date[0] + c.date[1])
670 <= (e.date[0] + e.date[1])
669 <= (e.date[0] + e.date[1])
671 <= (c.date[0] + c.date[1]) + fuzz
670 <= (c.date[0] + c.date[1]) + fuzz
672 )
671 )
673 and e.file not in files
672 and e.file not in files
674 )
673 )
675 )
674 )
676 ):
675 ):
677 c = changeset(
676 c = changeset(
678 comment=e.comment,
677 comment=e.comment,
679 author=e.author,
678 author=e.author,
680 branch=e.branch,
679 branch=e.branch,
681 date=e.date,
680 date=e.date,
682 entries=[],
681 entries=[],
683 mergepoint=e.mergepoint,
682 mergepoint=e.mergepoint,
684 branchpoints=e.branchpoints,
683 branchpoints=e.branchpoints,
685 commitid=e.commitid,
684 commitid=e.commitid,
686 )
685 )
687 changesets.append(c)
686 changesets.append(c)
688
687
689 files = set()
688 files = set()
690 if len(changesets) % 100 == 0:
689 if len(changesets) % 100 == 0:
691 t = b'%d %s' % (len(changesets), repr(e.comment)[1:-1])
690 t = b'%d %s' % (len(changesets), repr(e.comment)[1:-1])
692 ui.status(stringutil.ellipsis(t, 80) + b'\n')
691 ui.status(stringutil.ellipsis(t, 80) + b'\n')
693
692
694 c.entries.append(e)
693 c.entries.append(e)
695 files.add(e.file)
694 files.add(e.file)
696 c.date = e.date # changeset date is date of latest commit in it
695 c.date = e.date # changeset date is date of latest commit in it
697
696
698 # Mark synthetic changesets
697 # Mark synthetic changesets
699
698
700 for c in changesets:
699 for c in changesets:
701 # Synthetic revisions always get their own changeset, because
700 # Synthetic revisions always get their own changeset, because
702 # the log message includes the filename. E.g. if you add file3
701 # the log message includes the filename. E.g. if you add file3
703 # and file4 on a branch, you get four log entries and three
702 # and file4 on a branch, you get four log entries and three
704 # changesets:
703 # changesets:
705 # "File file3 was added on branch ..." (synthetic, 1 entry)
704 # "File file3 was added on branch ..." (synthetic, 1 entry)
706 # "File file4 was added on branch ..." (synthetic, 1 entry)
705 # "File file4 was added on branch ..." (synthetic, 1 entry)
707 # "Add file3 and file4 to fix ..." (real, 2 entries)
706 # "Add file3 and file4 to fix ..." (real, 2 entries)
708 # Hence the check for 1 entry here.
707 # Hence the check for 1 entry here.
709 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
708 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
710
709
711 # Sort files in each changeset
710 # Sort files in each changeset
712
711
713 def entitycompare(l, r):
712 def entitycompare(l, r):
714 """Mimic cvsps sorting order"""
713 """Mimic cvsps sorting order"""
715 l = l.file.split(b'/')
714 l = l.file.split(b'/')
716 r = r.file.split(b'/')
715 r = r.file.split(b'/')
717 nl = len(l)
716 nl = len(l)
718 nr = len(r)
717 nr = len(r)
719 n = min(nl, nr)
718 n = min(nl, nr)
720 for i in range(n):
719 for i in range(n):
721 if i + 1 == nl and nl < nr:
720 if i + 1 == nl and nl < nr:
722 return -1
721 return -1
723 elif i + 1 == nr and nl > nr:
722 elif i + 1 == nr and nl > nr:
724 return +1
723 return +1
725 elif l[i] < r[i]:
724 elif l[i] < r[i]:
726 return -1
725 return -1
727 elif l[i] > r[i]:
726 elif l[i] > r[i]:
728 return +1
727 return +1
729 return 0
728 return 0
730
729
731 for c in changesets:
730 for c in changesets:
732 c.entries.sort(key=functools.cmp_to_key(entitycompare))
731 c.entries.sort(key=functools.cmp_to_key(entitycompare))
733
732
734 # Sort changesets by date
733 # Sort changesets by date
735
734
736 odd = set()
735 odd = set()
737
736
738 def cscmp(l, r):
737 def cscmp(l, r):
739 d = sum(l.date) - sum(r.date)
738 d = sum(l.date) - sum(r.date)
740 if d:
739 if d:
741 return d
740 return d
742
741
743 # detect vendor branches and initial commits on a branch
742 # detect vendor branches and initial commits on a branch
744 le = {}
743 le = {}
745 for e in l.entries:
744 for e in l.entries:
746 le[e.rcs] = e.revision
745 le[e.rcs] = e.revision
747 re = {}
746 re = {}
748 for e in r.entries:
747 for e in r.entries:
749 re[e.rcs] = e.revision
748 re[e.rcs] = e.revision
750
749
751 d = 0
750 d = 0
752 for e in l.entries:
751 for e in l.entries:
753 if re.get(e.rcs, None) == e.parent:
752 if re.get(e.rcs, None) == e.parent:
754 assert not d
753 assert not d
755 d = 1
754 d = 1
756 break
755 break
757
756
758 for e in r.entries:
757 for e in r.entries:
759 if le.get(e.rcs, None) == e.parent:
758 if le.get(e.rcs, None) == e.parent:
760 if d:
759 if d:
761 odd.add((l, r))
760 odd.add((l, r))
762 d = -1
761 d = -1
763 break
762 break
764 # By this point, the changesets are sufficiently compared that
763 # By this point, the changesets are sufficiently compared that
765 # we don't really care about ordering. However, this leaves
764 # we don't really care about ordering. However, this leaves
766 # some race conditions in the tests, so we compare on the
765 # some race conditions in the tests, so we compare on the
767 # number of files modified, the files contained in each
766 # number of files modified, the files contained in each
768 # changeset, and the branchpoints in the change to ensure test
767 # changeset, and the branchpoints in the change to ensure test
769 # output remains stable.
768 # output remains stable.
770
769
771 # recommended replacement for cmp from
770 # recommended replacement for cmp from
772 # https://docs.python.org/3.0/whatsnew/3.0.html
771 # https://docs.python.org/3.0/whatsnew/3.0.html
773 c = lambda x, y: (x > y) - (x < y)
772 c = lambda x, y: (x > y) - (x < y)
774 # Sort bigger changes first.
773 # Sort bigger changes first.
775 if not d:
774 if not d:
776 d = c(len(l.entries), len(r.entries))
775 d = c(len(l.entries), len(r.entries))
777 # Try sorting by filename in the change.
776 # Try sorting by filename in the change.
778 if not d:
777 if not d:
779 d = c([e.file for e in l.entries], [e.file for e in r.entries])
778 d = c([e.file for e in l.entries], [e.file for e in r.entries])
780 # Try and put changes without a branch point before ones with
779 # Try and put changes without a branch point before ones with
781 # a branch point.
780 # a branch point.
782 if not d:
781 if not d:
783 d = c(len(l.branchpoints), len(r.branchpoints))
782 d = c(len(l.branchpoints), len(r.branchpoints))
784 return d
783 return d
785
784
786 changesets.sort(key=functools.cmp_to_key(cscmp))
785 changesets.sort(key=functools.cmp_to_key(cscmp))
787
786
788 # Collect tags
787 # Collect tags
789
788
790 globaltags = {}
789 globaltags = {}
791 for c in changesets:
790 for c in changesets:
792 for e in c.entries:
791 for e in c.entries:
793 for tag in e.tags:
792 for tag in e.tags:
794 # remember which is the latest changeset to have this tag
793 # remember which is the latest changeset to have this tag
795 globaltags[tag] = c
794 globaltags[tag] = c
796
795
797 for c in changesets:
796 for c in changesets:
798 tags = set()
797 tags = set()
799 for e in c.entries:
798 for e in c.entries:
800 tags.update(e.tags)
799 tags.update(e.tags)
801 # remember tags only if this is the latest changeset to have it
800 # remember tags only if this is the latest changeset to have it
802 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
801 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
803
802
804 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
803 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
805 # by inserting dummy changesets with two parents, and handle
804 # by inserting dummy changesets with two parents, and handle
806 # {{mergefrombranch BRANCHNAME}} by setting two parents.
805 # {{mergefrombranch BRANCHNAME}} by setting two parents.
807
806
808 if mergeto is None:
807 if mergeto is None:
809 mergeto = br'{{mergetobranch ([-\w]+)}}'
808 mergeto = br'{{mergetobranch ([-\w]+)}}'
810 if mergeto:
809 if mergeto:
811 mergeto = re.compile(mergeto)
810 mergeto = re.compile(mergeto)
812
811
813 if mergefrom is None:
812 if mergefrom is None:
814 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
813 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
815 if mergefrom:
814 if mergefrom:
816 mergefrom = re.compile(mergefrom)
815 mergefrom = re.compile(mergefrom)
817
816
818 versions = {} # changeset index where we saw any particular file version
817 versions = {} # changeset index where we saw any particular file version
819 branches = {} # changeset index where we saw a branch
818 branches = {} # changeset index where we saw a branch
820 n = len(changesets)
819 n = len(changesets)
821 i = 0
820 i = 0
822 while i < n:
821 while i < n:
823 c = changesets[i]
822 c = changesets[i]
824
823
825 for f in c.entries:
824 for f in c.entries:
826 versions[(f.rcs, f.revision)] = i
825 versions[(f.rcs, f.revision)] = i
827
826
828 p = None
827 p = None
829 if c.branch in branches:
828 if c.branch in branches:
830 p = branches[c.branch]
829 p = branches[c.branch]
831 else:
830 else:
832 # first changeset on a new branch
831 # first changeset on a new branch
833 # the parent is a changeset with the branch in its
832 # the parent is a changeset with the branch in its
834 # branchpoints such that it is the latest possible
833 # branchpoints such that it is the latest possible
835 # commit without any intervening, unrelated commits.
834 # commit without any intervening, unrelated commits.
836
835
837 for candidate in pycompat.xrange(i):
836 for candidate in pycompat.xrange(i):
838 if c.branch not in changesets[candidate].branchpoints:
837 if c.branch not in changesets[candidate].branchpoints:
839 if p is not None:
838 if p is not None:
840 break
839 break
841 continue
840 continue
842 p = candidate
841 p = candidate
843
842
844 c.parents = []
843 c.parents = []
845 if p is not None:
844 if p is not None:
846 p = changesets[p]
845 p = changesets[p]
847
846
848 # Ensure no changeset has a synthetic changeset as a parent.
847 # Ensure no changeset has a synthetic changeset as a parent.
849 while p.synthetic:
848 while p.synthetic:
850 assert len(p.parents) <= 1, _(
849 assert len(p.parents) <= 1, _(
851 b'synthetic changeset cannot have multiple parents'
850 b'synthetic changeset cannot have multiple parents'
852 )
851 )
853 if p.parents:
852 if p.parents:
854 p = p.parents[0]
853 p = p.parents[0]
855 else:
854 else:
856 p = None
855 p = None
857 break
856 break
858
857
859 if p is not None:
858 if p is not None:
860 c.parents.append(p)
859 c.parents.append(p)
861
860
862 if c.mergepoint:
861 if c.mergepoint:
863 if c.mergepoint == b'HEAD':
862 if c.mergepoint == b'HEAD':
864 c.mergepoint = None
863 c.mergepoint = None
865 c.parents.append(changesets[branches[c.mergepoint]])
864 c.parents.append(changesets[branches[c.mergepoint]])
866
865
867 if mergefrom:
866 if mergefrom:
868 m = mergefrom.search(c.comment)
867 m = mergefrom.search(c.comment)
869 if m:
868 if m:
870 m = m.group(1)
869 m = m.group(1)
871 if m == b'HEAD':
870 if m == b'HEAD':
872 m = None
871 m = None
873 try:
872 try:
874 candidate = changesets[branches[m]]
873 candidate = changesets[branches[m]]
875 except KeyError:
874 except KeyError:
876 ui.warn(
875 ui.warn(
877 _(
876 _(
878 b"warning: CVS commit message references "
877 b"warning: CVS commit message references "
879 b"non-existent branch %r:\n%s\n"
878 b"non-existent branch %r:\n%s\n"
880 )
879 )
881 % (pycompat.bytestr(m), c.comment)
880 % (pycompat.bytestr(m), c.comment)
882 )
881 )
883 if m in branches and c.branch != m and not candidate.synthetic:
882 if m in branches and c.branch != m and not candidate.synthetic:
884 c.parents.append(candidate)
883 c.parents.append(candidate)
885
884
886 if mergeto:
885 if mergeto:
887 m = mergeto.search(c.comment)
886 m = mergeto.search(c.comment)
888 if m:
887 if m:
889 if m.groups():
888 if m.groups():
890 m = m.group(1)
889 m = m.group(1)
891 if m == b'HEAD':
890 if m == b'HEAD':
892 m = None
891 m = None
893 else:
892 else:
894 m = None # if no group found then merge to HEAD
893 m = None # if no group found then merge to HEAD
895 if m in branches and c.branch != m:
894 if m in branches and c.branch != m:
896 # insert empty changeset for merge
895 # insert empty changeset for merge
897 cc = changeset(
896 cc = changeset(
898 author=c.author,
897 author=c.author,
899 branch=m,
898 branch=m,
900 date=c.date,
899 date=c.date,
901 comment=b'convert-repo: CVS merge from branch %s'
900 comment=b'convert-repo: CVS merge from branch %s'
902 % c.branch,
901 % c.branch,
903 entries=[],
902 entries=[],
904 tags=[],
903 tags=[],
905 parents=[changesets[branches[m]], c],
904 parents=[changesets[branches[m]], c],
906 )
905 )
907 changesets.insert(i + 1, cc)
906 changesets.insert(i + 1, cc)
908 branches[m] = i + 1
907 branches[m] = i + 1
909
908
910 # adjust our loop counters now we have inserted a new entry
909 # adjust our loop counters now we have inserted a new entry
911 n += 1
910 n += 1
912 i += 2
911 i += 2
913 continue
912 continue
914
913
915 branches[c.branch] = i
914 branches[c.branch] = i
916 i += 1
915 i += 1
917
916
918 # Drop synthetic changesets (safe now that we have ensured no other
917 # Drop synthetic changesets (safe now that we have ensured no other
919 # changesets can have them as parents).
918 # changesets can have them as parents).
920 i = 0
919 i = 0
921 while i < len(changesets):
920 while i < len(changesets):
922 if changesets[i].synthetic:
921 if changesets[i].synthetic:
923 del changesets[i]
922 del changesets[i]
924 else:
923 else:
925 i += 1
924 i += 1
926
925
927 # Number changesets
926 # Number changesets
928
927
929 for i, c in enumerate(changesets):
928 for i, c in enumerate(changesets):
930 c.id = i + 1
929 c.id = i + 1
931
930
932 if odd:
931 if odd:
933 for l, r in odd:
932 for l, r in odd:
934 if l.id is not None and r.id is not None:
933 if l.id is not None and r.id is not None:
935 ui.warn(
934 ui.warn(
936 _(b'changeset %d is both before and after %d\n')
935 _(b'changeset %d is both before and after %d\n')
937 % (l.id, r.id)
936 % (l.id, r.id)
938 )
937 )
939
938
940 ui.status(_(b'%d changeset entries\n') % len(changesets))
939 ui.status(_(b'%d changeset entries\n') % len(changesets))
941
940
942 hook.hook(ui, None, b"cvschangesets", True, changesets=changesets)
941 hook.hook(ui, None, b"cvschangesets", True, changesets=changesets)
943
942
944 return changesets
943 return changesets
945
944
946
945
947 def debugcvsps(ui, *args, **opts):
946 def debugcvsps(ui, *args, **opts):
948 """Read CVS rlog for current directory or named path in
947 """Read CVS rlog for current directory or named path in
949 repository, and convert the log to changesets based on matching
948 repository, and convert the log to changesets based on matching
950 commit log entries and dates.
949 commit log entries and dates.
951 """
950 """
952 opts = pycompat.byteskwargs(opts)
951 opts = pycompat.byteskwargs(opts)
953 if opts[b"new_cache"]:
952 if opts[b"new_cache"]:
954 cache = b"write"
953 cache = b"write"
955 elif opts[b"update_cache"]:
954 elif opts[b"update_cache"]:
956 cache = b"update"
955 cache = b"update"
957 else:
956 else:
958 cache = None
957 cache = None
959
958
960 revisions = opts[b"revisions"]
959 revisions = opts[b"revisions"]
961
960
962 try:
961 try:
963 if args:
962 if args:
964 log = []
963 log = []
965 for d in args:
964 for d in args:
966 log += createlog(ui, d, root=opts[b"root"], cache=cache)
965 log += createlog(ui, d, root=opts[b"root"], cache=cache)
967 else:
966 else:
968 log = createlog(ui, root=opts[b"root"], cache=cache)
967 log = createlog(ui, root=opts[b"root"], cache=cache)
969 except logerror as e:
968 except logerror as e:
970 ui.write(b"%r\n" % e)
969 ui.write(b"%r\n" % e)
971 return
970 return
972
971
973 changesets = createchangeset(ui, log, opts[b"fuzz"])
972 changesets = createchangeset(ui, log, opts[b"fuzz"])
974 del log
973 del log
975
974
976 # Print changesets (optionally filtered)
975 # Print changesets (optionally filtered)
977
976
978 off = len(revisions)
977 off = len(revisions)
979 branches = {} # latest version number in each branch
978 branches = {} # latest version number in each branch
980 ancestors = {} # parent branch
979 ancestors = {} # parent branch
981 for cs in changesets:
980 for cs in changesets:
982
981
983 if opts[b"ancestors"]:
982 if opts[b"ancestors"]:
984 if cs.branch not in branches and cs.parents and cs.parents[0].id:
983 if cs.branch not in branches and cs.parents and cs.parents[0].id:
985 ancestors[cs.branch] = (
984 ancestors[cs.branch] = (
986 changesets[cs.parents[0].id - 1].branch,
985 changesets[cs.parents[0].id - 1].branch,
987 cs.parents[0].id,
986 cs.parents[0].id,
988 )
987 )
989 branches[cs.branch] = cs.id
988 branches[cs.branch] = cs.id
990
989
991 # limit by branches
990 # limit by branches
992 if (
991 if (
993 opts[b"branches"]
992 opts[b"branches"]
994 and (cs.branch or b'HEAD') not in opts[b"branches"]
993 and (cs.branch or b'HEAD') not in opts[b"branches"]
995 ):
994 ):
996 continue
995 continue
997
996
998 if not off:
997 if not off:
999 # Note: trailing spaces on several lines here are needed to have
998 # Note: trailing spaces on several lines here are needed to have
1000 # bug-for-bug compatibility with cvsps.
999 # bug-for-bug compatibility with cvsps.
1001 ui.write(b'---------------------\n')
1000 ui.write(b'---------------------\n')
1002 ui.write((b'PatchSet %d \n' % cs.id))
1001 ui.write((b'PatchSet %d \n' % cs.id))
1003 ui.write(
1002 ui.write(
1004 (
1003 (
1005 b'Date: %s\n'
1004 b'Date: %s\n'
1006 % dateutil.datestr(cs.date, b'%Y/%m/%d %H:%M:%S %1%2')
1005 % dateutil.datestr(cs.date, b'%Y/%m/%d %H:%M:%S %1%2')
1007 )
1006 )
1008 )
1007 )
1009 ui.write((b'Author: %s\n' % cs.author))
1008 ui.write((b'Author: %s\n' % cs.author))
1010 ui.write((b'Branch: %s\n' % (cs.branch or b'HEAD')))
1009 ui.write((b'Branch: %s\n' % (cs.branch or b'HEAD')))
1011 ui.write(
1010 ui.write(
1012 (
1011 (
1013 b'Tag%s: %s \n'
1012 b'Tag%s: %s \n'
1014 % (
1013 % (
1015 [b'', b's'][len(cs.tags) > 1],
1014 [b'', b's'][len(cs.tags) > 1],
1016 b','.join(cs.tags) or b'(none)',
1015 b','.join(cs.tags) or b'(none)',
1017 )
1016 )
1018 )
1017 )
1019 )
1018 )
1020 if cs.branchpoints:
1019 if cs.branchpoints:
1021 ui.writenoi18n(
1020 ui.writenoi18n(
1022 b'Branchpoints: %s \n' % b', '.join(sorted(cs.branchpoints))
1021 b'Branchpoints: %s \n' % b', '.join(sorted(cs.branchpoints))
1023 )
1022 )
1024 if opts[b"parents"] and cs.parents:
1023 if opts[b"parents"] and cs.parents:
1025 if len(cs.parents) > 1:
1024 if len(cs.parents) > 1:
1026 ui.write(
1025 ui.write(
1027 (
1026 (
1028 b'Parents: %s\n'
1027 b'Parents: %s\n'
1029 % (b','.join([(b"%d" % p.id) for p in cs.parents]))
1028 % (b','.join([(b"%d" % p.id) for p in cs.parents]))
1030 )
1029 )
1031 )
1030 )
1032 else:
1031 else:
1033 ui.write((b'Parent: %d\n' % cs.parents[0].id))
1032 ui.write((b'Parent: %d\n' % cs.parents[0].id))
1034
1033
1035 if opts[b"ancestors"]:
1034 if opts[b"ancestors"]:
1036 b = cs.branch
1035 b = cs.branch
1037 r = []
1036 r = []
1038 while b:
1037 while b:
1039 b, c = ancestors[b]
1038 b, c = ancestors[b]
1040 r.append(b'%s:%d:%d' % (b or b"HEAD", c, branches[b]))
1039 r.append(b'%s:%d:%d' % (b or b"HEAD", c, branches[b]))
1041 if r:
1040 if r:
1042 ui.write((b'Ancestors: %s\n' % (b','.join(r))))
1041 ui.write((b'Ancestors: %s\n' % (b','.join(r))))
1043
1042
1044 ui.writenoi18n(b'Log:\n')
1043 ui.writenoi18n(b'Log:\n')
1045 ui.write(b'%s\n\n' % cs.comment)
1044 ui.write(b'%s\n\n' % cs.comment)
1046 ui.writenoi18n(b'Members: \n')
1045 ui.writenoi18n(b'Members: \n')
1047 for f in cs.entries:
1046 for f in cs.entries:
1048 fn = f.file
1047 fn = f.file
1049 if fn.startswith(opts[b"prefix"]):
1048 if fn.startswith(opts[b"prefix"]):
1050 fn = fn[len(opts[b"prefix"]) :]
1049 fn = fn[len(opts[b"prefix"]) :]
1051 ui.write(
1050 ui.write(
1052 b'\t%s:%s->%s%s \n'
1051 b'\t%s:%s->%s%s \n'
1053 % (
1052 % (
1054 fn,
1053 fn,
1055 b'.'.join([b"%d" % x for x in f.parent]) or b'INITIAL',
1054 b'.'.join([b"%d" % x for x in f.parent]) or b'INITIAL',
1056 b'.'.join([(b"%d" % x) for x in f.revision]),
1055 b'.'.join([(b"%d" % x) for x in f.revision]),
1057 [b'', b'(DEAD)'][f.dead],
1056 [b'', b'(DEAD)'][f.dead],
1058 )
1057 )
1059 )
1058 )
1060 ui.write(b'\n')
1059 ui.write(b'\n')
1061
1060
1062 # have we seen the start tag?
1061 # have we seen the start tag?
1063 if revisions and off:
1062 if revisions and off:
1064 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
1063 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
1065 off = False
1064 off = False
1066
1065
1067 # see if we reached the end tag
1066 # see if we reached the end tag
1068 if len(revisions) > 1 and not off:
1067 if len(revisions) > 1 and not off:
1069 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
1068 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
1070 break
1069 break
@@ -1,1741 +1,1741 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import codecs
6 import codecs
7 import locale
7 import locale
8 import os
8 import os
9 import pickle
9 import re
10 import re
10 import xml.dom.minidom
11 import xml.dom.minidom
11
12
12 from mercurial.i18n import _
13 from mercurial.i18n import _
13 from mercurial.pycompat import open
14 from mercurial.pycompat import open
14 from mercurial import (
15 from mercurial import (
15 encoding,
16 encoding,
16 error,
17 error,
17 pycompat,
18 pycompat,
18 util,
19 util,
19 vfs as vfsmod,
20 vfs as vfsmod,
20 )
21 )
21 from mercurial.utils import (
22 from mercurial.utils import (
22 dateutil,
23 dateutil,
23 procutil,
24 procutil,
24 stringutil,
25 stringutil,
25 )
26 )
26
27
27 from . import common
28 from . import common
28
29
29 pickle = util.pickle
30 stringio = util.stringio
30 stringio = util.stringio
31 propertycache = util.propertycache
31 propertycache = util.propertycache
32 urlerr = util.urlerr
32 urlerr = util.urlerr
33 urlreq = util.urlreq
33 urlreq = util.urlreq
34
34
35 commandline = common.commandline
35 commandline = common.commandline
36 commit = common.commit
36 commit = common.commit
37 converter_sink = common.converter_sink
37 converter_sink = common.converter_sink
38 converter_source = common.converter_source
38 converter_source = common.converter_source
39 decodeargs = common.decodeargs
39 decodeargs = common.decodeargs
40 encodeargs = common.encodeargs
40 encodeargs = common.encodeargs
41 makedatetimestamp = common.makedatetimestamp
41 makedatetimestamp = common.makedatetimestamp
42 mapfile = common.mapfile
42 mapfile = common.mapfile
43 MissingTool = common.MissingTool
43 MissingTool = common.MissingTool
44 NoRepo = common.NoRepo
44 NoRepo = common.NoRepo
45
45
46 # Subversion stuff. Works best with very recent Python SVN bindings
46 # Subversion stuff. Works best with very recent Python SVN bindings
47 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
47 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
48 # these bindings.
48 # these bindings.
49
49
50 try:
50 try:
51 import svn
51 import svn
52 import svn.client
52 import svn.client
53 import svn.core
53 import svn.core
54 import svn.ra
54 import svn.ra
55 import svn.delta
55 import svn.delta
56 from . import transport
56 from . import transport
57 import warnings
57 import warnings
58
58
59 warnings.filterwarnings(
59 warnings.filterwarnings(
60 'ignore', module='svn.core', category=DeprecationWarning
60 'ignore', module='svn.core', category=DeprecationWarning
61 )
61 )
62 svn.core.SubversionException # trigger import to catch error
62 svn.core.SubversionException # trigger import to catch error
63
63
64 except ImportError:
64 except ImportError:
65 svn = None
65 svn = None
66
66
67
67
68 # In Subversion, paths and URLs are Unicode (encoded as UTF-8), which
68 # In Subversion, paths and URLs are Unicode (encoded as UTF-8), which
69 # Subversion converts from / to native strings when interfacing with the OS.
69 # Subversion converts from / to native strings when interfacing with the OS.
70 # When passing paths and URLs to Subversion, we have to recode them such that
70 # When passing paths and URLs to Subversion, we have to recode them such that
71 # it roundstrips with what Subversion is doing.
71 # it roundstrips with what Subversion is doing.
72
72
73 fsencoding = None
73 fsencoding = None
74
74
75
75
76 def init_fsencoding():
76 def init_fsencoding():
77 global fsencoding, fsencoding_is_utf8
77 global fsencoding, fsencoding_is_utf8
78 if fsencoding is not None:
78 if fsencoding is not None:
79 return
79 return
80 if pycompat.iswindows:
80 if pycompat.iswindows:
81 # On Windows, filenames are Unicode, but we store them using the MBCS
81 # On Windows, filenames are Unicode, but we store them using the MBCS
82 # encoding.
82 # encoding.
83 fsencoding = 'mbcs'
83 fsencoding = 'mbcs'
84 else:
84 else:
85 # This is the encoding used to convert UTF-8 back to natively-encoded
85 # This is the encoding used to convert UTF-8 back to natively-encoded
86 # strings in Subversion 1.14.0 or earlier with APR 1.7.0 or earlier.
86 # strings in Subversion 1.14.0 or earlier with APR 1.7.0 or earlier.
87 with util.with_lc_ctype():
87 with util.with_lc_ctype():
88 fsencoding = locale.nl_langinfo(locale.CODESET) or 'ISO-8859-1'
88 fsencoding = locale.nl_langinfo(locale.CODESET) or 'ISO-8859-1'
89 fsencoding = codecs.lookup(fsencoding).name
89 fsencoding = codecs.lookup(fsencoding).name
90 fsencoding_is_utf8 = fsencoding == codecs.lookup('utf-8').name
90 fsencoding_is_utf8 = fsencoding == codecs.lookup('utf-8').name
91
91
92
92
93 def fs2svn(s):
93 def fs2svn(s):
94 if fsencoding_is_utf8:
94 if fsencoding_is_utf8:
95 return s
95 return s
96 else:
96 else:
97 return s.decode(fsencoding).encode('utf-8')
97 return s.decode(fsencoding).encode('utf-8')
98
98
99
99
100 def formatsvndate(date):
100 def formatsvndate(date):
101 return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
101 return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
102
102
103
103
104 def parsesvndate(s):
104 def parsesvndate(s):
105 # Example SVN datetime. Includes microseconds.
105 # Example SVN datetime. Includes microseconds.
106 # ISO-8601 conformant
106 # ISO-8601 conformant
107 # '2007-01-04T17:35:00.902377Z'
107 # '2007-01-04T17:35:00.902377Z'
108 return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
108 return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
109
109
110
110
111 class SvnPathNotFound(Exception):
111 class SvnPathNotFound(Exception):
112 pass
112 pass
113
113
114
114
115 def revsplit(rev):
115 def revsplit(rev):
116 """Parse a revision string and return (uuid, path, revnum).
116 """Parse a revision string and return (uuid, path, revnum).
117 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
117 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
118 ... b'/proj%20B/mytrunk/mytrunk@1')
118 ... b'/proj%20B/mytrunk/mytrunk@1')
119 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
119 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
120 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
120 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
121 ('', '', 1)
121 ('', '', 1)
122 >>> revsplit(b'@7')
122 >>> revsplit(b'@7')
123 ('', '', 7)
123 ('', '', 7)
124 >>> revsplit(b'7')
124 >>> revsplit(b'7')
125 ('', '', 0)
125 ('', '', 0)
126 >>> revsplit(b'bad')
126 >>> revsplit(b'bad')
127 ('', '', 0)
127 ('', '', 0)
128 """
128 """
129 parts = rev.rsplit(b'@', 1)
129 parts = rev.rsplit(b'@', 1)
130 revnum = 0
130 revnum = 0
131 if len(parts) > 1:
131 if len(parts) > 1:
132 revnum = int(parts[1])
132 revnum = int(parts[1])
133 parts = parts[0].split(b'/', 1)
133 parts = parts[0].split(b'/', 1)
134 uuid = b''
134 uuid = b''
135 mod = b''
135 mod = b''
136 if len(parts) > 1 and parts[0].startswith(b'svn:'):
136 if len(parts) > 1 and parts[0].startswith(b'svn:'):
137 uuid = parts[0][4:]
137 uuid = parts[0][4:]
138 mod = b'/' + parts[1]
138 mod = b'/' + parts[1]
139 return uuid, mod, revnum
139 return uuid, mod, revnum
140
140
141
141
142 def quote(s):
142 def quote(s):
143 # As of svn 1.7, many svn calls expect "canonical" paths. In
143 # As of svn 1.7, many svn calls expect "canonical" paths. In
144 # theory, we should call svn.core.*canonicalize() on all paths
144 # theory, we should call svn.core.*canonicalize() on all paths
145 # before passing them to the API. Instead, we assume the base url
145 # before passing them to the API. Instead, we assume the base url
146 # is canonical and copy the behaviour of svn URL encoding function
146 # is canonical and copy the behaviour of svn URL encoding function
147 # so we can extend it safely with new components. The "safe"
147 # so we can extend it safely with new components. The "safe"
148 # characters were taken from the "svn_uri__char_validity" table in
148 # characters were taken from the "svn_uri__char_validity" table in
149 # libsvn_subr/path.c.
149 # libsvn_subr/path.c.
150 return urlreq.quote(s, b"!$&'()*+,-./:=@_~")
150 return urlreq.quote(s, b"!$&'()*+,-./:=@_~")
151
151
152
152
153 def geturl(path):
153 def geturl(path):
154 """Convert path or URL to a SVN URL, encoded in UTF-8.
154 """Convert path or URL to a SVN URL, encoded in UTF-8.
155
155
156 This can raise UnicodeDecodeError if the path or URL can't be converted to
156 This can raise UnicodeDecodeError if the path or URL can't be converted to
157 unicode using `fsencoding`.
157 unicode using `fsencoding`.
158 """
158 """
159 try:
159 try:
160 return svn.client.url_from_path(
160 return svn.client.url_from_path(
161 svn.core.svn_path_canonicalize(fs2svn(path))
161 svn.core.svn_path_canonicalize(fs2svn(path))
162 )
162 )
163 except svn.core.SubversionException:
163 except svn.core.SubversionException:
164 # svn.client.url_from_path() fails with local repositories
164 # svn.client.url_from_path() fails with local repositories
165 pass
165 pass
166 if os.path.isdir(path):
166 if os.path.isdir(path):
167 path = os.path.normpath(util.abspath(path))
167 path = os.path.normpath(util.abspath(path))
168 if pycompat.iswindows:
168 if pycompat.iswindows:
169 path = b'/' + util.normpath(path)
169 path = b'/' + util.normpath(path)
170 # Module URL is later compared with the repository URL returned
170 # Module URL is later compared with the repository URL returned
171 # by svn API, which is UTF-8.
171 # by svn API, which is UTF-8.
172 path = fs2svn(path)
172 path = fs2svn(path)
173 path = b'file://%s' % quote(path)
173 path = b'file://%s' % quote(path)
174 return svn.core.svn_path_canonicalize(path)
174 return svn.core.svn_path_canonicalize(path)
175
175
176
176
177 def optrev(number):
177 def optrev(number):
178 optrev = svn.core.svn_opt_revision_t()
178 optrev = svn.core.svn_opt_revision_t()
179 optrev.kind = svn.core.svn_opt_revision_number
179 optrev.kind = svn.core.svn_opt_revision_number
180 optrev.value.number = number
180 optrev.value.number = number
181 return optrev
181 return optrev
182
182
183
183
184 class changedpath(object):
184 class changedpath(object):
185 def __init__(self, p):
185 def __init__(self, p):
186 self.copyfrom_path = p.copyfrom_path
186 self.copyfrom_path = p.copyfrom_path
187 self.copyfrom_rev = p.copyfrom_rev
187 self.copyfrom_rev = p.copyfrom_rev
188 self.action = p.action
188 self.action = p.action
189
189
190
190
191 def get_log_child(
191 def get_log_child(
192 fp,
192 fp,
193 url,
193 url,
194 paths,
194 paths,
195 start,
195 start,
196 end,
196 end,
197 limit=0,
197 limit=0,
198 discover_changed_paths=True,
198 discover_changed_paths=True,
199 strict_node_history=False,
199 strict_node_history=False,
200 ):
200 ):
201 protocol = -1
201 protocol = -1
202
202
203 def receiver(orig_paths, revnum, author, date, message, pool):
203 def receiver(orig_paths, revnum, author, date, message, pool):
204 paths = {}
204 paths = {}
205 if orig_paths is not None:
205 if orig_paths is not None:
206 for k, v in pycompat.iteritems(orig_paths):
206 for k, v in pycompat.iteritems(orig_paths):
207 paths[k] = changedpath(v)
207 paths[k] = changedpath(v)
208 pickle.dump((paths, revnum, author, date, message), fp, protocol)
208 pickle.dump((paths, revnum, author, date, message), fp, protocol)
209
209
210 try:
210 try:
211 # Use an ra of our own so that our parent can consume
211 # Use an ra of our own so that our parent can consume
212 # our results without confusing the server.
212 # our results without confusing the server.
213 t = transport.SvnRaTransport(url=url)
213 t = transport.SvnRaTransport(url=url)
214 svn.ra.get_log(
214 svn.ra.get_log(
215 t.ra,
215 t.ra,
216 paths,
216 paths,
217 start,
217 start,
218 end,
218 end,
219 limit,
219 limit,
220 discover_changed_paths,
220 discover_changed_paths,
221 strict_node_history,
221 strict_node_history,
222 receiver,
222 receiver,
223 )
223 )
224 except IOError:
224 except IOError:
225 # Caller may interrupt the iteration
225 # Caller may interrupt the iteration
226 pickle.dump(None, fp, protocol)
226 pickle.dump(None, fp, protocol)
227 except Exception as inst:
227 except Exception as inst:
228 pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
228 pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
229 else:
229 else:
230 pickle.dump(None, fp, protocol)
230 pickle.dump(None, fp, protocol)
231 fp.flush()
231 fp.flush()
232 # With large history, cleanup process goes crazy and suddenly
232 # With large history, cleanup process goes crazy and suddenly
233 # consumes *huge* amount of memory. The output file being closed,
233 # consumes *huge* amount of memory. The output file being closed,
234 # there is no need for clean termination.
234 # there is no need for clean termination.
235 os._exit(0)
235 os._exit(0)
236
236
237
237
238 def debugsvnlog(ui, **opts):
238 def debugsvnlog(ui, **opts):
239 """Fetch SVN log in a subprocess and channel them back to parent to
239 """Fetch SVN log in a subprocess and channel them back to parent to
240 avoid memory collection issues.
240 avoid memory collection issues.
241 """
241 """
242 with util.with_lc_ctype():
242 with util.with_lc_ctype():
243 if svn is None:
243 if svn is None:
244 raise error.Abort(
244 raise error.Abort(
245 _(b'debugsvnlog could not load Subversion python bindings')
245 _(b'debugsvnlog could not load Subversion python bindings')
246 )
246 )
247
247
248 args = decodeargs(ui.fin.read())
248 args = decodeargs(ui.fin.read())
249 get_log_child(ui.fout, *args)
249 get_log_child(ui.fout, *args)
250
250
251
251
252 class logstream(object):
252 class logstream(object):
253 """Interruptible revision log iterator."""
253 """Interruptible revision log iterator."""
254
254
255 def __init__(self, stdout):
255 def __init__(self, stdout):
256 self._stdout = stdout
256 self._stdout = stdout
257
257
258 def __iter__(self):
258 def __iter__(self):
259 while True:
259 while True:
260 try:
260 try:
261 entry = pickle.load(self._stdout)
261 entry = pickle.load(self._stdout)
262 except EOFError:
262 except EOFError:
263 raise error.Abort(
263 raise error.Abort(
264 _(
264 _(
265 b'Mercurial failed to run itself, check'
265 b'Mercurial failed to run itself, check'
266 b' hg executable is in PATH'
266 b' hg executable is in PATH'
267 )
267 )
268 )
268 )
269 try:
269 try:
270 orig_paths, revnum, author, date, message = entry
270 orig_paths, revnum, author, date, message = entry
271 except (TypeError, ValueError):
271 except (TypeError, ValueError):
272 if entry is None:
272 if entry is None:
273 break
273 break
274 raise error.Abort(_(b"log stream exception '%s'") % entry)
274 raise error.Abort(_(b"log stream exception '%s'") % entry)
275 yield entry
275 yield entry
276
276
277 def close(self):
277 def close(self):
278 if self._stdout:
278 if self._stdout:
279 self._stdout.close()
279 self._stdout.close()
280 self._stdout = None
280 self._stdout = None
281
281
282
282
283 class directlogstream(list):
283 class directlogstream(list):
284 """Direct revision log iterator.
284 """Direct revision log iterator.
285 This can be used for debugging and development but it will probably leak
285 This can be used for debugging and development but it will probably leak
286 memory and is not suitable for real conversions."""
286 memory and is not suitable for real conversions."""
287
287
288 def __init__(
288 def __init__(
289 self,
289 self,
290 url,
290 url,
291 paths,
291 paths,
292 start,
292 start,
293 end,
293 end,
294 limit=0,
294 limit=0,
295 discover_changed_paths=True,
295 discover_changed_paths=True,
296 strict_node_history=False,
296 strict_node_history=False,
297 ):
297 ):
298 def receiver(orig_paths, revnum, author, date, message, pool):
298 def receiver(orig_paths, revnum, author, date, message, pool):
299 paths = {}
299 paths = {}
300 if orig_paths is not None:
300 if orig_paths is not None:
301 for k, v in pycompat.iteritems(orig_paths):
301 for k, v in pycompat.iteritems(orig_paths):
302 paths[k] = changedpath(v)
302 paths[k] = changedpath(v)
303 self.append((paths, revnum, author, date, message))
303 self.append((paths, revnum, author, date, message))
304
304
305 # Use an ra of our own so that our parent can consume
305 # Use an ra of our own so that our parent can consume
306 # our results without confusing the server.
306 # our results without confusing the server.
307 t = transport.SvnRaTransport(url=url)
307 t = transport.SvnRaTransport(url=url)
308 svn.ra.get_log(
308 svn.ra.get_log(
309 t.ra,
309 t.ra,
310 paths,
310 paths,
311 start,
311 start,
312 end,
312 end,
313 limit,
313 limit,
314 discover_changed_paths,
314 discover_changed_paths,
315 strict_node_history,
315 strict_node_history,
316 receiver,
316 receiver,
317 )
317 )
318
318
319 def close(self):
319 def close(self):
320 pass
320 pass
321
321
322
322
323 # Check to see if the given path is a local Subversion repo. Verify this by
323 # Check to see if the given path is a local Subversion repo. Verify this by
324 # looking for several svn-specific files and directories in the given
324 # looking for several svn-specific files and directories in the given
325 # directory.
325 # directory.
326 def filecheck(ui, path, proto):
326 def filecheck(ui, path, proto):
327 for x in (b'locks', b'hooks', b'format', b'db'):
327 for x in (b'locks', b'hooks', b'format', b'db'):
328 if not os.path.exists(os.path.join(path, x)):
328 if not os.path.exists(os.path.join(path, x)):
329 return False
329 return False
330 return True
330 return True
331
331
332
332
333 # Check to see if a given path is the root of an svn repo over http. We verify
333 # Check to see if a given path is the root of an svn repo over http. We verify
334 # this by requesting a version-controlled URL we know can't exist and looking
334 # this by requesting a version-controlled URL we know can't exist and looking
335 # for the svn-specific "not found" XML.
335 # for the svn-specific "not found" XML.
336 def httpcheck(ui, path, proto):
336 def httpcheck(ui, path, proto):
337 try:
337 try:
338 opener = urlreq.buildopener()
338 opener = urlreq.buildopener()
339 rsp = opener.open(
339 rsp = opener.open(
340 pycompat.strurl(b'%s://%s/!svn/ver/0/.svn' % (proto, path)), b'rb'
340 pycompat.strurl(b'%s://%s/!svn/ver/0/.svn' % (proto, path)), b'rb'
341 )
341 )
342 data = rsp.read()
342 data = rsp.read()
343 except urlerr.httperror as inst:
343 except urlerr.httperror as inst:
344 if inst.code != 404:
344 if inst.code != 404:
345 # Except for 404 we cannot know for sure this is not an svn repo
345 # Except for 404 we cannot know for sure this is not an svn repo
346 ui.warn(
346 ui.warn(
347 _(
347 _(
348 b'svn: cannot probe remote repository, assume it could '
348 b'svn: cannot probe remote repository, assume it could '
349 b'be a subversion repository. Use --source-type if you '
349 b'be a subversion repository. Use --source-type if you '
350 b'know better.\n'
350 b'know better.\n'
351 )
351 )
352 )
352 )
353 return True
353 return True
354 data = inst.fp.read()
354 data = inst.fp.read()
355 except Exception:
355 except Exception:
356 # Could be urlerr.urlerror if the URL is invalid or anything else.
356 # Could be urlerr.urlerror if the URL is invalid or anything else.
357 return False
357 return False
358 return b'<m:human-readable errcode="160013">' in data
358 return b'<m:human-readable errcode="160013">' in data
359
359
360
360
361 protomap = {
361 protomap = {
362 b'http': httpcheck,
362 b'http': httpcheck,
363 b'https': httpcheck,
363 b'https': httpcheck,
364 b'file': filecheck,
364 b'file': filecheck,
365 }
365 }
366
366
367
367
368 class NonUtf8PercentEncodedBytes(Exception):
368 class NonUtf8PercentEncodedBytes(Exception):
369 pass
369 pass
370
370
371
371
372 # Subversion paths are Unicode. Since the percent-decoding is done on
372 # Subversion paths are Unicode. Since the percent-decoding is done on
373 # UTF-8-encoded strings, percent-encoded bytes are interpreted as UTF-8.
373 # UTF-8-encoded strings, percent-encoded bytes are interpreted as UTF-8.
374 def url2pathname_like_subversion(unicodepath):
374 def url2pathname_like_subversion(unicodepath):
375 if pycompat.ispy3:
375 if pycompat.ispy3:
376 # On Python 3, we have to pass unicode to urlreq.url2pathname().
376 # On Python 3, we have to pass unicode to urlreq.url2pathname().
377 # Percent-decoded bytes get decoded using UTF-8 and the 'replace' error
377 # Percent-decoded bytes get decoded using UTF-8 and the 'replace' error
378 # handler.
378 # handler.
379 unicodepath = urlreq.url2pathname(unicodepath)
379 unicodepath = urlreq.url2pathname(unicodepath)
380 if u'\N{REPLACEMENT CHARACTER}' in unicodepath:
380 if u'\N{REPLACEMENT CHARACTER}' in unicodepath:
381 raise NonUtf8PercentEncodedBytes
381 raise NonUtf8PercentEncodedBytes
382 else:
382 else:
383 return unicodepath
383 return unicodepath
384 else:
384 else:
385 # If we passed unicode on Python 2, it would be converted using the
385 # If we passed unicode on Python 2, it would be converted using the
386 # latin-1 encoding. Therefore, we pass UTF-8-encoded bytes.
386 # latin-1 encoding. Therefore, we pass UTF-8-encoded bytes.
387 unicodepath = urlreq.url2pathname(unicodepath.encode('utf-8'))
387 unicodepath = urlreq.url2pathname(unicodepath.encode('utf-8'))
388 try:
388 try:
389 return unicodepath.decode('utf-8')
389 return unicodepath.decode('utf-8')
390 except UnicodeDecodeError:
390 except UnicodeDecodeError:
391 raise NonUtf8PercentEncodedBytes
391 raise NonUtf8PercentEncodedBytes
392
392
393
393
394 def issvnurl(ui, url):
394 def issvnurl(ui, url):
395 try:
395 try:
396 proto, path = url.split(b'://', 1)
396 proto, path = url.split(b'://', 1)
397 if proto == b'file':
397 if proto == b'file':
398 if (
398 if (
399 pycompat.iswindows
399 pycompat.iswindows
400 and path[:1] == b'/'
400 and path[:1] == b'/'
401 and path[1:2].isalpha()
401 and path[1:2].isalpha()
402 and path[2:6].lower() == b'%3a/'
402 and path[2:6].lower() == b'%3a/'
403 ):
403 ):
404 path = path[:2] + b':/' + path[6:]
404 path = path[:2] + b':/' + path[6:]
405 try:
405 try:
406 unicodepath = path.decode(fsencoding)
406 unicodepath = path.decode(fsencoding)
407 except UnicodeDecodeError:
407 except UnicodeDecodeError:
408 ui.warn(
408 ui.warn(
409 _(
409 _(
410 b'Subversion requires that file URLs can be converted '
410 b'Subversion requires that file URLs can be converted '
411 b'to Unicode using the current locale encoding (%s)\n'
411 b'to Unicode using the current locale encoding (%s)\n'
412 )
412 )
413 % pycompat.sysbytes(fsencoding)
413 % pycompat.sysbytes(fsencoding)
414 )
414 )
415 return False
415 return False
416 try:
416 try:
417 unicodepath = url2pathname_like_subversion(unicodepath)
417 unicodepath = url2pathname_like_subversion(unicodepath)
418 except NonUtf8PercentEncodedBytes:
418 except NonUtf8PercentEncodedBytes:
419 ui.warn(
419 ui.warn(
420 _(
420 _(
421 b'Subversion does not support non-UTF-8 '
421 b'Subversion does not support non-UTF-8 '
422 b'percent-encoded bytes in file URLs\n'
422 b'percent-encoded bytes in file URLs\n'
423 )
423 )
424 )
424 )
425 return False
425 return False
426 # Below, we approximate how Subversion checks the path. On Unix, we
426 # Below, we approximate how Subversion checks the path. On Unix, we
427 # should therefore convert the path to bytes using `fsencoding`
427 # should therefore convert the path to bytes using `fsencoding`
428 # (like Subversion does). On Windows, the right thing would
428 # (like Subversion does). On Windows, the right thing would
429 # actually be to leave the path as unicode. For now, we restrict
429 # actually be to leave the path as unicode. For now, we restrict
430 # the path to MBCS.
430 # the path to MBCS.
431 path = unicodepath.encode(fsencoding)
431 path = unicodepath.encode(fsencoding)
432 except ValueError:
432 except ValueError:
433 proto = b'file'
433 proto = b'file'
434 path = util.abspath(url)
434 path = util.abspath(url)
435 try:
435 try:
436 path.decode(fsencoding)
436 path.decode(fsencoding)
437 except UnicodeDecodeError:
437 except UnicodeDecodeError:
438 ui.warn(
438 ui.warn(
439 _(
439 _(
440 b'Subversion requires that paths can be converted to '
440 b'Subversion requires that paths can be converted to '
441 b'Unicode using the current locale encoding (%s)\n'
441 b'Unicode using the current locale encoding (%s)\n'
442 )
442 )
443 % pycompat.sysbytes(fsencoding)
443 % pycompat.sysbytes(fsencoding)
444 )
444 )
445 return False
445 return False
446 if proto == b'file':
446 if proto == b'file':
447 path = util.pconvert(path)
447 path = util.pconvert(path)
448 elif proto in (b'http', 'https'):
448 elif proto in (b'http', 'https'):
449 if not encoding.isasciistr(path):
449 if not encoding.isasciistr(path):
450 ui.warn(
450 ui.warn(
451 _(
451 _(
452 b"Subversion sources don't support non-ASCII characters in "
452 b"Subversion sources don't support non-ASCII characters in "
453 b"HTTP(S) URLs. Please percent-encode them.\n"
453 b"HTTP(S) URLs. Please percent-encode them.\n"
454 )
454 )
455 )
455 )
456 return False
456 return False
457 check = protomap.get(proto, lambda *args: False)
457 check = protomap.get(proto, lambda *args: False)
458 while b'/' in path:
458 while b'/' in path:
459 if check(ui, path, proto):
459 if check(ui, path, proto):
460 return True
460 return True
461 path = path.rsplit(b'/', 1)[0]
461 path = path.rsplit(b'/', 1)[0]
462 return False
462 return False
463
463
464
464
465 # SVN conversion code stolen from bzr-svn and tailor
465 # SVN conversion code stolen from bzr-svn and tailor
466 #
466 #
467 # Subversion looks like a versioned filesystem, branches structures
467 # Subversion looks like a versioned filesystem, branches structures
468 # are defined by conventions and not enforced by the tool. First,
468 # are defined by conventions and not enforced by the tool. First,
469 # we define the potential branches (modules) as "trunk" and "branches"
469 # we define the potential branches (modules) as "trunk" and "branches"
470 # children directories. Revisions are then identified by their
470 # children directories. Revisions are then identified by their
471 # module and revision number (and a repository identifier).
471 # module and revision number (and a repository identifier).
472 #
472 #
473 # The revision graph is really a tree (or a forest). By default, a
473 # The revision graph is really a tree (or a forest). By default, a
474 # revision parent is the previous revision in the same module. If the
474 # revision parent is the previous revision in the same module. If the
475 # module directory is copied/moved from another module then the
475 # module directory is copied/moved from another module then the
476 # revision is the module root and its parent the source revision in
476 # revision is the module root and its parent the source revision in
477 # the parent module. A revision has at most one parent.
477 # the parent module. A revision has at most one parent.
478 #
478 #
479 class svn_source(converter_source):
479 class svn_source(converter_source):
480 def __init__(self, ui, repotype, url, revs=None):
480 def __init__(self, ui, repotype, url, revs=None):
481 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
481 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
482
482
483 init_fsencoding()
483 init_fsencoding()
484 if not (
484 if not (
485 url.startswith(b'svn://')
485 url.startswith(b'svn://')
486 or url.startswith(b'svn+ssh://')
486 or url.startswith(b'svn+ssh://')
487 or (
487 or (
488 os.path.exists(url)
488 os.path.exists(url)
489 and os.path.exists(os.path.join(url, b'.svn'))
489 and os.path.exists(os.path.join(url, b'.svn'))
490 )
490 )
491 or issvnurl(ui, url)
491 or issvnurl(ui, url)
492 ):
492 ):
493 raise NoRepo(
493 raise NoRepo(
494 _(b"%s does not look like a Subversion repository") % url
494 _(b"%s does not look like a Subversion repository") % url
495 )
495 )
496 if svn is None:
496 if svn is None:
497 raise MissingTool(_(b'could not load Subversion python bindings'))
497 raise MissingTool(_(b'could not load Subversion python bindings'))
498
498
499 try:
499 try:
500 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
500 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
501 if version < (1, 4):
501 if version < (1, 4):
502 raise MissingTool(
502 raise MissingTool(
503 _(
503 _(
504 b'Subversion python bindings %d.%d found, '
504 b'Subversion python bindings %d.%d found, '
505 b'1.4 or later required'
505 b'1.4 or later required'
506 )
506 )
507 % version
507 % version
508 )
508 )
509 except AttributeError:
509 except AttributeError:
510 raise MissingTool(
510 raise MissingTool(
511 _(
511 _(
512 b'Subversion python bindings are too old, 1.4 '
512 b'Subversion python bindings are too old, 1.4 '
513 b'or later required'
513 b'or later required'
514 )
514 )
515 )
515 )
516
516
517 self.lastrevs = {}
517 self.lastrevs = {}
518
518
519 latest = None
519 latest = None
520 try:
520 try:
521 # Support file://path@rev syntax. Useful e.g. to convert
521 # Support file://path@rev syntax. Useful e.g. to convert
522 # deleted branches.
522 # deleted branches.
523 at = url.rfind(b'@')
523 at = url.rfind(b'@')
524 if at >= 0:
524 if at >= 0:
525 latest = int(url[at + 1 :])
525 latest = int(url[at + 1 :])
526 url = url[:at]
526 url = url[:at]
527 except ValueError:
527 except ValueError:
528 pass
528 pass
529 self.url = geturl(url)
529 self.url = geturl(url)
530 self.encoding = b'UTF-8' # Subversion is always nominal UTF-8
530 self.encoding = b'UTF-8' # Subversion is always nominal UTF-8
531 try:
531 try:
532 with util.with_lc_ctype():
532 with util.with_lc_ctype():
533 self.transport = transport.SvnRaTransport(url=self.url)
533 self.transport = transport.SvnRaTransport(url=self.url)
534 self.ra = self.transport.ra
534 self.ra = self.transport.ra
535 self.ctx = self.transport.client
535 self.ctx = self.transport.client
536 self.baseurl = svn.ra.get_repos_root(self.ra)
536 self.baseurl = svn.ra.get_repos_root(self.ra)
537 # Module is either empty or a repository path starting with
537 # Module is either empty or a repository path starting with
538 # a slash and not ending with a slash.
538 # a slash and not ending with a slash.
539 self.module = urlreq.unquote(self.url[len(self.baseurl) :])
539 self.module = urlreq.unquote(self.url[len(self.baseurl) :])
540 self.prevmodule = None
540 self.prevmodule = None
541 self.rootmodule = self.module
541 self.rootmodule = self.module
542 self.commits = {}
542 self.commits = {}
543 self.paths = {}
543 self.paths = {}
544 self.uuid = svn.ra.get_uuid(self.ra)
544 self.uuid = svn.ra.get_uuid(self.ra)
545 except svn.core.SubversionException:
545 except svn.core.SubversionException:
546 ui.traceback()
546 ui.traceback()
547 svnversion = b'%d.%d.%d' % (
547 svnversion = b'%d.%d.%d' % (
548 svn.core.SVN_VER_MAJOR,
548 svn.core.SVN_VER_MAJOR,
549 svn.core.SVN_VER_MINOR,
549 svn.core.SVN_VER_MINOR,
550 svn.core.SVN_VER_MICRO,
550 svn.core.SVN_VER_MICRO,
551 )
551 )
552 raise NoRepo(
552 raise NoRepo(
553 _(
553 _(
554 b"%s does not look like a Subversion repository "
554 b"%s does not look like a Subversion repository "
555 b"to libsvn version %s"
555 b"to libsvn version %s"
556 )
556 )
557 % (self.url, svnversion)
557 % (self.url, svnversion)
558 )
558 )
559
559
560 if revs:
560 if revs:
561 if len(revs) > 1:
561 if len(revs) > 1:
562 raise error.Abort(
562 raise error.Abort(
563 _(
563 _(
564 b'subversion source does not support '
564 b'subversion source does not support '
565 b'specifying multiple revisions'
565 b'specifying multiple revisions'
566 )
566 )
567 )
567 )
568 try:
568 try:
569 latest = int(revs[0])
569 latest = int(revs[0])
570 except ValueError:
570 except ValueError:
571 raise error.Abort(
571 raise error.Abort(
572 _(b'svn: revision %s is not an integer') % revs[0]
572 _(b'svn: revision %s is not an integer') % revs[0]
573 )
573 )
574
574
575 trunkcfg = self.ui.config(b'convert', b'svn.trunk')
575 trunkcfg = self.ui.config(b'convert', b'svn.trunk')
576 if trunkcfg is None:
576 if trunkcfg is None:
577 trunkcfg = b'trunk'
577 trunkcfg = b'trunk'
578 self.trunkname = trunkcfg.strip(b'/')
578 self.trunkname = trunkcfg.strip(b'/')
579 self.startrev = self.ui.config(b'convert', b'svn.startrev')
579 self.startrev = self.ui.config(b'convert', b'svn.startrev')
580 try:
580 try:
581 self.startrev = int(self.startrev)
581 self.startrev = int(self.startrev)
582 if self.startrev < 0:
582 if self.startrev < 0:
583 self.startrev = 0
583 self.startrev = 0
584 except ValueError:
584 except ValueError:
585 raise error.Abort(
585 raise error.Abort(
586 _(b'svn: start revision %s is not an integer') % self.startrev
586 _(b'svn: start revision %s is not an integer') % self.startrev
587 )
587 )
588
588
589 try:
589 try:
590 with util.with_lc_ctype():
590 with util.with_lc_ctype():
591 self.head = self.latest(self.module, latest)
591 self.head = self.latest(self.module, latest)
592 except SvnPathNotFound:
592 except SvnPathNotFound:
593 self.head = None
593 self.head = None
594 if not self.head:
594 if not self.head:
595 raise error.Abort(
595 raise error.Abort(
596 _(b'no revision found in module %s') % self.module
596 _(b'no revision found in module %s') % self.module
597 )
597 )
598 self.last_changed = self.revnum(self.head)
598 self.last_changed = self.revnum(self.head)
599
599
600 self._changescache = (None, None)
600 self._changescache = (None, None)
601
601
602 if os.path.exists(os.path.join(url, b'.svn/entries')):
602 if os.path.exists(os.path.join(url, b'.svn/entries')):
603 self.wc = url
603 self.wc = url
604 else:
604 else:
605 self.wc = None
605 self.wc = None
606 self.convertfp = None
606 self.convertfp = None
607
607
608 def before(self):
608 def before(self):
609 self.with_lc_ctype = util.with_lc_ctype()
609 self.with_lc_ctype = util.with_lc_ctype()
610 self.with_lc_ctype.__enter__()
610 self.with_lc_ctype.__enter__()
611
611
612 def after(self):
612 def after(self):
613 self.with_lc_ctype.__exit__(None, None, None)
613 self.with_lc_ctype.__exit__(None, None, None)
614
614
615 def setrevmap(self, revmap):
615 def setrevmap(self, revmap):
616 lastrevs = {}
616 lastrevs = {}
617 for revid in revmap:
617 for revid in revmap:
618 uuid, module, revnum = revsplit(revid)
618 uuid, module, revnum = revsplit(revid)
619 lastrevnum = lastrevs.setdefault(module, revnum)
619 lastrevnum = lastrevs.setdefault(module, revnum)
620 if revnum > lastrevnum:
620 if revnum > lastrevnum:
621 lastrevs[module] = revnum
621 lastrevs[module] = revnum
622 self.lastrevs = lastrevs
622 self.lastrevs = lastrevs
623
623
624 def exists(self, path, optrev):
624 def exists(self, path, optrev):
625 try:
625 try:
626 svn.client.ls(
626 svn.client.ls(
627 self.url.rstrip(b'/') + b'/' + quote(path),
627 self.url.rstrip(b'/') + b'/' + quote(path),
628 optrev,
628 optrev,
629 False,
629 False,
630 self.ctx,
630 self.ctx,
631 )
631 )
632 return True
632 return True
633 except svn.core.SubversionException:
633 except svn.core.SubversionException:
634 return False
634 return False
635
635
636 def getheads(self):
636 def getheads(self):
637 def isdir(path, revnum):
637 def isdir(path, revnum):
638 kind = self._checkpath(path, revnum)
638 kind = self._checkpath(path, revnum)
639 return kind == svn.core.svn_node_dir
639 return kind == svn.core.svn_node_dir
640
640
641 def getcfgpath(name, rev):
641 def getcfgpath(name, rev):
642 cfgpath = self.ui.config(b'convert', b'svn.' + name)
642 cfgpath = self.ui.config(b'convert', b'svn.' + name)
643 if cfgpath is not None and cfgpath.strip() == b'':
643 if cfgpath is not None and cfgpath.strip() == b'':
644 return None
644 return None
645 path = (cfgpath or name).strip(b'/')
645 path = (cfgpath or name).strip(b'/')
646 if not self.exists(path, rev):
646 if not self.exists(path, rev):
647 if self.module.endswith(path) and name == b'trunk':
647 if self.module.endswith(path) and name == b'trunk':
648 # we are converting from inside this directory
648 # we are converting from inside this directory
649 return None
649 return None
650 if cfgpath:
650 if cfgpath:
651 raise error.Abort(
651 raise error.Abort(
652 _(b'expected %s to be at %r, but not found')
652 _(b'expected %s to be at %r, but not found')
653 % (name, path)
653 % (name, path)
654 )
654 )
655 return None
655 return None
656 self.ui.note(
656 self.ui.note(
657 _(b'found %s at %r\n') % (name, pycompat.bytestr(path))
657 _(b'found %s at %r\n') % (name, pycompat.bytestr(path))
658 )
658 )
659 return path
659 return path
660
660
661 rev = optrev(self.last_changed)
661 rev = optrev(self.last_changed)
662 oldmodule = b''
662 oldmodule = b''
663 trunk = getcfgpath(b'trunk', rev)
663 trunk = getcfgpath(b'trunk', rev)
664 self.tags = getcfgpath(b'tags', rev)
664 self.tags = getcfgpath(b'tags', rev)
665 branches = getcfgpath(b'branches', rev)
665 branches = getcfgpath(b'branches', rev)
666
666
667 # If the project has a trunk or branches, we will extract heads
667 # If the project has a trunk or branches, we will extract heads
668 # from them. We keep the project root otherwise.
668 # from them. We keep the project root otherwise.
669 if trunk:
669 if trunk:
670 oldmodule = self.module or b''
670 oldmodule = self.module or b''
671 self.module += b'/' + trunk
671 self.module += b'/' + trunk
672 self.head = self.latest(self.module, self.last_changed)
672 self.head = self.latest(self.module, self.last_changed)
673 if not self.head:
673 if not self.head:
674 raise error.Abort(
674 raise error.Abort(
675 _(b'no revision found in module %s') % self.module
675 _(b'no revision found in module %s') % self.module
676 )
676 )
677
677
678 # First head in the list is the module's head
678 # First head in the list is the module's head
679 self.heads = [self.head]
679 self.heads = [self.head]
680 if self.tags is not None:
680 if self.tags is not None:
681 self.tags = b'%s/%s' % (oldmodule, (self.tags or b'tags'))
681 self.tags = b'%s/%s' % (oldmodule, (self.tags or b'tags'))
682
682
683 # Check if branches bring a few more heads to the list
683 # Check if branches bring a few more heads to the list
684 if branches:
684 if branches:
685 rpath = self.url.strip(b'/')
685 rpath = self.url.strip(b'/')
686 branchnames = svn.client.ls(
686 branchnames = svn.client.ls(
687 rpath + b'/' + quote(branches), rev, False, self.ctx
687 rpath + b'/' + quote(branches), rev, False, self.ctx
688 )
688 )
689 for branch in sorted(branchnames):
689 for branch in sorted(branchnames):
690 module = b'%s/%s/%s' % (oldmodule, branches, branch)
690 module = b'%s/%s/%s' % (oldmodule, branches, branch)
691 if not isdir(module, self.last_changed):
691 if not isdir(module, self.last_changed):
692 continue
692 continue
693 brevid = self.latest(module, self.last_changed)
693 brevid = self.latest(module, self.last_changed)
694 if not brevid:
694 if not brevid:
695 self.ui.note(_(b'ignoring empty branch %s\n') % branch)
695 self.ui.note(_(b'ignoring empty branch %s\n') % branch)
696 continue
696 continue
697 self.ui.note(
697 self.ui.note(
698 _(b'found branch %s at %d\n')
698 _(b'found branch %s at %d\n')
699 % (branch, self.revnum(brevid))
699 % (branch, self.revnum(brevid))
700 )
700 )
701 self.heads.append(brevid)
701 self.heads.append(brevid)
702
702
703 if self.startrev and self.heads:
703 if self.startrev and self.heads:
704 if len(self.heads) > 1:
704 if len(self.heads) > 1:
705 raise error.Abort(
705 raise error.Abort(
706 _(
706 _(
707 b'svn: start revision is not supported '
707 b'svn: start revision is not supported '
708 b'with more than one branch'
708 b'with more than one branch'
709 )
709 )
710 )
710 )
711 revnum = self.revnum(self.heads[0])
711 revnum = self.revnum(self.heads[0])
712 if revnum < self.startrev:
712 if revnum < self.startrev:
713 raise error.Abort(
713 raise error.Abort(
714 _(b'svn: no revision found after start revision %d')
714 _(b'svn: no revision found after start revision %d')
715 % self.startrev
715 % self.startrev
716 )
716 )
717
717
718 return self.heads
718 return self.heads
719
719
720 def _getchanges(self, rev, full):
720 def _getchanges(self, rev, full):
721 (paths, parents) = self.paths[rev]
721 (paths, parents) = self.paths[rev]
722 copies = {}
722 copies = {}
723 if parents:
723 if parents:
724 files, self.removed, copies = self.expandpaths(rev, paths, parents)
724 files, self.removed, copies = self.expandpaths(rev, paths, parents)
725 if full or not parents:
725 if full or not parents:
726 # Perform a full checkout on roots
726 # Perform a full checkout on roots
727 uuid, module, revnum = revsplit(rev)
727 uuid, module, revnum = revsplit(rev)
728 entries = svn.client.ls(
728 entries = svn.client.ls(
729 self.baseurl + quote(module), optrev(revnum), True, self.ctx
729 self.baseurl + quote(module), optrev(revnum), True, self.ctx
730 )
730 )
731 files = [
731 files = [
732 n
732 n
733 for n, e in pycompat.iteritems(entries)
733 for n, e in pycompat.iteritems(entries)
734 if e.kind == svn.core.svn_node_file
734 if e.kind == svn.core.svn_node_file
735 ]
735 ]
736 self.removed = set()
736 self.removed = set()
737
737
738 files.sort()
738 files.sort()
739 files = pycompat.ziplist(files, [rev] * len(files))
739 files = pycompat.ziplist(files, [rev] * len(files))
740 return (files, copies)
740 return (files, copies)
741
741
742 def getchanges(self, rev, full):
742 def getchanges(self, rev, full):
743 # reuse cache from getchangedfiles
743 # reuse cache from getchangedfiles
744 if self._changescache[0] == rev and not full:
744 if self._changescache[0] == rev and not full:
745 (files, copies) = self._changescache[1]
745 (files, copies) = self._changescache[1]
746 else:
746 else:
747 (files, copies) = self._getchanges(rev, full)
747 (files, copies) = self._getchanges(rev, full)
748 # caller caches the result, so free it here to release memory
748 # caller caches the result, so free it here to release memory
749 del self.paths[rev]
749 del self.paths[rev]
750 return (files, copies, set())
750 return (files, copies, set())
751
751
752 def getchangedfiles(self, rev, i):
752 def getchangedfiles(self, rev, i):
753 # called from filemap - cache computed values for reuse in getchanges
753 # called from filemap - cache computed values for reuse in getchanges
754 (files, copies) = self._getchanges(rev, False)
754 (files, copies) = self._getchanges(rev, False)
755 self._changescache = (rev, (files, copies))
755 self._changescache = (rev, (files, copies))
756 return [f[0] for f in files]
756 return [f[0] for f in files]
757
757
758 def getcommit(self, rev):
758 def getcommit(self, rev):
759 if rev not in self.commits:
759 if rev not in self.commits:
760 uuid, module, revnum = revsplit(rev)
760 uuid, module, revnum = revsplit(rev)
761 self.module = module
761 self.module = module
762 self.reparent(module)
762 self.reparent(module)
763 # We assume that:
763 # We assume that:
764 # - requests for revisions after "stop" come from the
764 # - requests for revisions after "stop" come from the
765 # revision graph backward traversal. Cache all of them
765 # revision graph backward traversal. Cache all of them
766 # down to stop, they will be used eventually.
766 # down to stop, they will be used eventually.
767 # - requests for revisions before "stop" come to get
767 # - requests for revisions before "stop" come to get
768 # isolated branches parents. Just fetch what is needed.
768 # isolated branches parents. Just fetch what is needed.
769 stop = self.lastrevs.get(module, 0)
769 stop = self.lastrevs.get(module, 0)
770 if revnum < stop:
770 if revnum < stop:
771 stop = revnum + 1
771 stop = revnum + 1
772 self._fetch_revisions(revnum, stop)
772 self._fetch_revisions(revnum, stop)
773 if rev not in self.commits:
773 if rev not in self.commits:
774 raise error.Abort(_(b'svn: revision %s not found') % revnum)
774 raise error.Abort(_(b'svn: revision %s not found') % revnum)
775 revcommit = self.commits[rev]
775 revcommit = self.commits[rev]
776 # caller caches the result, so free it here to release memory
776 # caller caches the result, so free it here to release memory
777 del self.commits[rev]
777 del self.commits[rev]
778 return revcommit
778 return revcommit
779
779
780 def checkrevformat(self, revstr, mapname=b'splicemap'):
780 def checkrevformat(self, revstr, mapname=b'splicemap'):
781 """fails if revision format does not match the correct format"""
781 """fails if revision format does not match the correct format"""
782 if not re.match(
782 if not re.match(
783 br'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
783 br'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
784 br'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
784 br'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
785 br'{12,12}(.*)@[0-9]+$',
785 br'{12,12}(.*)@[0-9]+$',
786 revstr,
786 revstr,
787 ):
787 ):
788 raise error.Abort(
788 raise error.Abort(
789 _(b'%s entry %s is not a valid revision identifier')
789 _(b'%s entry %s is not a valid revision identifier')
790 % (mapname, revstr)
790 % (mapname, revstr)
791 )
791 )
792
792
793 def numcommits(self):
793 def numcommits(self):
794 return int(self.head.rsplit(b'@', 1)[1]) - self.startrev
794 return int(self.head.rsplit(b'@', 1)[1]) - self.startrev
795
795
796 def gettags(self):
796 def gettags(self):
797 tags = {}
797 tags = {}
798 if self.tags is None:
798 if self.tags is None:
799 return tags
799 return tags
800
800
801 # svn tags are just a convention, project branches left in a
801 # svn tags are just a convention, project branches left in a
802 # 'tags' directory. There is no other relationship than
802 # 'tags' directory. There is no other relationship than
803 # ancestry, which is expensive to discover and makes them hard
803 # ancestry, which is expensive to discover and makes them hard
804 # to update incrementally. Worse, past revisions may be
804 # to update incrementally. Worse, past revisions may be
805 # referenced by tags far away in the future, requiring a deep
805 # referenced by tags far away in the future, requiring a deep
806 # history traversal on every calculation. Current code
806 # history traversal on every calculation. Current code
807 # performs a single backward traversal, tracking moves within
807 # performs a single backward traversal, tracking moves within
808 # the tags directory (tag renaming) and recording a new tag
808 # the tags directory (tag renaming) and recording a new tag
809 # everytime a project is copied from outside the tags
809 # everytime a project is copied from outside the tags
810 # directory. It also lists deleted tags, this behaviour may
810 # directory. It also lists deleted tags, this behaviour may
811 # change in the future.
811 # change in the future.
812 pendings = []
812 pendings = []
813 tagspath = self.tags
813 tagspath = self.tags
814 start = svn.ra.get_latest_revnum(self.ra)
814 start = svn.ra.get_latest_revnum(self.ra)
815 stream = self._getlog([self.tags], start, self.startrev)
815 stream = self._getlog([self.tags], start, self.startrev)
816 try:
816 try:
817 for entry in stream:
817 for entry in stream:
818 origpaths, revnum, author, date, message = entry
818 origpaths, revnum, author, date, message = entry
819 if not origpaths:
819 if not origpaths:
820 origpaths = []
820 origpaths = []
821 copies = [
821 copies = [
822 (e.copyfrom_path, e.copyfrom_rev, p)
822 (e.copyfrom_path, e.copyfrom_rev, p)
823 for p, e in pycompat.iteritems(origpaths)
823 for p, e in pycompat.iteritems(origpaths)
824 if e.copyfrom_path
824 if e.copyfrom_path
825 ]
825 ]
826 # Apply moves/copies from more specific to general
826 # Apply moves/copies from more specific to general
827 copies.sort(reverse=True)
827 copies.sort(reverse=True)
828
828
829 srctagspath = tagspath
829 srctagspath = tagspath
830 if copies and copies[-1][2] == tagspath:
830 if copies and copies[-1][2] == tagspath:
831 # Track tags directory moves
831 # Track tags directory moves
832 srctagspath = copies.pop()[0]
832 srctagspath = copies.pop()[0]
833
833
834 for source, sourcerev, dest in copies:
834 for source, sourcerev, dest in copies:
835 if not dest.startswith(tagspath + b'/'):
835 if not dest.startswith(tagspath + b'/'):
836 continue
836 continue
837 for tag in pendings:
837 for tag in pendings:
838 if tag[0].startswith(dest):
838 if tag[0].startswith(dest):
839 tagpath = source + tag[0][len(dest) :]
839 tagpath = source + tag[0][len(dest) :]
840 tag[:2] = [tagpath, sourcerev]
840 tag[:2] = [tagpath, sourcerev]
841 break
841 break
842 else:
842 else:
843 pendings.append([source, sourcerev, dest])
843 pendings.append([source, sourcerev, dest])
844
844
845 # Filter out tags with children coming from different
845 # Filter out tags with children coming from different
846 # parts of the repository like:
846 # parts of the repository like:
847 # /tags/tag.1 (from /trunk:10)
847 # /tags/tag.1 (from /trunk:10)
848 # /tags/tag.1/foo (from /branches/foo:12)
848 # /tags/tag.1/foo (from /branches/foo:12)
849 # Here/tags/tag.1 discarded as well as its children.
849 # Here/tags/tag.1 discarded as well as its children.
850 # It happens with tools like cvs2svn. Such tags cannot
850 # It happens with tools like cvs2svn. Such tags cannot
851 # be represented in mercurial.
851 # be represented in mercurial.
852 addeds = {
852 addeds = {
853 p: e.copyfrom_path
853 p: e.copyfrom_path
854 for p, e in pycompat.iteritems(origpaths)
854 for p, e in pycompat.iteritems(origpaths)
855 if e.action == b'A' and e.copyfrom_path
855 if e.action == b'A' and e.copyfrom_path
856 }
856 }
857 badroots = set()
857 badroots = set()
858 for destroot in addeds:
858 for destroot in addeds:
859 for source, sourcerev, dest in pendings:
859 for source, sourcerev, dest in pendings:
860 if not dest.startswith(
860 if not dest.startswith(
861 destroot + b'/'
861 destroot + b'/'
862 ) or source.startswith(addeds[destroot] + b'/'):
862 ) or source.startswith(addeds[destroot] + b'/'):
863 continue
863 continue
864 badroots.add(destroot)
864 badroots.add(destroot)
865 break
865 break
866
866
867 for badroot in badroots:
867 for badroot in badroots:
868 pendings = [
868 pendings = [
869 p
869 p
870 for p in pendings
870 for p in pendings
871 if p[2] != badroot
871 if p[2] != badroot
872 and not p[2].startswith(badroot + b'/')
872 and not p[2].startswith(badroot + b'/')
873 ]
873 ]
874
874
875 # Tell tag renamings from tag creations
875 # Tell tag renamings from tag creations
876 renamings = []
876 renamings = []
877 for source, sourcerev, dest in pendings:
877 for source, sourcerev, dest in pendings:
878 tagname = dest.split(b'/')[-1]
878 tagname = dest.split(b'/')[-1]
879 if source.startswith(srctagspath):
879 if source.startswith(srctagspath):
880 renamings.append([source, sourcerev, tagname])
880 renamings.append([source, sourcerev, tagname])
881 continue
881 continue
882 if tagname in tags:
882 if tagname in tags:
883 # Keep the latest tag value
883 # Keep the latest tag value
884 continue
884 continue
885 # From revision may be fake, get one with changes
885 # From revision may be fake, get one with changes
886 try:
886 try:
887 tagid = self.latest(source, sourcerev)
887 tagid = self.latest(source, sourcerev)
888 if tagid and tagname not in tags:
888 if tagid and tagname not in tags:
889 tags[tagname] = tagid
889 tags[tagname] = tagid
890 except SvnPathNotFound:
890 except SvnPathNotFound:
891 # It happens when we are following directories
891 # It happens when we are following directories
892 # we assumed were copied with their parents
892 # we assumed were copied with their parents
893 # but were really created in the tag
893 # but were really created in the tag
894 # directory.
894 # directory.
895 pass
895 pass
896 pendings = renamings
896 pendings = renamings
897 tagspath = srctagspath
897 tagspath = srctagspath
898 finally:
898 finally:
899 stream.close()
899 stream.close()
900 return tags
900 return tags
901
901
902 def converted(self, rev, destrev):
902 def converted(self, rev, destrev):
903 if not self.wc:
903 if not self.wc:
904 return
904 return
905 if self.convertfp is None:
905 if self.convertfp is None:
906 self.convertfp = open(
906 self.convertfp = open(
907 os.path.join(self.wc, b'.svn', b'hg-shamap'), b'ab'
907 os.path.join(self.wc, b'.svn', b'hg-shamap'), b'ab'
908 )
908 )
909 self.convertfp.write(
909 self.convertfp.write(
910 util.tonativeeol(b'%s %d\n' % (destrev, self.revnum(rev)))
910 util.tonativeeol(b'%s %d\n' % (destrev, self.revnum(rev)))
911 )
911 )
912 self.convertfp.flush()
912 self.convertfp.flush()
913
913
914 def revid(self, revnum, module=None):
914 def revid(self, revnum, module=None):
915 return b'svn:%s%s@%d' % (self.uuid, module or self.module, revnum)
915 return b'svn:%s%s@%d' % (self.uuid, module or self.module, revnum)
916
916
917 def revnum(self, rev):
917 def revnum(self, rev):
918 return int(rev.split(b'@')[-1])
918 return int(rev.split(b'@')[-1])
919
919
920 def latest(self, path, stop=None):
920 def latest(self, path, stop=None):
921 """Find the latest revid affecting path, up to stop revision
921 """Find the latest revid affecting path, up to stop revision
922 number. If stop is None, default to repository latest
922 number. If stop is None, default to repository latest
923 revision. It may return a revision in a different module,
923 revision. It may return a revision in a different module,
924 since a branch may be moved without a change being
924 since a branch may be moved without a change being
925 reported. Return None if computed module does not belong to
925 reported. Return None if computed module does not belong to
926 rootmodule subtree.
926 rootmodule subtree.
927 """
927 """
928
928
929 def findchanges(path, start, stop=None):
929 def findchanges(path, start, stop=None):
930 stream = self._getlog([path], start, stop or 1)
930 stream = self._getlog([path], start, stop or 1)
931 try:
931 try:
932 for entry in stream:
932 for entry in stream:
933 paths, revnum, author, date, message = entry
933 paths, revnum, author, date, message = entry
934 if stop is None and paths:
934 if stop is None and paths:
935 # We do not know the latest changed revision,
935 # We do not know the latest changed revision,
936 # keep the first one with changed paths.
936 # keep the first one with changed paths.
937 break
937 break
938 if stop is not None and revnum <= stop:
938 if stop is not None and revnum <= stop:
939 break
939 break
940
940
941 for p in paths:
941 for p in paths:
942 if not path.startswith(p) or not paths[p].copyfrom_path:
942 if not path.startswith(p) or not paths[p].copyfrom_path:
943 continue
943 continue
944 newpath = paths[p].copyfrom_path + path[len(p) :]
944 newpath = paths[p].copyfrom_path + path[len(p) :]
945 self.ui.debug(
945 self.ui.debug(
946 b"branch renamed from %s to %s at %d\n"
946 b"branch renamed from %s to %s at %d\n"
947 % (path, newpath, revnum)
947 % (path, newpath, revnum)
948 )
948 )
949 path = newpath
949 path = newpath
950 break
950 break
951 if not paths:
951 if not paths:
952 revnum = None
952 revnum = None
953 return revnum, path
953 return revnum, path
954 finally:
954 finally:
955 stream.close()
955 stream.close()
956
956
957 if not path.startswith(self.rootmodule):
957 if not path.startswith(self.rootmodule):
958 # Requests on foreign branches may be forbidden at server level
958 # Requests on foreign branches may be forbidden at server level
959 self.ui.debug(b'ignoring foreign branch %r\n' % path)
959 self.ui.debug(b'ignoring foreign branch %r\n' % path)
960 return None
960 return None
961
961
962 if stop is None:
962 if stop is None:
963 stop = svn.ra.get_latest_revnum(self.ra)
963 stop = svn.ra.get_latest_revnum(self.ra)
964 try:
964 try:
965 prevmodule = self.reparent(b'')
965 prevmodule = self.reparent(b'')
966 dirent = svn.ra.stat(self.ra, path.strip(b'/'), stop)
966 dirent = svn.ra.stat(self.ra, path.strip(b'/'), stop)
967 self.reparent(prevmodule)
967 self.reparent(prevmodule)
968 except svn.core.SubversionException:
968 except svn.core.SubversionException:
969 dirent = None
969 dirent = None
970 if not dirent:
970 if not dirent:
971 raise SvnPathNotFound(
971 raise SvnPathNotFound(
972 _(b'%s not found up to revision %d') % (path, stop)
972 _(b'%s not found up to revision %d') % (path, stop)
973 )
973 )
974
974
975 # stat() gives us the previous revision on this line of
975 # stat() gives us the previous revision on this line of
976 # development, but it might be in *another module*. Fetch the
976 # development, but it might be in *another module*. Fetch the
977 # log and detect renames down to the latest revision.
977 # log and detect renames down to the latest revision.
978 revnum, realpath = findchanges(path, stop, dirent.created_rev)
978 revnum, realpath = findchanges(path, stop, dirent.created_rev)
979 if revnum is None:
979 if revnum is None:
980 # Tools like svnsync can create empty revision, when
980 # Tools like svnsync can create empty revision, when
981 # synchronizing only a subtree for instance. These empty
981 # synchronizing only a subtree for instance. These empty
982 # revisions created_rev still have their original values
982 # revisions created_rev still have their original values
983 # despite all changes having disappeared and can be
983 # despite all changes having disappeared and can be
984 # returned by ra.stat(), at least when stating the root
984 # returned by ra.stat(), at least when stating the root
985 # module. In that case, do not trust created_rev and scan
985 # module. In that case, do not trust created_rev and scan
986 # the whole history.
986 # the whole history.
987 revnum, realpath = findchanges(path, stop)
987 revnum, realpath = findchanges(path, stop)
988 if revnum is None:
988 if revnum is None:
989 self.ui.debug(b'ignoring empty branch %r\n' % realpath)
989 self.ui.debug(b'ignoring empty branch %r\n' % realpath)
990 return None
990 return None
991
991
992 if not realpath.startswith(self.rootmodule):
992 if not realpath.startswith(self.rootmodule):
993 self.ui.debug(b'ignoring foreign branch %r\n' % realpath)
993 self.ui.debug(b'ignoring foreign branch %r\n' % realpath)
994 return None
994 return None
995 return self.revid(revnum, realpath)
995 return self.revid(revnum, realpath)
996
996
997 def reparent(self, module):
997 def reparent(self, module):
998 """Reparent the svn transport and return the previous parent."""
998 """Reparent the svn transport and return the previous parent."""
999 if self.prevmodule == module:
999 if self.prevmodule == module:
1000 return module
1000 return module
1001 svnurl = self.baseurl + quote(module)
1001 svnurl = self.baseurl + quote(module)
1002 prevmodule = self.prevmodule
1002 prevmodule = self.prevmodule
1003 if prevmodule is None:
1003 if prevmodule is None:
1004 prevmodule = b''
1004 prevmodule = b''
1005 self.ui.debug(b"reparent to %s\n" % svnurl)
1005 self.ui.debug(b"reparent to %s\n" % svnurl)
1006 svn.ra.reparent(self.ra, svnurl)
1006 svn.ra.reparent(self.ra, svnurl)
1007 self.prevmodule = module
1007 self.prevmodule = module
1008 return prevmodule
1008 return prevmodule
1009
1009
1010 def expandpaths(self, rev, paths, parents):
1010 def expandpaths(self, rev, paths, parents):
1011 changed, removed = set(), set()
1011 changed, removed = set(), set()
1012 copies = {}
1012 copies = {}
1013
1013
1014 new_module, revnum = revsplit(rev)[1:]
1014 new_module, revnum = revsplit(rev)[1:]
1015 if new_module != self.module:
1015 if new_module != self.module:
1016 self.module = new_module
1016 self.module = new_module
1017 self.reparent(self.module)
1017 self.reparent(self.module)
1018
1018
1019 progress = self.ui.makeprogress(
1019 progress = self.ui.makeprogress(
1020 _(b'scanning paths'), unit=_(b'paths'), total=len(paths)
1020 _(b'scanning paths'), unit=_(b'paths'), total=len(paths)
1021 )
1021 )
1022 for i, (path, ent) in enumerate(paths):
1022 for i, (path, ent) in enumerate(paths):
1023 progress.update(i, item=path)
1023 progress.update(i, item=path)
1024 entrypath = self.getrelpath(path)
1024 entrypath = self.getrelpath(path)
1025
1025
1026 kind = self._checkpath(entrypath, revnum)
1026 kind = self._checkpath(entrypath, revnum)
1027 if kind == svn.core.svn_node_file:
1027 if kind == svn.core.svn_node_file:
1028 changed.add(self.recode(entrypath))
1028 changed.add(self.recode(entrypath))
1029 if not ent.copyfrom_path or not parents:
1029 if not ent.copyfrom_path or not parents:
1030 continue
1030 continue
1031 # Copy sources not in parent revisions cannot be
1031 # Copy sources not in parent revisions cannot be
1032 # represented, ignore their origin for now
1032 # represented, ignore their origin for now
1033 pmodule, prevnum = revsplit(parents[0])[1:]
1033 pmodule, prevnum = revsplit(parents[0])[1:]
1034 if ent.copyfrom_rev < prevnum:
1034 if ent.copyfrom_rev < prevnum:
1035 continue
1035 continue
1036 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
1036 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
1037 if not copyfrom_path:
1037 if not copyfrom_path:
1038 continue
1038 continue
1039 self.ui.debug(
1039 self.ui.debug(
1040 b"copied to %s from %s@%d\n"
1040 b"copied to %s from %s@%d\n"
1041 % (entrypath, copyfrom_path, ent.copyfrom_rev)
1041 % (entrypath, copyfrom_path, ent.copyfrom_rev)
1042 )
1042 )
1043 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
1043 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
1044 elif kind == 0: # gone, but had better be a deleted *file*
1044 elif kind == 0: # gone, but had better be a deleted *file*
1045 self.ui.debug(b"gone from %d\n" % ent.copyfrom_rev)
1045 self.ui.debug(b"gone from %d\n" % ent.copyfrom_rev)
1046 pmodule, prevnum = revsplit(parents[0])[1:]
1046 pmodule, prevnum = revsplit(parents[0])[1:]
1047 parentpath = pmodule + b"/" + entrypath
1047 parentpath = pmodule + b"/" + entrypath
1048 fromkind = self._checkpath(entrypath, prevnum, pmodule)
1048 fromkind = self._checkpath(entrypath, prevnum, pmodule)
1049
1049
1050 if fromkind == svn.core.svn_node_file:
1050 if fromkind == svn.core.svn_node_file:
1051 removed.add(self.recode(entrypath))
1051 removed.add(self.recode(entrypath))
1052 elif fromkind == svn.core.svn_node_dir:
1052 elif fromkind == svn.core.svn_node_dir:
1053 oroot = parentpath.strip(b'/')
1053 oroot = parentpath.strip(b'/')
1054 nroot = path.strip(b'/')
1054 nroot = path.strip(b'/')
1055 children = self._iterfiles(oroot, prevnum)
1055 children = self._iterfiles(oroot, prevnum)
1056 for childpath in children:
1056 for childpath in children:
1057 childpath = childpath.replace(oroot, nroot)
1057 childpath = childpath.replace(oroot, nroot)
1058 childpath = self.getrelpath(b"/" + childpath, pmodule)
1058 childpath = self.getrelpath(b"/" + childpath, pmodule)
1059 if childpath:
1059 if childpath:
1060 removed.add(self.recode(childpath))
1060 removed.add(self.recode(childpath))
1061 else:
1061 else:
1062 self.ui.debug(
1062 self.ui.debug(
1063 b'unknown path in revision %d: %s\n' % (revnum, path)
1063 b'unknown path in revision %d: %s\n' % (revnum, path)
1064 )
1064 )
1065 elif kind == svn.core.svn_node_dir:
1065 elif kind == svn.core.svn_node_dir:
1066 if ent.action == b'M':
1066 if ent.action == b'M':
1067 # If the directory just had a prop change,
1067 # If the directory just had a prop change,
1068 # then we shouldn't need to look for its children.
1068 # then we shouldn't need to look for its children.
1069 continue
1069 continue
1070 if ent.action == b'R' and parents:
1070 if ent.action == b'R' and parents:
1071 # If a directory is replacing a file, mark the previous
1071 # If a directory is replacing a file, mark the previous
1072 # file as deleted
1072 # file as deleted
1073 pmodule, prevnum = revsplit(parents[0])[1:]
1073 pmodule, prevnum = revsplit(parents[0])[1:]
1074 pkind = self._checkpath(entrypath, prevnum, pmodule)
1074 pkind = self._checkpath(entrypath, prevnum, pmodule)
1075 if pkind == svn.core.svn_node_file:
1075 if pkind == svn.core.svn_node_file:
1076 removed.add(self.recode(entrypath))
1076 removed.add(self.recode(entrypath))
1077 elif pkind == svn.core.svn_node_dir:
1077 elif pkind == svn.core.svn_node_dir:
1078 # We do not know what files were kept or removed,
1078 # We do not know what files were kept or removed,
1079 # mark them all as changed.
1079 # mark them all as changed.
1080 for childpath in self._iterfiles(pmodule, prevnum):
1080 for childpath in self._iterfiles(pmodule, prevnum):
1081 childpath = self.getrelpath(b"/" + childpath)
1081 childpath = self.getrelpath(b"/" + childpath)
1082 if childpath:
1082 if childpath:
1083 changed.add(self.recode(childpath))
1083 changed.add(self.recode(childpath))
1084
1084
1085 for childpath in self._iterfiles(path, revnum):
1085 for childpath in self._iterfiles(path, revnum):
1086 childpath = self.getrelpath(b"/" + childpath)
1086 childpath = self.getrelpath(b"/" + childpath)
1087 if childpath:
1087 if childpath:
1088 changed.add(self.recode(childpath))
1088 changed.add(self.recode(childpath))
1089
1089
1090 # Handle directory copies
1090 # Handle directory copies
1091 if not ent.copyfrom_path or not parents:
1091 if not ent.copyfrom_path or not parents:
1092 continue
1092 continue
1093 # Copy sources not in parent revisions cannot be
1093 # Copy sources not in parent revisions cannot be
1094 # represented, ignore their origin for now
1094 # represented, ignore their origin for now
1095 pmodule, prevnum = revsplit(parents[0])[1:]
1095 pmodule, prevnum = revsplit(parents[0])[1:]
1096 if ent.copyfrom_rev < prevnum:
1096 if ent.copyfrom_rev < prevnum:
1097 continue
1097 continue
1098 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
1098 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
1099 if not copyfrompath:
1099 if not copyfrompath:
1100 continue
1100 continue
1101 self.ui.debug(
1101 self.ui.debug(
1102 b"mark %s came from %s:%d\n"
1102 b"mark %s came from %s:%d\n"
1103 % (path, copyfrompath, ent.copyfrom_rev)
1103 % (path, copyfrompath, ent.copyfrom_rev)
1104 )
1104 )
1105 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
1105 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
1106 for childpath in children:
1106 for childpath in children:
1107 childpath = self.getrelpath(b"/" + childpath, pmodule)
1107 childpath = self.getrelpath(b"/" + childpath, pmodule)
1108 if not childpath:
1108 if not childpath:
1109 continue
1109 continue
1110 copytopath = path + childpath[len(copyfrompath) :]
1110 copytopath = path + childpath[len(copyfrompath) :]
1111 copytopath = self.getrelpath(copytopath)
1111 copytopath = self.getrelpath(copytopath)
1112 copies[self.recode(copytopath)] = self.recode(childpath)
1112 copies[self.recode(copytopath)] = self.recode(childpath)
1113
1113
1114 progress.complete()
1114 progress.complete()
1115 changed.update(removed)
1115 changed.update(removed)
1116 return (list(changed), removed, copies)
1116 return (list(changed), removed, copies)
1117
1117
1118 def _fetch_revisions(self, from_revnum, to_revnum):
1118 def _fetch_revisions(self, from_revnum, to_revnum):
1119 if from_revnum < to_revnum:
1119 if from_revnum < to_revnum:
1120 from_revnum, to_revnum = to_revnum, from_revnum
1120 from_revnum, to_revnum = to_revnum, from_revnum
1121
1121
1122 self.child_cset = None
1122 self.child_cset = None
1123
1123
1124 def parselogentry(orig_paths, revnum, author, date, message):
1124 def parselogentry(orig_paths, revnum, author, date, message):
1125 """Return the parsed commit object or None, and True if
1125 """Return the parsed commit object or None, and True if
1126 the revision is a branch root.
1126 the revision is a branch root.
1127 """
1127 """
1128 self.ui.debug(
1128 self.ui.debug(
1129 b"parsing revision %d (%d changes)\n"
1129 b"parsing revision %d (%d changes)\n"
1130 % (revnum, len(orig_paths))
1130 % (revnum, len(orig_paths))
1131 )
1131 )
1132
1132
1133 branched = False
1133 branched = False
1134 rev = self.revid(revnum)
1134 rev = self.revid(revnum)
1135 # branch log might return entries for a parent we already have
1135 # branch log might return entries for a parent we already have
1136
1136
1137 if rev in self.commits or revnum < to_revnum:
1137 if rev in self.commits or revnum < to_revnum:
1138 return None, branched
1138 return None, branched
1139
1139
1140 parents = []
1140 parents = []
1141 # check whether this revision is the start of a branch or part
1141 # check whether this revision is the start of a branch or part
1142 # of a branch renaming
1142 # of a branch renaming
1143 orig_paths = sorted(pycompat.iteritems(orig_paths))
1143 orig_paths = sorted(pycompat.iteritems(orig_paths))
1144 root_paths = [
1144 root_paths = [
1145 (p, e) for p, e in orig_paths if self.module.startswith(p)
1145 (p, e) for p, e in orig_paths if self.module.startswith(p)
1146 ]
1146 ]
1147 if root_paths:
1147 if root_paths:
1148 path, ent = root_paths[-1]
1148 path, ent = root_paths[-1]
1149 if ent.copyfrom_path:
1149 if ent.copyfrom_path:
1150 branched = True
1150 branched = True
1151 newpath = ent.copyfrom_path + self.module[len(path) :]
1151 newpath = ent.copyfrom_path + self.module[len(path) :]
1152 # ent.copyfrom_rev may not be the actual last revision
1152 # ent.copyfrom_rev may not be the actual last revision
1153 previd = self.latest(newpath, ent.copyfrom_rev)
1153 previd = self.latest(newpath, ent.copyfrom_rev)
1154 if previd is not None:
1154 if previd is not None:
1155 prevmodule, prevnum = revsplit(previd)[1:]
1155 prevmodule, prevnum = revsplit(previd)[1:]
1156 if prevnum >= self.startrev:
1156 if prevnum >= self.startrev:
1157 parents = [previd]
1157 parents = [previd]
1158 self.ui.note(
1158 self.ui.note(
1159 _(b'found parent of branch %s at %d: %s\n')
1159 _(b'found parent of branch %s at %d: %s\n')
1160 % (self.module, prevnum, prevmodule)
1160 % (self.module, prevnum, prevmodule)
1161 )
1161 )
1162 else:
1162 else:
1163 self.ui.debug(b"no copyfrom path, don't know what to do.\n")
1163 self.ui.debug(b"no copyfrom path, don't know what to do.\n")
1164
1164
1165 paths = []
1165 paths = []
1166 # filter out unrelated paths
1166 # filter out unrelated paths
1167 for path, ent in orig_paths:
1167 for path, ent in orig_paths:
1168 if self.getrelpath(path) is None:
1168 if self.getrelpath(path) is None:
1169 continue
1169 continue
1170 paths.append((path, ent))
1170 paths.append((path, ent))
1171
1171
1172 date = parsesvndate(date)
1172 date = parsesvndate(date)
1173 if self.ui.configbool(b'convert', b'localtimezone'):
1173 if self.ui.configbool(b'convert', b'localtimezone'):
1174 date = makedatetimestamp(date[0])
1174 date = makedatetimestamp(date[0])
1175
1175
1176 if message:
1176 if message:
1177 log = self.recode(message)
1177 log = self.recode(message)
1178 else:
1178 else:
1179 log = b''
1179 log = b''
1180
1180
1181 if author:
1181 if author:
1182 author = self.recode(author)
1182 author = self.recode(author)
1183 else:
1183 else:
1184 author = b''
1184 author = b''
1185
1185
1186 try:
1186 try:
1187 branch = self.module.split(b"/")[-1]
1187 branch = self.module.split(b"/")[-1]
1188 if branch == self.trunkname:
1188 if branch == self.trunkname:
1189 branch = None
1189 branch = None
1190 except IndexError:
1190 except IndexError:
1191 branch = None
1191 branch = None
1192
1192
1193 cset = commit(
1193 cset = commit(
1194 author=author,
1194 author=author,
1195 date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
1195 date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
1196 desc=log,
1196 desc=log,
1197 parents=parents,
1197 parents=parents,
1198 branch=branch,
1198 branch=branch,
1199 rev=rev,
1199 rev=rev,
1200 )
1200 )
1201
1201
1202 self.commits[rev] = cset
1202 self.commits[rev] = cset
1203 # The parents list is *shared* among self.paths and the
1203 # The parents list is *shared* among self.paths and the
1204 # commit object. Both will be updated below.
1204 # commit object. Both will be updated below.
1205 self.paths[rev] = (paths, cset.parents)
1205 self.paths[rev] = (paths, cset.parents)
1206 if self.child_cset and not self.child_cset.parents:
1206 if self.child_cset and not self.child_cset.parents:
1207 self.child_cset.parents[:] = [rev]
1207 self.child_cset.parents[:] = [rev]
1208 self.child_cset = cset
1208 self.child_cset = cset
1209 return cset, branched
1209 return cset, branched
1210
1210
1211 self.ui.note(
1211 self.ui.note(
1212 _(b'fetching revision log for "%s" from %d to %d\n')
1212 _(b'fetching revision log for "%s" from %d to %d\n')
1213 % (self.module, from_revnum, to_revnum)
1213 % (self.module, from_revnum, to_revnum)
1214 )
1214 )
1215
1215
1216 try:
1216 try:
1217 firstcset = None
1217 firstcset = None
1218 lastonbranch = False
1218 lastonbranch = False
1219 stream = self._getlog([self.module], from_revnum, to_revnum)
1219 stream = self._getlog([self.module], from_revnum, to_revnum)
1220 try:
1220 try:
1221 for entry in stream:
1221 for entry in stream:
1222 paths, revnum, author, date, message = entry
1222 paths, revnum, author, date, message = entry
1223 if revnum < self.startrev:
1223 if revnum < self.startrev:
1224 lastonbranch = True
1224 lastonbranch = True
1225 break
1225 break
1226 if not paths:
1226 if not paths:
1227 self.ui.debug(b'revision %d has no entries\n' % revnum)
1227 self.ui.debug(b'revision %d has no entries\n' % revnum)
1228 # If we ever leave the loop on an empty
1228 # If we ever leave the loop on an empty
1229 # revision, do not try to get a parent branch
1229 # revision, do not try to get a parent branch
1230 lastonbranch = lastonbranch or revnum == 0
1230 lastonbranch = lastonbranch or revnum == 0
1231 continue
1231 continue
1232 cset, lastonbranch = parselogentry(
1232 cset, lastonbranch = parselogentry(
1233 paths, revnum, author, date, message
1233 paths, revnum, author, date, message
1234 )
1234 )
1235 if cset:
1235 if cset:
1236 firstcset = cset
1236 firstcset = cset
1237 if lastonbranch:
1237 if lastonbranch:
1238 break
1238 break
1239 finally:
1239 finally:
1240 stream.close()
1240 stream.close()
1241
1241
1242 if not lastonbranch and firstcset and not firstcset.parents:
1242 if not lastonbranch and firstcset and not firstcset.parents:
1243 # The first revision of the sequence (the last fetched one)
1243 # The first revision of the sequence (the last fetched one)
1244 # has invalid parents if not a branch root. Find the parent
1244 # has invalid parents if not a branch root. Find the parent
1245 # revision now, if any.
1245 # revision now, if any.
1246 try:
1246 try:
1247 firstrevnum = self.revnum(firstcset.rev)
1247 firstrevnum = self.revnum(firstcset.rev)
1248 if firstrevnum > 1:
1248 if firstrevnum > 1:
1249 latest = self.latest(self.module, firstrevnum - 1)
1249 latest = self.latest(self.module, firstrevnum - 1)
1250 if latest:
1250 if latest:
1251 firstcset.parents.append(latest)
1251 firstcset.parents.append(latest)
1252 except SvnPathNotFound:
1252 except SvnPathNotFound:
1253 pass
1253 pass
1254 except svn.core.SubversionException as xxx_todo_changeme:
1254 except svn.core.SubversionException as xxx_todo_changeme:
1255 (inst, num) = xxx_todo_changeme.args
1255 (inst, num) = xxx_todo_changeme.args
1256 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
1256 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
1257 raise error.Abort(
1257 raise error.Abort(
1258 _(b'svn: branch has no revision %s') % to_revnum
1258 _(b'svn: branch has no revision %s') % to_revnum
1259 )
1259 )
1260 raise
1260 raise
1261
1261
1262 def getfile(self, file, rev):
1262 def getfile(self, file, rev):
1263 # TODO: ra.get_file transmits the whole file instead of diffs.
1263 # TODO: ra.get_file transmits the whole file instead of diffs.
1264 if file in self.removed:
1264 if file in self.removed:
1265 return None, None
1265 return None, None
1266 try:
1266 try:
1267 new_module, revnum = revsplit(rev)[1:]
1267 new_module, revnum = revsplit(rev)[1:]
1268 if self.module != new_module:
1268 if self.module != new_module:
1269 self.module = new_module
1269 self.module = new_module
1270 self.reparent(self.module)
1270 self.reparent(self.module)
1271 io = stringio()
1271 io = stringio()
1272 info = svn.ra.get_file(self.ra, file, revnum, io)
1272 info = svn.ra.get_file(self.ra, file, revnum, io)
1273 data = io.getvalue()
1273 data = io.getvalue()
1274 # ra.get_file() seems to keep a reference on the input buffer
1274 # ra.get_file() seems to keep a reference on the input buffer
1275 # preventing collection. Release it explicitly.
1275 # preventing collection. Release it explicitly.
1276 io.close()
1276 io.close()
1277 if isinstance(info, list):
1277 if isinstance(info, list):
1278 info = info[-1]
1278 info = info[-1]
1279 mode = (b"svn:executable" in info) and b'x' or b''
1279 mode = (b"svn:executable" in info) and b'x' or b''
1280 mode = (b"svn:special" in info) and b'l' or mode
1280 mode = (b"svn:special" in info) and b'l' or mode
1281 except svn.core.SubversionException as e:
1281 except svn.core.SubversionException as e:
1282 notfound = (
1282 notfound = (
1283 svn.core.SVN_ERR_FS_NOT_FOUND,
1283 svn.core.SVN_ERR_FS_NOT_FOUND,
1284 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
1284 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
1285 )
1285 )
1286 if e.apr_err in notfound: # File not found
1286 if e.apr_err in notfound: # File not found
1287 return None, None
1287 return None, None
1288 raise
1288 raise
1289 if mode == b'l':
1289 if mode == b'l':
1290 link_prefix = b"link "
1290 link_prefix = b"link "
1291 if data.startswith(link_prefix):
1291 if data.startswith(link_prefix):
1292 data = data[len(link_prefix) :]
1292 data = data[len(link_prefix) :]
1293 return data, mode
1293 return data, mode
1294
1294
1295 def _iterfiles(self, path, revnum):
1295 def _iterfiles(self, path, revnum):
1296 """Enumerate all files in path at revnum, recursively."""
1296 """Enumerate all files in path at revnum, recursively."""
1297 path = path.strip(b'/')
1297 path = path.strip(b'/')
1298 pool = svn.core.Pool()
1298 pool = svn.core.Pool()
1299 rpath = b'/'.join([self.baseurl, quote(path)]).strip(b'/')
1299 rpath = b'/'.join([self.baseurl, quote(path)]).strip(b'/')
1300 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1300 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1301 if path:
1301 if path:
1302 path += b'/'
1302 path += b'/'
1303 return (
1303 return (
1304 (path + p)
1304 (path + p)
1305 for p, e in pycompat.iteritems(entries)
1305 for p, e in pycompat.iteritems(entries)
1306 if e.kind == svn.core.svn_node_file
1306 if e.kind == svn.core.svn_node_file
1307 )
1307 )
1308
1308
1309 def getrelpath(self, path, module=None):
1309 def getrelpath(self, path, module=None):
1310 if module is None:
1310 if module is None:
1311 module = self.module
1311 module = self.module
1312 # Given the repository url of this wc, say
1312 # Given the repository url of this wc, say
1313 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1313 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1314 # extract the "entry" portion (a relative path) from what
1314 # extract the "entry" portion (a relative path) from what
1315 # svn log --xml says, i.e.
1315 # svn log --xml says, i.e.
1316 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1316 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1317 # that is to say "tests/PloneTestCase.py"
1317 # that is to say "tests/PloneTestCase.py"
1318 if path.startswith(module):
1318 if path.startswith(module):
1319 relative = path.rstrip(b'/')[len(module) :]
1319 relative = path.rstrip(b'/')[len(module) :]
1320 if relative.startswith(b'/'):
1320 if relative.startswith(b'/'):
1321 return relative[1:]
1321 return relative[1:]
1322 elif relative == b'':
1322 elif relative == b'':
1323 return relative
1323 return relative
1324
1324
1325 # The path is outside our tracked tree...
1325 # The path is outside our tracked tree...
1326 self.ui.debug(
1326 self.ui.debug(
1327 b'%r is not under %r, ignoring\n'
1327 b'%r is not under %r, ignoring\n'
1328 % (pycompat.bytestr(path), pycompat.bytestr(module))
1328 % (pycompat.bytestr(path), pycompat.bytestr(module))
1329 )
1329 )
1330 return None
1330 return None
1331
1331
1332 def _checkpath(self, path, revnum, module=None):
1332 def _checkpath(self, path, revnum, module=None):
1333 if module is not None:
1333 if module is not None:
1334 prevmodule = self.reparent(b'')
1334 prevmodule = self.reparent(b'')
1335 path = module + b'/' + path
1335 path = module + b'/' + path
1336 try:
1336 try:
1337 # ra.check_path does not like leading slashes very much, it leads
1337 # ra.check_path does not like leading slashes very much, it leads
1338 # to PROPFIND subversion errors
1338 # to PROPFIND subversion errors
1339 return svn.ra.check_path(self.ra, path.strip(b'/'), revnum)
1339 return svn.ra.check_path(self.ra, path.strip(b'/'), revnum)
1340 finally:
1340 finally:
1341 if module is not None:
1341 if module is not None:
1342 self.reparent(prevmodule)
1342 self.reparent(prevmodule)
1343
1343
1344 def _getlog(
1344 def _getlog(
1345 self,
1345 self,
1346 paths,
1346 paths,
1347 start,
1347 start,
1348 end,
1348 end,
1349 limit=0,
1349 limit=0,
1350 discover_changed_paths=True,
1350 discover_changed_paths=True,
1351 strict_node_history=False,
1351 strict_node_history=False,
1352 ):
1352 ):
1353 # Normalize path names, svn >= 1.5 only wants paths relative to
1353 # Normalize path names, svn >= 1.5 only wants paths relative to
1354 # supplied URL
1354 # supplied URL
1355 relpaths = []
1355 relpaths = []
1356 for p in paths:
1356 for p in paths:
1357 if not p.startswith(b'/'):
1357 if not p.startswith(b'/'):
1358 p = self.module + b'/' + p
1358 p = self.module + b'/' + p
1359 relpaths.append(p.strip(b'/'))
1359 relpaths.append(p.strip(b'/'))
1360 args = [
1360 args = [
1361 self.baseurl,
1361 self.baseurl,
1362 relpaths,
1362 relpaths,
1363 start,
1363 start,
1364 end,
1364 end,
1365 limit,
1365 limit,
1366 discover_changed_paths,
1366 discover_changed_paths,
1367 strict_node_history,
1367 strict_node_history,
1368 ]
1368 ]
1369 # developer config: convert.svn.debugsvnlog
1369 # developer config: convert.svn.debugsvnlog
1370 if not self.ui.configbool(b'convert', b'svn.debugsvnlog'):
1370 if not self.ui.configbool(b'convert', b'svn.debugsvnlog'):
1371 return directlogstream(*args)
1371 return directlogstream(*args)
1372 arg = encodeargs(args)
1372 arg = encodeargs(args)
1373 hgexe = procutil.hgexecutable()
1373 hgexe = procutil.hgexecutable()
1374 cmd = b'%s debugsvnlog' % procutil.shellquote(hgexe)
1374 cmd = b'%s debugsvnlog' % procutil.shellquote(hgexe)
1375 stdin, stdout = procutil.popen2(cmd)
1375 stdin, stdout = procutil.popen2(cmd)
1376 stdin.write(arg)
1376 stdin.write(arg)
1377 try:
1377 try:
1378 stdin.close()
1378 stdin.close()
1379 except IOError:
1379 except IOError:
1380 raise error.Abort(
1380 raise error.Abort(
1381 _(
1381 _(
1382 b'Mercurial failed to run itself, check'
1382 b'Mercurial failed to run itself, check'
1383 b' hg executable is in PATH'
1383 b' hg executable is in PATH'
1384 )
1384 )
1385 )
1385 )
1386 return logstream(stdout)
1386 return logstream(stdout)
1387
1387
1388
1388
1389 pre_revprop_change_template = b'''#!/bin/sh
1389 pre_revprop_change_template = b'''#!/bin/sh
1390
1390
1391 REPOS="$1"
1391 REPOS="$1"
1392 REV="$2"
1392 REV="$2"
1393 USER="$3"
1393 USER="$3"
1394 PROPNAME="$4"
1394 PROPNAME="$4"
1395 ACTION="$5"
1395 ACTION="$5"
1396
1396
1397 %(rules)s
1397 %(rules)s
1398
1398
1399 echo "Changing prohibited revision property" >&2
1399 echo "Changing prohibited revision property" >&2
1400 exit 1
1400 exit 1
1401 '''
1401 '''
1402
1402
1403
1403
1404 def gen_pre_revprop_change_hook(prop_actions_allowed):
1404 def gen_pre_revprop_change_hook(prop_actions_allowed):
1405 rules = []
1405 rules = []
1406 for action, propname in prop_actions_allowed:
1406 for action, propname in prop_actions_allowed:
1407 rules.append(
1407 rules.append(
1408 (
1408 (
1409 b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
1409 b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
1410 b'then exit 0; fi'
1410 b'then exit 0; fi'
1411 )
1411 )
1412 % (action, propname)
1412 % (action, propname)
1413 )
1413 )
1414 return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
1414 return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
1415
1415
1416
1416
1417 class svn_sink(converter_sink, commandline):
1417 class svn_sink(converter_sink, commandline):
1418 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1418 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1419 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1419 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1420
1420
1421 def prerun(self):
1421 def prerun(self):
1422 if self.wc:
1422 if self.wc:
1423 os.chdir(self.wc)
1423 os.chdir(self.wc)
1424
1424
1425 def postrun(self):
1425 def postrun(self):
1426 if self.wc:
1426 if self.wc:
1427 os.chdir(self.cwd)
1427 os.chdir(self.cwd)
1428
1428
1429 def join(self, name):
1429 def join(self, name):
1430 return os.path.join(self.wc, b'.svn', name)
1430 return os.path.join(self.wc, b'.svn', name)
1431
1431
1432 def revmapfile(self):
1432 def revmapfile(self):
1433 return self.join(b'hg-shamap')
1433 return self.join(b'hg-shamap')
1434
1434
1435 def authorfile(self):
1435 def authorfile(self):
1436 return self.join(b'hg-authormap')
1436 return self.join(b'hg-authormap')
1437
1437
1438 def __init__(self, ui, repotype, path):
1438 def __init__(self, ui, repotype, path):
1439
1439
1440 converter_sink.__init__(self, ui, repotype, path)
1440 converter_sink.__init__(self, ui, repotype, path)
1441 commandline.__init__(self, ui, b'svn')
1441 commandline.__init__(self, ui, b'svn')
1442 self.delete = []
1442 self.delete = []
1443 self.setexec = []
1443 self.setexec = []
1444 self.delexec = []
1444 self.delexec = []
1445 self.copies = []
1445 self.copies = []
1446 self.wc = None
1446 self.wc = None
1447 self.cwd = encoding.getcwd()
1447 self.cwd = encoding.getcwd()
1448
1448
1449 created = False
1449 created = False
1450 if os.path.isfile(os.path.join(path, b'.svn', b'entries')):
1450 if os.path.isfile(os.path.join(path, b'.svn', b'entries')):
1451 self.wc = os.path.realpath(path)
1451 self.wc = os.path.realpath(path)
1452 self.run0(b'update')
1452 self.run0(b'update')
1453 else:
1453 else:
1454 if not re.search(br'^(file|http|https|svn|svn\+ssh)://', path):
1454 if not re.search(br'^(file|http|https|svn|svn\+ssh)://', path):
1455 path = os.path.realpath(path)
1455 path = os.path.realpath(path)
1456 if os.path.isdir(os.path.dirname(path)):
1456 if os.path.isdir(os.path.dirname(path)):
1457 if not os.path.exists(
1457 if not os.path.exists(
1458 os.path.join(path, b'db', b'fs-type')
1458 os.path.join(path, b'db', b'fs-type')
1459 ):
1459 ):
1460 ui.status(
1460 ui.status(
1461 _(b"initializing svn repository '%s'\n")
1461 _(b"initializing svn repository '%s'\n")
1462 % os.path.basename(path)
1462 % os.path.basename(path)
1463 )
1463 )
1464 commandline(ui, b'svnadmin').run0(b'create', path)
1464 commandline(ui, b'svnadmin').run0(b'create', path)
1465 created = path
1465 created = path
1466 path = util.normpath(path)
1466 path = util.normpath(path)
1467 if not path.startswith(b'/'):
1467 if not path.startswith(b'/'):
1468 path = b'/' + path
1468 path = b'/' + path
1469 path = b'file://' + path
1469 path = b'file://' + path
1470
1470
1471 wcpath = os.path.join(
1471 wcpath = os.path.join(
1472 encoding.getcwd(), os.path.basename(path) + b'-wc'
1472 encoding.getcwd(), os.path.basename(path) + b'-wc'
1473 )
1473 )
1474 ui.status(
1474 ui.status(
1475 _(b"initializing svn working copy '%s'\n")
1475 _(b"initializing svn working copy '%s'\n")
1476 % os.path.basename(wcpath)
1476 % os.path.basename(wcpath)
1477 )
1477 )
1478 self.run0(b'checkout', path, wcpath)
1478 self.run0(b'checkout', path, wcpath)
1479
1479
1480 self.wc = wcpath
1480 self.wc = wcpath
1481 self.opener = vfsmod.vfs(self.wc)
1481 self.opener = vfsmod.vfs(self.wc)
1482 self.wopener = vfsmod.vfs(self.wc)
1482 self.wopener = vfsmod.vfs(self.wc)
1483 self.childmap = mapfile(ui, self.join(b'hg-childmap'))
1483 self.childmap = mapfile(ui, self.join(b'hg-childmap'))
1484 if util.checkexec(self.wc):
1484 if util.checkexec(self.wc):
1485 self.is_exec = util.isexec
1485 self.is_exec = util.isexec
1486 else:
1486 else:
1487 self.is_exec = None
1487 self.is_exec = None
1488
1488
1489 if created:
1489 if created:
1490 prop_actions_allowed = [
1490 prop_actions_allowed = [
1491 (b'M', b'svn:log'),
1491 (b'M', b'svn:log'),
1492 (b'A', b'hg:convert-branch'),
1492 (b'A', b'hg:convert-branch'),
1493 (b'A', b'hg:convert-rev'),
1493 (b'A', b'hg:convert-rev'),
1494 ]
1494 ]
1495
1495
1496 if self.ui.configbool(
1496 if self.ui.configbool(
1497 b'convert', b'svn.dangerous-set-commit-dates'
1497 b'convert', b'svn.dangerous-set-commit-dates'
1498 ):
1498 ):
1499 prop_actions_allowed.append((b'M', b'svn:date'))
1499 prop_actions_allowed.append((b'M', b'svn:date'))
1500
1500
1501 hook = os.path.join(created, b'hooks', b'pre-revprop-change')
1501 hook = os.path.join(created, b'hooks', b'pre-revprop-change')
1502 fp = open(hook, b'wb')
1502 fp = open(hook, b'wb')
1503 fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
1503 fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
1504 fp.close()
1504 fp.close()
1505 util.setflags(hook, False, True)
1505 util.setflags(hook, False, True)
1506
1506
1507 output = self.run0(b'info')
1507 output = self.run0(b'info')
1508 self.uuid = self.uuid_re.search(output).group(1).strip()
1508 self.uuid = self.uuid_re.search(output).group(1).strip()
1509
1509
1510 def wjoin(self, *names):
1510 def wjoin(self, *names):
1511 return os.path.join(self.wc, *names)
1511 return os.path.join(self.wc, *names)
1512
1512
1513 @propertycache
1513 @propertycache
1514 def manifest(self):
1514 def manifest(self):
1515 # As of svn 1.7, the "add" command fails when receiving
1515 # As of svn 1.7, the "add" command fails when receiving
1516 # already tracked entries, so we have to track and filter them
1516 # already tracked entries, so we have to track and filter them
1517 # ourselves.
1517 # ourselves.
1518 m = set()
1518 m = set()
1519 output = self.run0(b'ls', recursive=True, xml=True)
1519 output = self.run0(b'ls', recursive=True, xml=True)
1520 doc = xml.dom.minidom.parseString(output)
1520 doc = xml.dom.minidom.parseString(output)
1521 for e in doc.getElementsByTagName('entry'):
1521 for e in doc.getElementsByTagName('entry'):
1522 for n in e.childNodes:
1522 for n in e.childNodes:
1523 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1523 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1524 continue
1524 continue
1525 name = ''.join(
1525 name = ''.join(
1526 c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
1526 c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
1527 )
1527 )
1528 # Entries are compared with names coming from
1528 # Entries are compared with names coming from
1529 # mercurial, so bytes with undefined encoding. Our
1529 # mercurial, so bytes with undefined encoding. Our
1530 # best bet is to assume they are in local
1530 # best bet is to assume they are in local
1531 # encoding. They will be passed to command line calls
1531 # encoding. They will be passed to command line calls
1532 # later anyway, so they better be.
1532 # later anyway, so they better be.
1533 m.add(encoding.unitolocal(name))
1533 m.add(encoding.unitolocal(name))
1534 break
1534 break
1535 return m
1535 return m
1536
1536
1537 def putfile(self, filename, flags, data):
1537 def putfile(self, filename, flags, data):
1538 if b'l' in flags:
1538 if b'l' in flags:
1539 self.wopener.symlink(data, filename)
1539 self.wopener.symlink(data, filename)
1540 else:
1540 else:
1541 try:
1541 try:
1542 if os.path.islink(self.wjoin(filename)):
1542 if os.path.islink(self.wjoin(filename)):
1543 os.unlink(filename)
1543 os.unlink(filename)
1544 except OSError:
1544 except OSError:
1545 pass
1545 pass
1546
1546
1547 if self.is_exec:
1547 if self.is_exec:
1548 # We need to check executability of the file before the change,
1548 # We need to check executability of the file before the change,
1549 # because `vfs.write` is able to reset exec bit.
1549 # because `vfs.write` is able to reset exec bit.
1550 wasexec = False
1550 wasexec = False
1551 if os.path.exists(self.wjoin(filename)):
1551 if os.path.exists(self.wjoin(filename)):
1552 wasexec = self.is_exec(self.wjoin(filename))
1552 wasexec = self.is_exec(self.wjoin(filename))
1553
1553
1554 self.wopener.write(filename, data)
1554 self.wopener.write(filename, data)
1555
1555
1556 if self.is_exec:
1556 if self.is_exec:
1557 if wasexec:
1557 if wasexec:
1558 if b'x' not in flags:
1558 if b'x' not in flags:
1559 self.delexec.append(filename)
1559 self.delexec.append(filename)
1560 else:
1560 else:
1561 if b'x' in flags:
1561 if b'x' in flags:
1562 self.setexec.append(filename)
1562 self.setexec.append(filename)
1563 util.setflags(self.wjoin(filename), False, b'x' in flags)
1563 util.setflags(self.wjoin(filename), False, b'x' in flags)
1564
1564
1565 def _copyfile(self, source, dest):
1565 def _copyfile(self, source, dest):
1566 # SVN's copy command pukes if the destination file exists, but
1566 # SVN's copy command pukes if the destination file exists, but
1567 # our copyfile method expects to record a copy that has
1567 # our copyfile method expects to record a copy that has
1568 # already occurred. Cross the semantic gap.
1568 # already occurred. Cross the semantic gap.
1569 wdest = self.wjoin(dest)
1569 wdest = self.wjoin(dest)
1570 exists = os.path.lexists(wdest)
1570 exists = os.path.lexists(wdest)
1571 if exists:
1571 if exists:
1572 fd, tempname = pycompat.mkstemp(
1572 fd, tempname = pycompat.mkstemp(
1573 prefix=b'hg-copy-', dir=os.path.dirname(wdest)
1573 prefix=b'hg-copy-', dir=os.path.dirname(wdest)
1574 )
1574 )
1575 os.close(fd)
1575 os.close(fd)
1576 os.unlink(tempname)
1576 os.unlink(tempname)
1577 os.rename(wdest, tempname)
1577 os.rename(wdest, tempname)
1578 try:
1578 try:
1579 self.run0(b'copy', source, dest)
1579 self.run0(b'copy', source, dest)
1580 finally:
1580 finally:
1581 self.manifest.add(dest)
1581 self.manifest.add(dest)
1582 if exists:
1582 if exists:
1583 try:
1583 try:
1584 os.unlink(wdest)
1584 os.unlink(wdest)
1585 except OSError:
1585 except OSError:
1586 pass
1586 pass
1587 os.rename(tempname, wdest)
1587 os.rename(tempname, wdest)
1588
1588
1589 def dirs_of(self, files):
1589 def dirs_of(self, files):
1590 dirs = set()
1590 dirs = set()
1591 for f in files:
1591 for f in files:
1592 if os.path.isdir(self.wjoin(f)):
1592 if os.path.isdir(self.wjoin(f)):
1593 dirs.add(f)
1593 dirs.add(f)
1594 i = len(f)
1594 i = len(f)
1595 for i in iter(lambda: f.rfind(b'/', 0, i), -1):
1595 for i in iter(lambda: f.rfind(b'/', 0, i), -1):
1596 dirs.add(f[:i])
1596 dirs.add(f[:i])
1597 return dirs
1597 return dirs
1598
1598
1599 def add_dirs(self, files):
1599 def add_dirs(self, files):
1600 add_dirs = [
1600 add_dirs = [
1601 d for d in sorted(self.dirs_of(files)) if d not in self.manifest
1601 d for d in sorted(self.dirs_of(files)) if d not in self.manifest
1602 ]
1602 ]
1603 if add_dirs:
1603 if add_dirs:
1604 self.manifest.update(add_dirs)
1604 self.manifest.update(add_dirs)
1605 self.xargs(add_dirs, b'add', non_recursive=True, quiet=True)
1605 self.xargs(add_dirs, b'add', non_recursive=True, quiet=True)
1606 return add_dirs
1606 return add_dirs
1607
1607
1608 def add_files(self, files):
1608 def add_files(self, files):
1609 files = [f for f in files if f not in self.manifest]
1609 files = [f for f in files if f not in self.manifest]
1610 if files:
1610 if files:
1611 self.manifest.update(files)
1611 self.manifest.update(files)
1612 self.xargs(files, b'add', quiet=True)
1612 self.xargs(files, b'add', quiet=True)
1613 return files
1613 return files
1614
1614
1615 def addchild(self, parent, child):
1615 def addchild(self, parent, child):
1616 self.childmap[parent] = child
1616 self.childmap[parent] = child
1617
1617
1618 def revid(self, rev):
1618 def revid(self, rev):
1619 return b"svn:%s@%s" % (self.uuid, rev)
1619 return b"svn:%s@%s" % (self.uuid, rev)
1620
1620
1621 def putcommit(
1621 def putcommit(
1622 self, files, copies, parents, commit, source, revmap, full, cleanp2
1622 self, files, copies, parents, commit, source, revmap, full, cleanp2
1623 ):
1623 ):
1624 for parent in parents:
1624 for parent in parents:
1625 try:
1625 try:
1626 return self.revid(self.childmap[parent])
1626 return self.revid(self.childmap[parent])
1627 except KeyError:
1627 except KeyError:
1628 pass
1628 pass
1629
1629
1630 # Apply changes to working copy
1630 # Apply changes to working copy
1631 for f, v in files:
1631 for f, v in files:
1632 data, mode = source.getfile(f, v)
1632 data, mode = source.getfile(f, v)
1633 if data is None:
1633 if data is None:
1634 self.delete.append(f)
1634 self.delete.append(f)
1635 else:
1635 else:
1636 self.putfile(f, mode, data)
1636 self.putfile(f, mode, data)
1637 if f in copies:
1637 if f in copies:
1638 self.copies.append([copies[f], f])
1638 self.copies.append([copies[f], f])
1639 if full:
1639 if full:
1640 self.delete.extend(sorted(self.manifest.difference(files)))
1640 self.delete.extend(sorted(self.manifest.difference(files)))
1641 files = [f[0] for f in files]
1641 files = [f[0] for f in files]
1642
1642
1643 entries = set(self.delete)
1643 entries = set(self.delete)
1644 files = frozenset(files)
1644 files = frozenset(files)
1645 entries.update(self.add_dirs(files.difference(entries)))
1645 entries.update(self.add_dirs(files.difference(entries)))
1646 if self.copies:
1646 if self.copies:
1647 for s, d in self.copies:
1647 for s, d in self.copies:
1648 self._copyfile(s, d)
1648 self._copyfile(s, d)
1649 self.copies = []
1649 self.copies = []
1650 if self.delete:
1650 if self.delete:
1651 self.xargs(self.delete, b'delete')
1651 self.xargs(self.delete, b'delete')
1652 for f in self.delete:
1652 for f in self.delete:
1653 self.manifest.remove(f)
1653 self.manifest.remove(f)
1654 self.delete = []
1654 self.delete = []
1655 entries.update(self.add_files(files.difference(entries)))
1655 entries.update(self.add_files(files.difference(entries)))
1656 if self.delexec:
1656 if self.delexec:
1657 self.xargs(self.delexec, b'propdel', b'svn:executable')
1657 self.xargs(self.delexec, b'propdel', b'svn:executable')
1658 self.delexec = []
1658 self.delexec = []
1659 if self.setexec:
1659 if self.setexec:
1660 self.xargs(self.setexec, b'propset', b'svn:executable', b'*')
1660 self.xargs(self.setexec, b'propset', b'svn:executable', b'*')
1661 self.setexec = []
1661 self.setexec = []
1662
1662
1663 fd, messagefile = pycompat.mkstemp(prefix=b'hg-convert-')
1663 fd, messagefile = pycompat.mkstemp(prefix=b'hg-convert-')
1664 fp = os.fdopen(fd, 'wb')
1664 fp = os.fdopen(fd, 'wb')
1665 fp.write(util.tonativeeol(commit.desc))
1665 fp.write(util.tonativeeol(commit.desc))
1666 fp.close()
1666 fp.close()
1667 try:
1667 try:
1668 output = self.run0(
1668 output = self.run0(
1669 b'commit',
1669 b'commit',
1670 username=stringutil.shortuser(commit.author),
1670 username=stringutil.shortuser(commit.author),
1671 file=messagefile,
1671 file=messagefile,
1672 encoding=b'utf-8',
1672 encoding=b'utf-8',
1673 )
1673 )
1674 try:
1674 try:
1675 rev = self.commit_re.search(output).group(1)
1675 rev = self.commit_re.search(output).group(1)
1676 except AttributeError:
1676 except AttributeError:
1677 if not files:
1677 if not files:
1678 return parents[0] if parents else b'None'
1678 return parents[0] if parents else b'None'
1679 self.ui.warn(_(b'unexpected svn output:\n'))
1679 self.ui.warn(_(b'unexpected svn output:\n'))
1680 self.ui.warn(output)
1680 self.ui.warn(output)
1681 raise error.Abort(_(b'unable to cope with svn output'))
1681 raise error.Abort(_(b'unable to cope with svn output'))
1682 if commit.rev:
1682 if commit.rev:
1683 self.run(
1683 self.run(
1684 b'propset',
1684 b'propset',
1685 b'hg:convert-rev',
1685 b'hg:convert-rev',
1686 commit.rev,
1686 commit.rev,
1687 revprop=True,
1687 revprop=True,
1688 revision=rev,
1688 revision=rev,
1689 )
1689 )
1690 if commit.branch and commit.branch != b'default':
1690 if commit.branch and commit.branch != b'default':
1691 self.run(
1691 self.run(
1692 b'propset',
1692 b'propset',
1693 b'hg:convert-branch',
1693 b'hg:convert-branch',
1694 commit.branch,
1694 commit.branch,
1695 revprop=True,
1695 revprop=True,
1696 revision=rev,
1696 revision=rev,
1697 )
1697 )
1698
1698
1699 if self.ui.configbool(
1699 if self.ui.configbool(
1700 b'convert', b'svn.dangerous-set-commit-dates'
1700 b'convert', b'svn.dangerous-set-commit-dates'
1701 ):
1701 ):
1702 # Subverson always uses UTC to represent date and time
1702 # Subverson always uses UTC to represent date and time
1703 date = dateutil.parsedate(commit.date)
1703 date = dateutil.parsedate(commit.date)
1704 date = (date[0], 0)
1704 date = (date[0], 0)
1705
1705
1706 # The only way to set date and time for svn commit is to use propset after commit is done
1706 # The only way to set date and time for svn commit is to use propset after commit is done
1707 self.run(
1707 self.run(
1708 b'propset',
1708 b'propset',
1709 b'svn:date',
1709 b'svn:date',
1710 formatsvndate(date),
1710 formatsvndate(date),
1711 revprop=True,
1711 revprop=True,
1712 revision=rev,
1712 revision=rev,
1713 )
1713 )
1714
1714
1715 for parent in parents:
1715 for parent in parents:
1716 self.addchild(parent, rev)
1716 self.addchild(parent, rev)
1717 return self.revid(rev)
1717 return self.revid(rev)
1718 finally:
1718 finally:
1719 os.unlink(messagefile)
1719 os.unlink(messagefile)
1720
1720
1721 def puttags(self, tags):
1721 def puttags(self, tags):
1722 self.ui.warn(_(b'writing Subversion tags is not yet implemented\n'))
1722 self.ui.warn(_(b'writing Subversion tags is not yet implemented\n'))
1723 return None, None
1723 return None, None
1724
1724
1725 def hascommitfrommap(self, rev):
1725 def hascommitfrommap(self, rev):
1726 # We trust that revisions referenced in a map still is present
1726 # We trust that revisions referenced in a map still is present
1727 # TODO: implement something better if necessary and feasible
1727 # TODO: implement something better if necessary and feasible
1728 return True
1728 return True
1729
1729
1730 def hascommitforsplicemap(self, rev):
1730 def hascommitforsplicemap(self, rev):
1731 # This is not correct as one can convert to an existing subversion
1731 # This is not correct as one can convert to an existing subversion
1732 # repository and childmap would not list all revisions. Too bad.
1732 # repository and childmap would not list all revisions. Too bad.
1733 if rev in self.childmap:
1733 if rev in self.childmap:
1734 return True
1734 return True
1735 raise error.Abort(
1735 raise error.Abort(
1736 _(
1736 _(
1737 b'splice map revision %s not found in subversion '
1737 b'splice map revision %s not found in subversion '
1738 b'child map (revision lookups are not implemented)'
1738 b'child map (revision lookups are not implemented)'
1739 )
1739 )
1740 % rev
1740 % rev
1741 )
1741 )
@@ -1,2690 +1,2690 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.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 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but allow edits before making new commit
37 # e, edit = use commit, but allow edits before making new commit
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description and date
39 # r, roll = like fold, but discard this commit's description and date
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 # b, base = checkout changeset and apply further changesets from there
42 # b, base = checkout changeset and apply further changesets from there
43 #
43 #
44
44
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 for each revision in your history. For example, if you had meant to add gamma
46 for each revision in your history. For example, if you had meant to add gamma
47 before beta, and then wanted to add delta in the same revision as beta, you
47 before beta, and then wanted to add delta in the same revision as beta, you
48 would reorganize the file to look like this::
48 would reorganize the file to look like this::
49
49
50 pick 030b686bedc4 Add gamma
50 pick 030b686bedc4 Add gamma
51 pick c561b4e977df Add beta
51 pick c561b4e977df Add beta
52 fold 7c2fd3b9020c Add delta
52 fold 7c2fd3b9020c Add delta
53
53
54 # Edit history between c561b4e977df and 7c2fd3b9020c
54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 #
55 #
56 # Commits are listed from least to most recent
56 # Commits are listed from least to most recent
57 #
57 #
58 # Commands:
58 # Commands:
59 # p, pick = use commit
59 # p, pick = use commit
60 # e, edit = use commit, but allow edits before making new commit
60 # e, edit = use commit, but allow edits before making new commit
61 # f, fold = use commit, but combine it with the one above
61 # f, fold = use commit, but combine it with the one above
62 # r, roll = like fold, but discard this commit's description and date
62 # r, roll = like fold, but discard this commit's description and date
63 # d, drop = remove commit from history
63 # d, drop = remove commit from history
64 # m, mess = edit commit message without changing commit content
64 # m, mess = edit commit message without changing commit content
65 # b, base = checkout changeset and apply further changesets from there
65 # b, base = checkout changeset and apply further changesets from there
66 #
66 #
67
67
68 At which point you close the editor and ``histedit`` starts working. When you
68 At which point you close the editor and ``histedit`` starts working. When you
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 those revisions together, offering you a chance to clean up the commit message::
70 those revisions together, offering you a chance to clean up the commit message::
71
71
72 Add beta
72 Add beta
73 ***
73 ***
74 Add delta
74 Add delta
75
75
76 Edit the commit message to your liking, then close the editor. The date used
76 Edit the commit message to your liking, then close the editor. The date used
77 for the commit will be the later of the two commits' dates. For this example,
77 for the commit will be the later of the two commits' dates. For this example,
78 let's assume that the commit message was changed to ``Add beta and delta.``
78 let's assume that the commit message was changed to ``Add beta and delta.``
79 After histedit has run and had a chance to remove any old or temporary
79 After histedit has run and had a chance to remove any old or temporary
80 revisions it needed, the history looks like this::
80 revisions it needed, the history looks like this::
81
81
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 | Add beta and delta.
83 | Add beta and delta.
84 |
84 |
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 | Add gamma
86 | Add gamma
87 |
87 |
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 Add alpha
89 Add alpha
90
90
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 ones) until after it has completed all the editing operations, so it will
92 ones) until after it has completed all the editing operations, so it will
93 probably perform several strip operations when it's done. For the above example,
93 probably perform several strip operations when it's done. For the above example,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 so you might need to be a little patient. You can choose to keep the original
95 so you might need to be a little patient. You can choose to keep the original
96 revisions by passing the ``--keep`` flag.
96 revisions by passing the ``--keep`` flag.
97
97
98 The ``edit`` operation will drop you back to a command prompt,
98 The ``edit`` operation will drop you back to a command prompt,
99 allowing you to edit files freely, or even use ``hg record`` to commit
99 allowing you to edit files freely, or even use ``hg record`` to commit
100 some changes as a separate commit. When you're done, any remaining
100 some changes as a separate commit. When you're done, any remaining
101 uncommitted changes will be committed as well. When done, run ``hg
101 uncommitted changes will be committed as well. When done, run ``hg
102 histedit --continue`` to finish this step. If there are uncommitted
102 histedit --continue`` to finish this step. If there are uncommitted
103 changes, you'll be prompted for a new commit message, but the default
103 changes, you'll be prompted for a new commit message, but the default
104 commit message will be the original message for the ``edit`` ed
104 commit message will be the original message for the ``edit`` ed
105 revision, and the date of the original commit will be preserved.
105 revision, and the date of the original commit will be preserved.
106
106
107 The ``message`` operation will give you a chance to revise a commit
107 The ``message`` operation will give you a chance to revise a commit
108 message without changing the contents. It's a shortcut for doing
108 message without changing the contents. It's a shortcut for doing
109 ``edit`` immediately followed by `hg histedit --continue``.
109 ``edit`` immediately followed by `hg histedit --continue``.
110
110
111 If ``histedit`` encounters a conflict when moving a revision (while
111 If ``histedit`` encounters a conflict when moving a revision (while
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 ``edit`` with the difference that it won't prompt you for a commit
113 ``edit`` with the difference that it won't prompt you for a commit
114 message when done. If you decide at this point that you don't like how
114 message when done. If you decide at this point that you don't like how
115 much work it will be to rearrange history, or that you made a mistake,
115 much work it will be to rearrange history, or that you made a mistake,
116 you can use ``hg histedit --abort`` to abandon the new changes you
116 you can use ``hg histedit --abort`` to abandon the new changes you
117 have made and return to the state before you attempted to edit your
117 have made and return to the state before you attempted to edit your
118 history.
118 history.
119
119
120 If we clone the histedit-ed example repository above and add four more
120 If we clone the histedit-ed example repository above and add four more
121 changes, such that we have the following history::
121 changes, such that we have the following history::
122
122
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 | Add theta
124 | Add theta
125 |
125 |
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 | Add eta
127 | Add eta
128 |
128 |
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 | Add zeta
130 | Add zeta
131 |
131 |
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 | Add epsilon
133 | Add epsilon
134 |
134 |
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 | Add beta and delta.
136 | Add beta and delta.
137 |
137 |
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 | Add gamma
139 | Add gamma
140 |
140 |
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 Add alpha
142 Add alpha
143
143
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 as running ``hg histedit 836302820282``. If you need plan to push to a
145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 repository that Mercurial does not detect to be related to the source
146 repository that Mercurial does not detect to be related to the source
147 repo, you can add a ``--force`` option.
147 repo, you can add a ``--force`` option.
148
148
149 Config
149 Config
150 ------
150 ------
151
151
152 Histedit rule lines are truncated to 80 characters by default. You
152 Histedit rule lines are truncated to 80 characters by default. You
153 can customize this behavior by setting a different length in your
153 can customize this behavior by setting a different length in your
154 configuration file::
154 configuration file::
155
155
156 [histedit]
156 [histedit]
157 linelen = 120 # truncate rule lines at 120 characters
157 linelen = 120 # truncate rule lines at 120 characters
158
158
159 The summary of a change can be customized as well::
159 The summary of a change can be customized as well::
160
160
161 [histedit]
161 [histedit]
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163
163
164 The customized summary should be kept short enough that rule lines
164 The customized summary should be kept short enough that rule lines
165 will fit in the configured line length. See above if that requires
165 will fit in the configured line length. See above if that requires
166 customization.
166 customization.
167
167
168 ``hg histedit`` attempts to automatically choose an appropriate base
168 ``hg histedit`` attempts to automatically choose an appropriate base
169 revision to use. To change which base revision is used, define a
169 revision to use. To change which base revision is used, define a
170 revset in your configuration file::
170 revset in your configuration file::
171
171
172 [histedit]
172 [histedit]
173 defaultrev = only(.) & draft()
173 defaultrev = only(.) & draft()
174
174
175 By default each edited revision needs to be present in histedit commands.
175 By default each edited revision needs to be present in histedit commands.
176 To remove revision you need to use ``drop`` operation. You can configure
176 To remove revision you need to use ``drop`` operation. You can configure
177 the drop to be implicit for missing commits by adding::
177 the drop to be implicit for missing commits by adding::
178
178
179 [histedit]
179 [histedit]
180 dropmissing = True
180 dropmissing = True
181
181
182 By default, histedit will close the transaction after each action. For
182 By default, histedit will close the transaction after each action. For
183 performance purposes, you can configure histedit to use a single transaction
183 performance purposes, you can configure histedit to use a single transaction
184 across the entire histedit. WARNING: This setting introduces a significant risk
184 across the entire histedit. WARNING: This setting introduces a significant risk
185 of losing the work you've done in a histedit if the histedit aborts
185 of losing the work you've done in a histedit if the histedit aborts
186 unexpectedly::
186 unexpectedly::
187
187
188 [histedit]
188 [histedit]
189 singletransaction = True
189 singletransaction = True
190
190
191 """
191 """
192
192
193 from __future__ import absolute_import
193 from __future__ import absolute_import
194
194
195 # chistedit dependencies that are not available everywhere
195 # chistedit dependencies that are not available everywhere
196 try:
196 try:
197 import fcntl
197 import fcntl
198 import termios
198 import termios
199 except ImportError:
199 except ImportError:
200 fcntl = None
200 fcntl = None
201 termios = None
201 termios = None
202
202
203 import functools
203 import functools
204 import os
204 import os
205 import pickle
205 import struct
206 import struct
206
207
207 from mercurial.i18n import _
208 from mercurial.i18n import _
208 from mercurial.pycompat import (
209 from mercurial.pycompat import (
209 getattr,
210 getattr,
210 open,
211 open,
211 )
212 )
212 from mercurial.node import (
213 from mercurial.node import (
213 bin,
214 bin,
214 hex,
215 hex,
215 short,
216 short,
216 )
217 )
217 from mercurial import (
218 from mercurial import (
218 bundle2,
219 bundle2,
219 cmdutil,
220 cmdutil,
220 context,
221 context,
221 copies,
222 copies,
222 destutil,
223 destutil,
223 discovery,
224 discovery,
224 encoding,
225 encoding,
225 error,
226 error,
226 exchange,
227 exchange,
227 extensions,
228 extensions,
228 hg,
229 hg,
229 logcmdutil,
230 logcmdutil,
230 merge as mergemod,
231 merge as mergemod,
231 mergestate as mergestatemod,
232 mergestate as mergestatemod,
232 mergeutil,
233 mergeutil,
233 obsolete,
234 obsolete,
234 pycompat,
235 pycompat,
235 registrar,
236 registrar,
236 repair,
237 repair,
237 rewriteutil,
238 rewriteutil,
238 scmutil,
239 scmutil,
239 state as statemod,
240 state as statemod,
240 util,
241 util,
241 )
242 )
242 from mercurial.utils import (
243 from mercurial.utils import (
243 dateutil,
244 dateutil,
244 stringutil,
245 stringutil,
245 urlutil,
246 urlutil,
246 )
247 )
247
248
248 pickle = util.pickle
249 cmdtable = {}
249 cmdtable = {}
250 command = registrar.command(cmdtable)
250 command = registrar.command(cmdtable)
251
251
252 configtable = {}
252 configtable = {}
253 configitem = registrar.configitem(configtable)
253 configitem = registrar.configitem(configtable)
254 configitem(
254 configitem(
255 b'experimental',
255 b'experimental',
256 b'histedit.autoverb',
256 b'histedit.autoverb',
257 default=False,
257 default=False,
258 )
258 )
259 configitem(
259 configitem(
260 b'histedit',
260 b'histedit',
261 b'defaultrev',
261 b'defaultrev',
262 default=None,
262 default=None,
263 )
263 )
264 configitem(
264 configitem(
265 b'histedit',
265 b'histedit',
266 b'dropmissing',
266 b'dropmissing',
267 default=False,
267 default=False,
268 )
268 )
269 configitem(
269 configitem(
270 b'histedit',
270 b'histedit',
271 b'linelen',
271 b'linelen',
272 default=80,
272 default=80,
273 )
273 )
274 configitem(
274 configitem(
275 b'histedit',
275 b'histedit',
276 b'singletransaction',
276 b'singletransaction',
277 default=False,
277 default=False,
278 )
278 )
279 configitem(
279 configitem(
280 b'ui',
280 b'ui',
281 b'interface.histedit',
281 b'interface.histedit',
282 default=None,
282 default=None,
283 )
283 )
284 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
284 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
285 # TODO: Teach the text-based histedit interface to respect this config option
285 # TODO: Teach the text-based histedit interface to respect this config option
286 # before we make it non-experimental.
286 # before we make it non-experimental.
287 configitem(
287 configitem(
288 b'histedit', b'later-commits-first', default=False, experimental=True
288 b'histedit', b'later-commits-first', default=False, experimental=True
289 )
289 )
290
290
291 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
291 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
292 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
292 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
293 # be specifying the version(s) of Mercurial they are tested with, or
293 # be specifying the version(s) of Mercurial they are tested with, or
294 # leave the attribute unspecified.
294 # leave the attribute unspecified.
295 testedwith = b'ships-with-hg-core'
295 testedwith = b'ships-with-hg-core'
296
296
297 actiontable = {}
297 actiontable = {}
298 primaryactions = set()
298 primaryactions = set()
299 secondaryactions = set()
299 secondaryactions = set()
300 tertiaryactions = set()
300 tertiaryactions = set()
301 internalactions = set()
301 internalactions = set()
302
302
303
303
304 def geteditcomment(ui, first, last):
304 def geteditcomment(ui, first, last):
305 """construct the editor comment
305 """construct the editor comment
306 The comment includes::
306 The comment includes::
307 - an intro
307 - an intro
308 - sorted primary commands
308 - sorted primary commands
309 - sorted short commands
309 - sorted short commands
310 - sorted long commands
310 - sorted long commands
311 - additional hints
311 - additional hints
312
312
313 Commands are only included once.
313 Commands are only included once.
314 """
314 """
315 intro = _(
315 intro = _(
316 b"""Edit history between %s and %s
316 b"""Edit history between %s and %s
317
317
318 Commits are listed from least to most recent
318 Commits are listed from least to most recent
319
319
320 You can reorder changesets by reordering the lines
320 You can reorder changesets by reordering the lines
321
321
322 Commands:
322 Commands:
323 """
323 """
324 )
324 )
325 actions = []
325 actions = []
326
326
327 def addverb(v):
327 def addverb(v):
328 a = actiontable[v]
328 a = actiontable[v]
329 lines = a.message.split(b"\n")
329 lines = a.message.split(b"\n")
330 if len(a.verbs):
330 if len(a.verbs):
331 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
331 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
332 actions.append(b" %s = %s" % (v, lines[0]))
332 actions.append(b" %s = %s" % (v, lines[0]))
333 actions.extend([b' %s'] * (len(lines) - 1))
333 actions.extend([b' %s'] * (len(lines) - 1))
334
334
335 for v in (
335 for v in (
336 sorted(primaryactions)
336 sorted(primaryactions)
337 + sorted(secondaryactions)
337 + sorted(secondaryactions)
338 + sorted(tertiaryactions)
338 + sorted(tertiaryactions)
339 ):
339 ):
340 addverb(v)
340 addverb(v)
341 actions.append(b'')
341 actions.append(b'')
342
342
343 hints = []
343 hints = []
344 if ui.configbool(b'histedit', b'dropmissing'):
344 if ui.configbool(b'histedit', b'dropmissing'):
345 hints.append(
345 hints.append(
346 b"Deleting a changeset from the list "
346 b"Deleting a changeset from the list "
347 b"will DISCARD it from the edited history!"
347 b"will DISCARD it from the edited history!"
348 )
348 )
349
349
350 lines = (intro % (first, last)).split(b'\n') + actions + hints
350 lines = (intro % (first, last)).split(b'\n') + actions + hints
351
351
352 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
352 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
353
353
354
354
355 class histeditstate(object):
355 class histeditstate(object):
356 def __init__(self, repo):
356 def __init__(self, repo):
357 self.repo = repo
357 self.repo = repo
358 self.actions = None
358 self.actions = None
359 self.keep = None
359 self.keep = None
360 self.topmost = None
360 self.topmost = None
361 self.parentctxnode = None
361 self.parentctxnode = None
362 self.lock = None
362 self.lock = None
363 self.wlock = None
363 self.wlock = None
364 self.backupfile = None
364 self.backupfile = None
365 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
365 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
366 self.replacements = []
366 self.replacements = []
367
367
368 def read(self):
368 def read(self):
369 """Load histedit state from disk and set fields appropriately."""
369 """Load histedit state from disk and set fields appropriately."""
370 if not self.stateobj.exists():
370 if not self.stateobj.exists():
371 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
371 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
372
372
373 data = self._read()
373 data = self._read()
374
374
375 self.parentctxnode = data[b'parentctxnode']
375 self.parentctxnode = data[b'parentctxnode']
376 actions = parserules(data[b'rules'], self)
376 actions = parserules(data[b'rules'], self)
377 self.actions = actions
377 self.actions = actions
378 self.keep = data[b'keep']
378 self.keep = data[b'keep']
379 self.topmost = data[b'topmost']
379 self.topmost = data[b'topmost']
380 self.replacements = data[b'replacements']
380 self.replacements = data[b'replacements']
381 self.backupfile = data[b'backupfile']
381 self.backupfile = data[b'backupfile']
382
382
383 def _read(self):
383 def _read(self):
384 fp = self.repo.vfs.read(b'histedit-state')
384 fp = self.repo.vfs.read(b'histedit-state')
385 if fp.startswith(b'v1\n'):
385 if fp.startswith(b'v1\n'):
386 data = self._load()
386 data = self._load()
387 parentctxnode, rules, keep, topmost, replacements, backupfile = data
387 parentctxnode, rules, keep, topmost, replacements, backupfile = data
388 else:
388 else:
389 data = pickle.loads(fp)
389 data = pickle.loads(fp)
390 parentctxnode, rules, keep, topmost, replacements = data
390 parentctxnode, rules, keep, topmost, replacements = data
391 backupfile = None
391 backupfile = None
392 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
392 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
393
393
394 return {
394 return {
395 b'parentctxnode': parentctxnode,
395 b'parentctxnode': parentctxnode,
396 b"rules": rules,
396 b"rules": rules,
397 b"keep": keep,
397 b"keep": keep,
398 b"topmost": topmost,
398 b"topmost": topmost,
399 b"replacements": replacements,
399 b"replacements": replacements,
400 b"backupfile": backupfile,
400 b"backupfile": backupfile,
401 }
401 }
402
402
403 def write(self, tr=None):
403 def write(self, tr=None):
404 if tr:
404 if tr:
405 tr.addfilegenerator(
405 tr.addfilegenerator(
406 b'histedit-state',
406 b'histedit-state',
407 (b'histedit-state',),
407 (b'histedit-state',),
408 self._write,
408 self._write,
409 location=b'plain',
409 location=b'plain',
410 )
410 )
411 else:
411 else:
412 with self.repo.vfs(b"histedit-state", b"w") as f:
412 with self.repo.vfs(b"histedit-state", b"w") as f:
413 self._write(f)
413 self._write(f)
414
414
415 def _write(self, fp):
415 def _write(self, fp):
416 fp.write(b'v1\n')
416 fp.write(b'v1\n')
417 fp.write(b'%s\n' % hex(self.parentctxnode))
417 fp.write(b'%s\n' % hex(self.parentctxnode))
418 fp.write(b'%s\n' % hex(self.topmost))
418 fp.write(b'%s\n' % hex(self.topmost))
419 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
419 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
420 fp.write(b'%d\n' % len(self.actions))
420 fp.write(b'%d\n' % len(self.actions))
421 for action in self.actions:
421 for action in self.actions:
422 fp.write(b'%s\n' % action.tostate())
422 fp.write(b'%s\n' % action.tostate())
423 fp.write(b'%d\n' % len(self.replacements))
423 fp.write(b'%d\n' % len(self.replacements))
424 for replacement in self.replacements:
424 for replacement in self.replacements:
425 fp.write(
425 fp.write(
426 b'%s%s\n'
426 b'%s%s\n'
427 % (
427 % (
428 hex(replacement[0]),
428 hex(replacement[0]),
429 b''.join(hex(r) for r in replacement[1]),
429 b''.join(hex(r) for r in replacement[1]),
430 )
430 )
431 )
431 )
432 backupfile = self.backupfile
432 backupfile = self.backupfile
433 if not backupfile:
433 if not backupfile:
434 backupfile = b''
434 backupfile = b''
435 fp.write(b'%s\n' % backupfile)
435 fp.write(b'%s\n' % backupfile)
436
436
437 def _load(self):
437 def _load(self):
438 fp = self.repo.vfs(b'histedit-state', b'r')
438 fp = self.repo.vfs(b'histedit-state', b'r')
439 lines = [l[:-1] for l in fp.readlines()]
439 lines = [l[:-1] for l in fp.readlines()]
440
440
441 index = 0
441 index = 0
442 lines[index] # version number
442 lines[index] # version number
443 index += 1
443 index += 1
444
444
445 parentctxnode = bin(lines[index])
445 parentctxnode = bin(lines[index])
446 index += 1
446 index += 1
447
447
448 topmost = bin(lines[index])
448 topmost = bin(lines[index])
449 index += 1
449 index += 1
450
450
451 keep = lines[index] == b'True'
451 keep = lines[index] == b'True'
452 index += 1
452 index += 1
453
453
454 # Rules
454 # Rules
455 rules = []
455 rules = []
456 rulelen = int(lines[index])
456 rulelen = int(lines[index])
457 index += 1
457 index += 1
458 for i in pycompat.xrange(rulelen):
458 for i in pycompat.xrange(rulelen):
459 ruleaction = lines[index]
459 ruleaction = lines[index]
460 index += 1
460 index += 1
461 rule = lines[index]
461 rule = lines[index]
462 index += 1
462 index += 1
463 rules.append((ruleaction, rule))
463 rules.append((ruleaction, rule))
464
464
465 # Replacements
465 # Replacements
466 replacements = []
466 replacements = []
467 replacementlen = int(lines[index])
467 replacementlen = int(lines[index])
468 index += 1
468 index += 1
469 for i in pycompat.xrange(replacementlen):
469 for i in pycompat.xrange(replacementlen):
470 replacement = lines[index]
470 replacement = lines[index]
471 original = bin(replacement[:40])
471 original = bin(replacement[:40])
472 succ = [
472 succ = [
473 bin(replacement[i : i + 40])
473 bin(replacement[i : i + 40])
474 for i in range(40, len(replacement), 40)
474 for i in range(40, len(replacement), 40)
475 ]
475 ]
476 replacements.append((original, succ))
476 replacements.append((original, succ))
477 index += 1
477 index += 1
478
478
479 backupfile = lines[index]
479 backupfile = lines[index]
480 index += 1
480 index += 1
481
481
482 fp.close()
482 fp.close()
483
483
484 return parentctxnode, rules, keep, topmost, replacements, backupfile
484 return parentctxnode, rules, keep, topmost, replacements, backupfile
485
485
486 def clear(self):
486 def clear(self):
487 if self.inprogress():
487 if self.inprogress():
488 self.repo.vfs.unlink(b'histedit-state')
488 self.repo.vfs.unlink(b'histedit-state')
489
489
490 def inprogress(self):
490 def inprogress(self):
491 return self.repo.vfs.exists(b'histedit-state')
491 return self.repo.vfs.exists(b'histedit-state')
492
492
493
493
494 class histeditaction(object):
494 class histeditaction(object):
495 def __init__(self, state, node):
495 def __init__(self, state, node):
496 self.state = state
496 self.state = state
497 self.repo = state.repo
497 self.repo = state.repo
498 self.node = node
498 self.node = node
499
499
500 @classmethod
500 @classmethod
501 def fromrule(cls, state, rule):
501 def fromrule(cls, state, rule):
502 """Parses the given rule, returning an instance of the histeditaction."""
502 """Parses the given rule, returning an instance of the histeditaction."""
503 ruleid = rule.strip().split(b' ', 1)[0]
503 ruleid = rule.strip().split(b' ', 1)[0]
504 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
504 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
505 # Check for validation of rule ids and get the rulehash
505 # Check for validation of rule ids and get the rulehash
506 try:
506 try:
507 rev = bin(ruleid)
507 rev = bin(ruleid)
508 except TypeError:
508 except TypeError:
509 try:
509 try:
510 _ctx = scmutil.revsingle(state.repo, ruleid)
510 _ctx = scmutil.revsingle(state.repo, ruleid)
511 rulehash = _ctx.hex()
511 rulehash = _ctx.hex()
512 rev = bin(rulehash)
512 rev = bin(rulehash)
513 except error.RepoLookupError:
513 except error.RepoLookupError:
514 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
514 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
515 return cls(state, rev)
515 return cls(state, rev)
516
516
517 def verify(self, prev, expected, seen):
517 def verify(self, prev, expected, seen):
518 """Verifies semantic correctness of the rule"""
518 """Verifies semantic correctness of the rule"""
519 repo = self.repo
519 repo = self.repo
520 ha = hex(self.node)
520 ha = hex(self.node)
521 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
521 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
522 if self.node is None:
522 if self.node is None:
523 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
523 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
524 self._verifynodeconstraints(prev, expected, seen)
524 self._verifynodeconstraints(prev, expected, seen)
525
525
526 def _verifynodeconstraints(self, prev, expected, seen):
526 def _verifynodeconstraints(self, prev, expected, seen):
527 # by default command need a node in the edited list
527 # by default command need a node in the edited list
528 if self.node not in expected:
528 if self.node not in expected:
529 raise error.ParseError(
529 raise error.ParseError(
530 _(b'%s "%s" changeset was not a candidate')
530 _(b'%s "%s" changeset was not a candidate')
531 % (self.verb, short(self.node)),
531 % (self.verb, short(self.node)),
532 hint=_(b'only use listed changesets'),
532 hint=_(b'only use listed changesets'),
533 )
533 )
534 # and only one command per node
534 # and only one command per node
535 if self.node in seen:
535 if self.node in seen:
536 raise error.ParseError(
536 raise error.ParseError(
537 _(b'duplicated command for changeset %s') % short(self.node)
537 _(b'duplicated command for changeset %s') % short(self.node)
538 )
538 )
539
539
540 def torule(self):
540 def torule(self):
541 """build a histedit rule line for an action
541 """build a histedit rule line for an action
542
542
543 by default lines are in the form:
543 by default lines are in the form:
544 <hash> <rev> <summary>
544 <hash> <rev> <summary>
545 """
545 """
546 ctx = self.repo[self.node]
546 ctx = self.repo[self.node]
547 ui = self.repo.ui
547 ui = self.repo.ui
548 # We don't want color codes in the commit message template, so
548 # We don't want color codes in the commit message template, so
549 # disable the label() template function while we render it.
549 # disable the label() template function while we render it.
550 with ui.configoverride(
550 with ui.configoverride(
551 {(b'templatealias', b'label(l,x)'): b"x"}, b'histedit'
551 {(b'templatealias', b'label(l,x)'): b"x"}, b'histedit'
552 ):
552 ):
553 summary = cmdutil.rendertemplate(
553 summary = cmdutil.rendertemplate(
554 ctx, ui.config(b'histedit', b'summary-template')
554 ctx, ui.config(b'histedit', b'summary-template')
555 )
555 )
556 # Handle the fact that `''.splitlines() => []`
556 # Handle the fact that `''.splitlines() => []`
557 summary = summary.splitlines()[0] if summary else b''
557 summary = summary.splitlines()[0] if summary else b''
558 line = b'%s %s %s' % (self.verb, ctx, summary)
558 line = b'%s %s %s' % (self.verb, ctx, summary)
559 # trim to 75 columns by default so it's not stupidly wide in my editor
559 # trim to 75 columns by default so it's not stupidly wide in my editor
560 # (the 5 more are left for verb)
560 # (the 5 more are left for verb)
561 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
561 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
562 maxlen = max(maxlen, 22) # avoid truncating hash
562 maxlen = max(maxlen, 22) # avoid truncating hash
563 return stringutil.ellipsis(line, maxlen)
563 return stringutil.ellipsis(line, maxlen)
564
564
565 def tostate(self):
565 def tostate(self):
566 """Print an action in format used by histedit state files
566 """Print an action in format used by histedit state files
567 (the first line is a verb, the remainder is the second)
567 (the first line is a verb, the remainder is the second)
568 """
568 """
569 return b"%s\n%s" % (self.verb, hex(self.node))
569 return b"%s\n%s" % (self.verb, hex(self.node))
570
570
571 def run(self):
571 def run(self):
572 """Runs the action. The default behavior is simply apply the action's
572 """Runs the action. The default behavior is simply apply the action's
573 rulectx onto the current parentctx."""
573 rulectx onto the current parentctx."""
574 self.applychange()
574 self.applychange()
575 self.continuedirty()
575 self.continuedirty()
576 return self.continueclean()
576 return self.continueclean()
577
577
578 def applychange(self):
578 def applychange(self):
579 """Applies the changes from this action's rulectx onto the current
579 """Applies the changes from this action's rulectx onto the current
580 parentctx, but does not commit them."""
580 parentctx, but does not commit them."""
581 repo = self.repo
581 repo = self.repo
582 rulectx = repo[self.node]
582 rulectx = repo[self.node]
583 with repo.ui.silent():
583 with repo.ui.silent():
584 hg.update(repo, self.state.parentctxnode, quietempty=True)
584 hg.update(repo, self.state.parentctxnode, quietempty=True)
585 stats = applychanges(repo.ui, repo, rulectx, {})
585 stats = applychanges(repo.ui, repo, rulectx, {})
586 repo.dirstate.setbranch(rulectx.branch())
586 repo.dirstate.setbranch(rulectx.branch())
587 if stats.unresolvedcount:
587 if stats.unresolvedcount:
588 raise error.InterventionRequired(
588 raise error.InterventionRequired(
589 _(b'Fix up the change (%s %s)') % (self.verb, short(self.node)),
589 _(b'Fix up the change (%s %s)') % (self.verb, short(self.node)),
590 hint=_(b'hg histedit --continue to resume'),
590 hint=_(b'hg histedit --continue to resume'),
591 )
591 )
592
592
593 def continuedirty(self):
593 def continuedirty(self):
594 """Continues the action when changes have been applied to the working
594 """Continues the action when changes have been applied to the working
595 copy. The default behavior is to commit the dirty changes."""
595 copy. The default behavior is to commit the dirty changes."""
596 repo = self.repo
596 repo = self.repo
597 rulectx = repo[self.node]
597 rulectx = repo[self.node]
598
598
599 editor = self.commiteditor()
599 editor = self.commiteditor()
600 commit = commitfuncfor(repo, rulectx)
600 commit = commitfuncfor(repo, rulectx)
601 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
601 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
602 date = dateutil.makedate()
602 date = dateutil.makedate()
603 else:
603 else:
604 date = rulectx.date()
604 date = rulectx.date()
605 commit(
605 commit(
606 text=rulectx.description(),
606 text=rulectx.description(),
607 user=rulectx.user(),
607 user=rulectx.user(),
608 date=date,
608 date=date,
609 extra=rulectx.extra(),
609 extra=rulectx.extra(),
610 editor=editor,
610 editor=editor,
611 )
611 )
612
612
613 def commiteditor(self):
613 def commiteditor(self):
614 """The editor to be used to edit the commit message."""
614 """The editor to be used to edit the commit message."""
615 return False
615 return False
616
616
617 def continueclean(self):
617 def continueclean(self):
618 """Continues the action when the working copy is clean. The default
618 """Continues the action when the working copy is clean. The default
619 behavior is to accept the current commit as the new version of the
619 behavior is to accept the current commit as the new version of the
620 rulectx."""
620 rulectx."""
621 ctx = self.repo[b'.']
621 ctx = self.repo[b'.']
622 if ctx.node() == self.state.parentctxnode:
622 if ctx.node() == self.state.parentctxnode:
623 self.repo.ui.warn(
623 self.repo.ui.warn(
624 _(b'%s: skipping changeset (no changes)\n') % short(self.node)
624 _(b'%s: skipping changeset (no changes)\n') % short(self.node)
625 )
625 )
626 return ctx, [(self.node, tuple())]
626 return ctx, [(self.node, tuple())]
627 if ctx.node() == self.node:
627 if ctx.node() == self.node:
628 # Nothing changed
628 # Nothing changed
629 return ctx, []
629 return ctx, []
630 return ctx, [(self.node, (ctx.node(),))]
630 return ctx, [(self.node, (ctx.node(),))]
631
631
632
632
633 def commitfuncfor(repo, src):
633 def commitfuncfor(repo, src):
634 """Build a commit function for the replacement of <src>
634 """Build a commit function for the replacement of <src>
635
635
636 This function ensure we apply the same treatment to all changesets.
636 This function ensure we apply the same treatment to all changesets.
637
637
638 - Add a 'histedit_source' entry in extra.
638 - Add a 'histedit_source' entry in extra.
639
639
640 Note that fold has its own separated logic because its handling is a bit
640 Note that fold has its own separated logic because its handling is a bit
641 different and not easily factored out of the fold method.
641 different and not easily factored out of the fold method.
642 """
642 """
643 phasemin = src.phase()
643 phasemin = src.phase()
644
644
645 def commitfunc(**kwargs):
645 def commitfunc(**kwargs):
646 overrides = {(b'phases', b'new-commit'): phasemin}
646 overrides = {(b'phases', b'new-commit'): phasemin}
647 with repo.ui.configoverride(overrides, b'histedit'):
647 with repo.ui.configoverride(overrides, b'histedit'):
648 extra = kwargs.get('extra', {}).copy()
648 extra = kwargs.get('extra', {}).copy()
649 extra[b'histedit_source'] = src.hex()
649 extra[b'histedit_source'] = src.hex()
650 kwargs['extra'] = extra
650 kwargs['extra'] = extra
651 return repo.commit(**kwargs)
651 return repo.commit(**kwargs)
652
652
653 return commitfunc
653 return commitfunc
654
654
655
655
656 def applychanges(ui, repo, ctx, opts):
656 def applychanges(ui, repo, ctx, opts):
657 """Merge changeset from ctx (only) in the current working directory"""
657 """Merge changeset from ctx (only) in the current working directory"""
658 if ctx.p1().node() == repo.dirstate.p1():
658 if ctx.p1().node() == repo.dirstate.p1():
659 # edits are "in place" we do not need to make any merge,
659 # edits are "in place" we do not need to make any merge,
660 # just applies changes on parent for editing
660 # just applies changes on parent for editing
661 with ui.silent():
661 with ui.silent():
662 cmdutil.revert(ui, repo, ctx, all=True)
662 cmdutil.revert(ui, repo, ctx, all=True)
663 stats = mergemod.updateresult(0, 0, 0, 0)
663 stats = mergemod.updateresult(0, 0, 0, 0)
664 else:
664 else:
665 try:
665 try:
666 # ui.forcemerge is an internal variable, do not document
666 # ui.forcemerge is an internal variable, do not document
667 repo.ui.setconfig(
667 repo.ui.setconfig(
668 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
668 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
669 )
669 )
670 stats = mergemod.graft(
670 stats = mergemod.graft(
671 repo,
671 repo,
672 ctx,
672 ctx,
673 labels=[
673 labels=[
674 b'already edited',
674 b'already edited',
675 b'current change',
675 b'current change',
676 b'parent of current change',
676 b'parent of current change',
677 ],
677 ],
678 )
678 )
679 finally:
679 finally:
680 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
680 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
681 return stats
681 return stats
682
682
683
683
684 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
684 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
685 """collapse the set of revisions from first to last as new one.
685 """collapse the set of revisions from first to last as new one.
686
686
687 Expected commit options are:
687 Expected commit options are:
688 - message
688 - message
689 - date
689 - date
690 - username
690 - username
691 Commit message is edited in all cases.
691 Commit message is edited in all cases.
692
692
693 This function works in memory."""
693 This function works in memory."""
694 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
694 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
695 if not ctxs:
695 if not ctxs:
696 return None
696 return None
697 for c in ctxs:
697 for c in ctxs:
698 if not c.mutable():
698 if not c.mutable():
699 raise error.ParseError(
699 raise error.ParseError(
700 _(b"cannot fold into public change %s") % short(c.node())
700 _(b"cannot fold into public change %s") % short(c.node())
701 )
701 )
702 base = firstctx.p1()
702 base = firstctx.p1()
703
703
704 # commit a new version of the old changeset, including the update
704 # commit a new version of the old changeset, including the update
705 # collect all files which might be affected
705 # collect all files which might be affected
706 files = set()
706 files = set()
707 for ctx in ctxs:
707 for ctx in ctxs:
708 files.update(ctx.files())
708 files.update(ctx.files())
709
709
710 # Recompute copies (avoid recording a -> b -> a)
710 # Recompute copies (avoid recording a -> b -> a)
711 copied = copies.pathcopies(base, lastctx)
711 copied = copies.pathcopies(base, lastctx)
712
712
713 # prune files which were reverted by the updates
713 # prune files which were reverted by the updates
714 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
714 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
715 # commit version of these files as defined by head
715 # commit version of these files as defined by head
716 headmf = lastctx.manifest()
716 headmf = lastctx.manifest()
717
717
718 def filectxfn(repo, ctx, path):
718 def filectxfn(repo, ctx, path):
719 if path in headmf:
719 if path in headmf:
720 fctx = lastctx[path]
720 fctx = lastctx[path]
721 flags = fctx.flags()
721 flags = fctx.flags()
722 mctx = context.memfilectx(
722 mctx = context.memfilectx(
723 repo,
723 repo,
724 ctx,
724 ctx,
725 fctx.path(),
725 fctx.path(),
726 fctx.data(),
726 fctx.data(),
727 islink=b'l' in flags,
727 islink=b'l' in flags,
728 isexec=b'x' in flags,
728 isexec=b'x' in flags,
729 copysource=copied.get(path),
729 copysource=copied.get(path),
730 )
730 )
731 return mctx
731 return mctx
732 return None
732 return None
733
733
734 if commitopts.get(b'message'):
734 if commitopts.get(b'message'):
735 message = commitopts[b'message']
735 message = commitopts[b'message']
736 else:
736 else:
737 message = firstctx.description()
737 message = firstctx.description()
738 user = commitopts.get(b'user')
738 user = commitopts.get(b'user')
739 date = commitopts.get(b'date')
739 date = commitopts.get(b'date')
740 extra = commitopts.get(b'extra')
740 extra = commitopts.get(b'extra')
741
741
742 parents = (firstctx.p1().node(), firstctx.p2().node())
742 parents = (firstctx.p1().node(), firstctx.p2().node())
743 editor = None
743 editor = None
744 if not skipprompt:
744 if not skipprompt:
745 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
745 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
746 new = context.memctx(
746 new = context.memctx(
747 repo,
747 repo,
748 parents=parents,
748 parents=parents,
749 text=message,
749 text=message,
750 files=files,
750 files=files,
751 filectxfn=filectxfn,
751 filectxfn=filectxfn,
752 user=user,
752 user=user,
753 date=date,
753 date=date,
754 extra=extra,
754 extra=extra,
755 editor=editor,
755 editor=editor,
756 )
756 )
757 return repo.commitctx(new)
757 return repo.commitctx(new)
758
758
759
759
760 def _isdirtywc(repo):
760 def _isdirtywc(repo):
761 return repo[None].dirty(missing=True)
761 return repo[None].dirty(missing=True)
762
762
763
763
764 def abortdirty():
764 def abortdirty():
765 raise error.StateError(
765 raise error.StateError(
766 _(b'working copy has pending changes'),
766 _(b'working copy has pending changes'),
767 hint=_(
767 hint=_(
768 b'amend, commit, or revert them and run histedit '
768 b'amend, commit, or revert them and run histedit '
769 b'--continue, or abort with histedit --abort'
769 b'--continue, or abort with histedit --abort'
770 ),
770 ),
771 )
771 )
772
772
773
773
774 def action(verbs, message, priority=False, internal=False):
774 def action(verbs, message, priority=False, internal=False):
775 def wrap(cls):
775 def wrap(cls):
776 assert not priority or not internal
776 assert not priority or not internal
777 verb = verbs[0]
777 verb = verbs[0]
778 if priority:
778 if priority:
779 primaryactions.add(verb)
779 primaryactions.add(verb)
780 elif internal:
780 elif internal:
781 internalactions.add(verb)
781 internalactions.add(verb)
782 elif len(verbs) > 1:
782 elif len(verbs) > 1:
783 secondaryactions.add(verb)
783 secondaryactions.add(verb)
784 else:
784 else:
785 tertiaryactions.add(verb)
785 tertiaryactions.add(verb)
786
786
787 cls.verb = verb
787 cls.verb = verb
788 cls.verbs = verbs
788 cls.verbs = verbs
789 cls.message = message
789 cls.message = message
790 for verb in verbs:
790 for verb in verbs:
791 actiontable[verb] = cls
791 actiontable[verb] = cls
792 return cls
792 return cls
793
793
794 return wrap
794 return wrap
795
795
796
796
797 @action([b'pick', b'p'], _(b'use commit'), priority=True)
797 @action([b'pick', b'p'], _(b'use commit'), priority=True)
798 class pick(histeditaction):
798 class pick(histeditaction):
799 def run(self):
799 def run(self):
800 rulectx = self.repo[self.node]
800 rulectx = self.repo[self.node]
801 if rulectx.p1().node() == self.state.parentctxnode:
801 if rulectx.p1().node() == self.state.parentctxnode:
802 self.repo.ui.debug(b'node %s unchanged\n' % short(self.node))
802 self.repo.ui.debug(b'node %s unchanged\n' % short(self.node))
803 return rulectx, []
803 return rulectx, []
804
804
805 return super(pick, self).run()
805 return super(pick, self).run()
806
806
807
807
808 @action(
808 @action(
809 [b'edit', b'e'],
809 [b'edit', b'e'],
810 _(b'use commit, but allow edits before making new commit'),
810 _(b'use commit, but allow edits before making new commit'),
811 priority=True,
811 priority=True,
812 )
812 )
813 class edit(histeditaction):
813 class edit(histeditaction):
814 def run(self):
814 def run(self):
815 repo = self.repo
815 repo = self.repo
816 rulectx = repo[self.node]
816 rulectx = repo[self.node]
817 hg.update(repo, self.state.parentctxnode, quietempty=True)
817 hg.update(repo, self.state.parentctxnode, quietempty=True)
818 applychanges(repo.ui, repo, rulectx, {})
818 applychanges(repo.ui, repo, rulectx, {})
819 hint = _(b'to edit %s, `hg histedit --continue` after making changes')
819 hint = _(b'to edit %s, `hg histedit --continue` after making changes')
820 raise error.InterventionRequired(
820 raise error.InterventionRequired(
821 _(b'Editing (%s), commit as needed now to split the change')
821 _(b'Editing (%s), commit as needed now to split the change')
822 % short(self.node),
822 % short(self.node),
823 hint=hint % short(self.node),
823 hint=hint % short(self.node),
824 )
824 )
825
825
826 def commiteditor(self):
826 def commiteditor(self):
827 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
827 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
828
828
829
829
830 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
830 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
831 class fold(histeditaction):
831 class fold(histeditaction):
832 def verify(self, prev, expected, seen):
832 def verify(self, prev, expected, seen):
833 """Verifies semantic correctness of the fold rule"""
833 """Verifies semantic correctness of the fold rule"""
834 super(fold, self).verify(prev, expected, seen)
834 super(fold, self).verify(prev, expected, seen)
835 repo = self.repo
835 repo = self.repo
836 if not prev:
836 if not prev:
837 c = repo[self.node].p1()
837 c = repo[self.node].p1()
838 elif not prev.verb in (b'pick', b'base'):
838 elif not prev.verb in (b'pick', b'base'):
839 return
839 return
840 else:
840 else:
841 c = repo[prev.node]
841 c = repo[prev.node]
842 if not c.mutable():
842 if not c.mutable():
843 raise error.ParseError(
843 raise error.ParseError(
844 _(b"cannot fold into public change %s") % short(c.node())
844 _(b"cannot fold into public change %s") % short(c.node())
845 )
845 )
846
846
847 def continuedirty(self):
847 def continuedirty(self):
848 repo = self.repo
848 repo = self.repo
849 rulectx = repo[self.node]
849 rulectx = repo[self.node]
850
850
851 commit = commitfuncfor(repo, rulectx)
851 commit = commitfuncfor(repo, rulectx)
852 commit(
852 commit(
853 text=b'fold-temp-revision %s' % short(self.node),
853 text=b'fold-temp-revision %s' % short(self.node),
854 user=rulectx.user(),
854 user=rulectx.user(),
855 date=rulectx.date(),
855 date=rulectx.date(),
856 extra=rulectx.extra(),
856 extra=rulectx.extra(),
857 )
857 )
858
858
859 def continueclean(self):
859 def continueclean(self):
860 repo = self.repo
860 repo = self.repo
861 ctx = repo[b'.']
861 ctx = repo[b'.']
862 rulectx = repo[self.node]
862 rulectx = repo[self.node]
863 parentctxnode = self.state.parentctxnode
863 parentctxnode = self.state.parentctxnode
864 if ctx.node() == parentctxnode:
864 if ctx.node() == parentctxnode:
865 repo.ui.warn(_(b'%s: empty changeset\n') % short(self.node))
865 repo.ui.warn(_(b'%s: empty changeset\n') % short(self.node))
866 return ctx, [(self.node, (parentctxnode,))]
866 return ctx, [(self.node, (parentctxnode,))]
867
867
868 parentctx = repo[parentctxnode]
868 parentctx = repo[parentctxnode]
869 newcommits = {
869 newcommits = {
870 c.node()
870 c.node()
871 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
871 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
872 }
872 }
873 if not newcommits:
873 if not newcommits:
874 repo.ui.warn(
874 repo.ui.warn(
875 _(
875 _(
876 b'%s: cannot fold - working copy is not a '
876 b'%s: cannot fold - working copy is not a '
877 b'descendant of previous commit %s\n'
877 b'descendant of previous commit %s\n'
878 )
878 )
879 % (short(self.node), short(parentctxnode))
879 % (short(self.node), short(parentctxnode))
880 )
880 )
881 return ctx, [(self.node, (ctx.node(),))]
881 return ctx, [(self.node, (ctx.node(),))]
882
882
883 middlecommits = newcommits.copy()
883 middlecommits = newcommits.copy()
884 middlecommits.discard(ctx.node())
884 middlecommits.discard(ctx.node())
885
885
886 return self.finishfold(
886 return self.finishfold(
887 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
887 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
888 )
888 )
889
889
890 def skipprompt(self):
890 def skipprompt(self):
891 """Returns true if the rule should skip the message editor.
891 """Returns true if the rule should skip the message editor.
892
892
893 For example, 'fold' wants to show an editor, but 'rollup'
893 For example, 'fold' wants to show an editor, but 'rollup'
894 doesn't want to.
894 doesn't want to.
895 """
895 """
896 return False
896 return False
897
897
898 def mergedescs(self):
898 def mergedescs(self):
899 """Returns true if the rule should merge messages of multiple changes.
899 """Returns true if the rule should merge messages of multiple changes.
900
900
901 This exists mainly so that 'rollup' rules can be a subclass of
901 This exists mainly so that 'rollup' rules can be a subclass of
902 'fold'.
902 'fold'.
903 """
903 """
904 return True
904 return True
905
905
906 def firstdate(self):
906 def firstdate(self):
907 """Returns true if the rule should preserve the date of the first
907 """Returns true if the rule should preserve the date of the first
908 change.
908 change.
909
909
910 This exists mainly so that 'rollup' rules can be a subclass of
910 This exists mainly so that 'rollup' rules can be a subclass of
911 'fold'.
911 'fold'.
912 """
912 """
913 return False
913 return False
914
914
915 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
915 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
916 mergemod.update(ctx.p1())
916 mergemod.update(ctx.p1())
917 ### prepare new commit data
917 ### prepare new commit data
918 commitopts = {}
918 commitopts = {}
919 commitopts[b'user'] = ctx.user()
919 commitopts[b'user'] = ctx.user()
920 # commit message
920 # commit message
921 if not self.mergedescs():
921 if not self.mergedescs():
922 newmessage = ctx.description()
922 newmessage = ctx.description()
923 else:
923 else:
924 newmessage = (
924 newmessage = (
925 b'\n***\n'.join(
925 b'\n***\n'.join(
926 [ctx.description()]
926 [ctx.description()]
927 + [repo[r].description() for r in internalchanges]
927 + [repo[r].description() for r in internalchanges]
928 + [oldctx.description()]
928 + [oldctx.description()]
929 )
929 )
930 + b'\n'
930 + b'\n'
931 )
931 )
932 commitopts[b'message'] = newmessage
932 commitopts[b'message'] = newmessage
933 # date
933 # date
934 if self.firstdate():
934 if self.firstdate():
935 commitopts[b'date'] = ctx.date()
935 commitopts[b'date'] = ctx.date()
936 else:
936 else:
937 commitopts[b'date'] = max(ctx.date(), oldctx.date())
937 commitopts[b'date'] = max(ctx.date(), oldctx.date())
938 # if date is to be updated to current
938 # if date is to be updated to current
939 if ui.configbool(b'rewrite', b'update-timestamp'):
939 if ui.configbool(b'rewrite', b'update-timestamp'):
940 commitopts[b'date'] = dateutil.makedate()
940 commitopts[b'date'] = dateutil.makedate()
941
941
942 extra = ctx.extra().copy()
942 extra = ctx.extra().copy()
943 # histedit_source
943 # histedit_source
944 # note: ctx is likely a temporary commit but that the best we can do
944 # note: ctx is likely a temporary commit but that the best we can do
945 # here. This is sufficient to solve issue3681 anyway.
945 # here. This is sufficient to solve issue3681 anyway.
946 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
946 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
947 commitopts[b'extra'] = extra
947 commitopts[b'extra'] = extra
948 phasemin = max(ctx.phase(), oldctx.phase())
948 phasemin = max(ctx.phase(), oldctx.phase())
949 overrides = {(b'phases', b'new-commit'): phasemin}
949 overrides = {(b'phases', b'new-commit'): phasemin}
950 with repo.ui.configoverride(overrides, b'histedit'):
950 with repo.ui.configoverride(overrides, b'histedit'):
951 n = collapse(
951 n = collapse(
952 repo,
952 repo,
953 ctx,
953 ctx,
954 repo[newnode],
954 repo[newnode],
955 commitopts,
955 commitopts,
956 skipprompt=self.skipprompt(),
956 skipprompt=self.skipprompt(),
957 )
957 )
958 if n is None:
958 if n is None:
959 return ctx, []
959 return ctx, []
960 mergemod.update(repo[n])
960 mergemod.update(repo[n])
961 replacements = [
961 replacements = [
962 (oldctx.node(), (newnode,)),
962 (oldctx.node(), (newnode,)),
963 (ctx.node(), (n,)),
963 (ctx.node(), (n,)),
964 (newnode, (n,)),
964 (newnode, (n,)),
965 ]
965 ]
966 for ich in internalchanges:
966 for ich in internalchanges:
967 replacements.append((ich, (n,)))
967 replacements.append((ich, (n,)))
968 return repo[n], replacements
968 return repo[n], replacements
969
969
970
970
971 @action(
971 @action(
972 [b'base', b'b'],
972 [b'base', b'b'],
973 _(b'checkout changeset and apply further changesets from there'),
973 _(b'checkout changeset and apply further changesets from there'),
974 )
974 )
975 class base(histeditaction):
975 class base(histeditaction):
976 def run(self):
976 def run(self):
977 if self.repo[b'.'].node() != self.node:
977 if self.repo[b'.'].node() != self.node:
978 mergemod.clean_update(self.repo[self.node])
978 mergemod.clean_update(self.repo[self.node])
979 return self.continueclean()
979 return self.continueclean()
980
980
981 def continuedirty(self):
981 def continuedirty(self):
982 abortdirty()
982 abortdirty()
983
983
984 def continueclean(self):
984 def continueclean(self):
985 basectx = self.repo[b'.']
985 basectx = self.repo[b'.']
986 return basectx, []
986 return basectx, []
987
987
988 def _verifynodeconstraints(self, prev, expected, seen):
988 def _verifynodeconstraints(self, prev, expected, seen):
989 # base can only be use with a node not in the edited set
989 # base can only be use with a node not in the edited set
990 if self.node in expected:
990 if self.node in expected:
991 msg = _(b'%s "%s" changeset was an edited list candidate')
991 msg = _(b'%s "%s" changeset was an edited list candidate')
992 raise error.ParseError(
992 raise error.ParseError(
993 msg % (self.verb, short(self.node)),
993 msg % (self.verb, short(self.node)),
994 hint=_(b'base must only use unlisted changesets'),
994 hint=_(b'base must only use unlisted changesets'),
995 )
995 )
996
996
997
997
998 @action(
998 @action(
999 [b'_multifold'],
999 [b'_multifold'],
1000 _(
1000 _(
1001 """fold subclass used for when multiple folds happen in a row
1001 """fold subclass used for when multiple folds happen in a row
1002
1002
1003 We only want to fire the editor for the folded message once when
1003 We only want to fire the editor for the folded message once when
1004 (say) four changes are folded down into a single change. This is
1004 (say) four changes are folded down into a single change. This is
1005 similar to rollup, but we should preserve both messages so that
1005 similar to rollup, but we should preserve both messages so that
1006 when the last fold operation runs we can show the user all the
1006 when the last fold operation runs we can show the user all the
1007 commit messages in their editor.
1007 commit messages in their editor.
1008 """
1008 """
1009 ),
1009 ),
1010 internal=True,
1010 internal=True,
1011 )
1011 )
1012 class _multifold(fold):
1012 class _multifold(fold):
1013 def skipprompt(self):
1013 def skipprompt(self):
1014 return True
1014 return True
1015
1015
1016
1016
1017 @action(
1017 @action(
1018 [b"roll", b"r"],
1018 [b"roll", b"r"],
1019 _(b"like fold, but discard this commit's description and date"),
1019 _(b"like fold, but discard this commit's description and date"),
1020 )
1020 )
1021 class rollup(fold):
1021 class rollup(fold):
1022 def mergedescs(self):
1022 def mergedescs(self):
1023 return False
1023 return False
1024
1024
1025 def skipprompt(self):
1025 def skipprompt(self):
1026 return True
1026 return True
1027
1027
1028 def firstdate(self):
1028 def firstdate(self):
1029 return True
1029 return True
1030
1030
1031
1031
1032 @action([b"drop", b"d"], _(b'remove commit from history'))
1032 @action([b"drop", b"d"], _(b'remove commit from history'))
1033 class drop(histeditaction):
1033 class drop(histeditaction):
1034 def run(self):
1034 def run(self):
1035 parentctx = self.repo[self.state.parentctxnode]
1035 parentctx = self.repo[self.state.parentctxnode]
1036 return parentctx, [(self.node, tuple())]
1036 return parentctx, [(self.node, tuple())]
1037
1037
1038
1038
1039 @action(
1039 @action(
1040 [b"mess", b"m"],
1040 [b"mess", b"m"],
1041 _(b'edit commit message without changing commit content'),
1041 _(b'edit commit message without changing commit content'),
1042 priority=True,
1042 priority=True,
1043 )
1043 )
1044 class message(histeditaction):
1044 class message(histeditaction):
1045 def commiteditor(self):
1045 def commiteditor(self):
1046 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1046 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1047
1047
1048
1048
1049 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1049 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1050 """utility function to find the first outgoing changeset
1050 """utility function to find the first outgoing changeset
1051
1051
1052 Used by initialization code"""
1052 Used by initialization code"""
1053 if opts is None:
1053 if opts is None:
1054 opts = {}
1054 opts = {}
1055 path = urlutil.get_unique_push_path(b'histedit', repo, ui, remote)
1055 path = urlutil.get_unique_push_path(b'histedit', repo, ui, remote)
1056 dest = path.pushloc or path.loc
1056 dest = path.pushloc or path.loc
1057
1057
1058 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(dest))
1058 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(dest))
1059
1059
1060 revs, checkout = hg.addbranchrevs(repo, repo, (path.branch, []), None)
1060 revs, checkout = hg.addbranchrevs(repo, repo, (path.branch, []), None)
1061 other = hg.peer(repo, opts, dest)
1061 other = hg.peer(repo, opts, dest)
1062
1062
1063 if revs:
1063 if revs:
1064 revs = [repo.lookup(rev) for rev in revs]
1064 revs = [repo.lookup(rev) for rev in revs]
1065
1065
1066 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1066 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1067 if not outgoing.missing:
1067 if not outgoing.missing:
1068 raise error.StateError(_(b'no outgoing ancestors'))
1068 raise error.StateError(_(b'no outgoing ancestors'))
1069 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1069 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1070 if len(roots) > 1:
1070 if len(roots) > 1:
1071 msg = _(b'there are ambiguous outgoing revisions')
1071 msg = _(b'there are ambiguous outgoing revisions')
1072 hint = _(b"see 'hg help histedit' for more detail")
1072 hint = _(b"see 'hg help histedit' for more detail")
1073 raise error.StateError(msg, hint=hint)
1073 raise error.StateError(msg, hint=hint)
1074 return repo[roots[0]].node()
1074 return repo[roots[0]].node()
1075
1075
1076
1076
1077 # Curses Support
1077 # Curses Support
1078 try:
1078 try:
1079 import curses
1079 import curses
1080 except ImportError:
1080 except ImportError:
1081 curses = None
1081 curses = None
1082
1082
1083 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1083 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1084 ACTION_LABELS = {
1084 ACTION_LABELS = {
1085 b'fold': b'^fold',
1085 b'fold': b'^fold',
1086 b'roll': b'^roll',
1086 b'roll': b'^roll',
1087 }
1087 }
1088
1088
1089 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1089 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1090 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1090 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1091 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1091 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1092
1092
1093 E_QUIT, E_HISTEDIT = 1, 2
1093 E_QUIT, E_HISTEDIT = 1, 2
1094 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1094 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1095 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1095 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1096
1096
1097 KEYTABLE = {
1097 KEYTABLE = {
1098 b'global': {
1098 b'global': {
1099 b'h': b'next-action',
1099 b'h': b'next-action',
1100 b'KEY_RIGHT': b'next-action',
1100 b'KEY_RIGHT': b'next-action',
1101 b'l': b'prev-action',
1101 b'l': b'prev-action',
1102 b'KEY_LEFT': b'prev-action',
1102 b'KEY_LEFT': b'prev-action',
1103 b'q': b'quit',
1103 b'q': b'quit',
1104 b'c': b'histedit',
1104 b'c': b'histedit',
1105 b'C': b'histedit',
1105 b'C': b'histedit',
1106 b'v': b'showpatch',
1106 b'v': b'showpatch',
1107 b'?': b'help',
1107 b'?': b'help',
1108 },
1108 },
1109 MODE_RULES: {
1109 MODE_RULES: {
1110 b'd': b'action-drop',
1110 b'd': b'action-drop',
1111 b'e': b'action-edit',
1111 b'e': b'action-edit',
1112 b'f': b'action-fold',
1112 b'f': b'action-fold',
1113 b'm': b'action-mess',
1113 b'm': b'action-mess',
1114 b'p': b'action-pick',
1114 b'p': b'action-pick',
1115 b'r': b'action-roll',
1115 b'r': b'action-roll',
1116 b' ': b'select',
1116 b' ': b'select',
1117 b'j': b'down',
1117 b'j': b'down',
1118 b'k': b'up',
1118 b'k': b'up',
1119 b'KEY_DOWN': b'down',
1119 b'KEY_DOWN': b'down',
1120 b'KEY_UP': b'up',
1120 b'KEY_UP': b'up',
1121 b'J': b'move-down',
1121 b'J': b'move-down',
1122 b'K': b'move-up',
1122 b'K': b'move-up',
1123 b'KEY_NPAGE': b'move-down',
1123 b'KEY_NPAGE': b'move-down',
1124 b'KEY_PPAGE': b'move-up',
1124 b'KEY_PPAGE': b'move-up',
1125 b'0': b'goto', # Used for 0..9
1125 b'0': b'goto', # Used for 0..9
1126 },
1126 },
1127 MODE_PATCH: {
1127 MODE_PATCH: {
1128 b' ': b'page-down',
1128 b' ': b'page-down',
1129 b'KEY_NPAGE': b'page-down',
1129 b'KEY_NPAGE': b'page-down',
1130 b'KEY_PPAGE': b'page-up',
1130 b'KEY_PPAGE': b'page-up',
1131 b'j': b'line-down',
1131 b'j': b'line-down',
1132 b'k': b'line-up',
1132 b'k': b'line-up',
1133 b'KEY_DOWN': b'line-down',
1133 b'KEY_DOWN': b'line-down',
1134 b'KEY_UP': b'line-up',
1134 b'KEY_UP': b'line-up',
1135 b'J': b'down',
1135 b'J': b'down',
1136 b'K': b'up',
1136 b'K': b'up',
1137 },
1137 },
1138 MODE_HELP: {},
1138 MODE_HELP: {},
1139 }
1139 }
1140
1140
1141
1141
1142 def screen_size():
1142 def screen_size():
1143 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1143 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1144
1144
1145
1145
1146 class histeditrule(object):
1146 class histeditrule(object):
1147 def __init__(self, ui, ctx, pos, action=b'pick'):
1147 def __init__(self, ui, ctx, pos, action=b'pick'):
1148 self.ui = ui
1148 self.ui = ui
1149 self.ctx = ctx
1149 self.ctx = ctx
1150 self.action = action
1150 self.action = action
1151 self.origpos = pos
1151 self.origpos = pos
1152 self.pos = pos
1152 self.pos = pos
1153 self.conflicts = []
1153 self.conflicts = []
1154
1154
1155 def __bytes__(self):
1155 def __bytes__(self):
1156 # Example display of several histeditrules:
1156 # Example display of several histeditrules:
1157 #
1157 #
1158 # #10 pick 316392:06a16c25c053 add option to skip tests
1158 # #10 pick 316392:06a16c25c053 add option to skip tests
1159 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1159 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1160 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1160 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1161 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1161 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1162 #
1162 #
1163 # The carets point to the changeset being folded into ("roll this
1163 # The carets point to the changeset being folded into ("roll this
1164 # changeset into the changeset above").
1164 # changeset into the changeset above").
1165 return b'%s%s' % (self.prefix, self.desc)
1165 return b'%s%s' % (self.prefix, self.desc)
1166
1166
1167 __str__ = encoding.strmethod(__bytes__)
1167 __str__ = encoding.strmethod(__bytes__)
1168
1168
1169 @property
1169 @property
1170 def prefix(self):
1170 def prefix(self):
1171 # Some actions ('fold' and 'roll') combine a patch with a
1171 # Some actions ('fold' and 'roll') combine a patch with a
1172 # previous one. Add a marker showing which patch they apply
1172 # previous one. Add a marker showing which patch they apply
1173 # to.
1173 # to.
1174 action = ACTION_LABELS.get(self.action, self.action)
1174 action = ACTION_LABELS.get(self.action, self.action)
1175
1175
1176 h = self.ctx.hex()[0:12]
1176 h = self.ctx.hex()[0:12]
1177 r = self.ctx.rev()
1177 r = self.ctx.rev()
1178
1178
1179 return b"#%s %s %d:%s " % (
1179 return b"#%s %s %d:%s " % (
1180 (b'%d' % self.origpos).ljust(2),
1180 (b'%d' % self.origpos).ljust(2),
1181 action.ljust(6),
1181 action.ljust(6),
1182 r,
1182 r,
1183 h,
1183 h,
1184 )
1184 )
1185
1185
1186 @util.propertycache
1186 @util.propertycache
1187 def desc(self):
1187 def desc(self):
1188 summary = cmdutil.rendertemplate(
1188 summary = cmdutil.rendertemplate(
1189 self.ctx, self.ui.config(b'histedit', b'summary-template')
1189 self.ctx, self.ui.config(b'histedit', b'summary-template')
1190 )
1190 )
1191 if summary:
1191 if summary:
1192 return summary
1192 return summary
1193 # This is split off from the prefix property so that we can
1193 # This is split off from the prefix property so that we can
1194 # separately make the description for 'roll' red (since it
1194 # separately make the description for 'roll' red (since it
1195 # will get discarded).
1195 # will get discarded).
1196 return self.ctx.description().splitlines()[0].strip()
1196 return self.ctx.description().splitlines()[0].strip()
1197
1197
1198 def checkconflicts(self, other):
1198 def checkconflicts(self, other):
1199 if other.pos > self.pos and other.origpos <= self.origpos:
1199 if other.pos > self.pos and other.origpos <= self.origpos:
1200 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1200 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1201 self.conflicts.append(other)
1201 self.conflicts.append(other)
1202 return self.conflicts
1202 return self.conflicts
1203
1203
1204 if other in self.conflicts:
1204 if other in self.conflicts:
1205 self.conflicts.remove(other)
1205 self.conflicts.remove(other)
1206 return self.conflicts
1206 return self.conflicts
1207
1207
1208
1208
1209 def makecommands(rules):
1209 def makecommands(rules):
1210 """Returns a list of commands consumable by histedit --commands based on
1210 """Returns a list of commands consumable by histedit --commands based on
1211 our list of rules"""
1211 our list of rules"""
1212 commands = []
1212 commands = []
1213 for rules in rules:
1213 for rules in rules:
1214 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1214 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1215 return commands
1215 return commands
1216
1216
1217
1217
1218 def addln(win, y, x, line, color=None):
1218 def addln(win, y, x, line, color=None):
1219 """Add a line to the given window left padding but 100% filled with
1219 """Add a line to the given window left padding but 100% filled with
1220 whitespace characters, so that the color appears on the whole line"""
1220 whitespace characters, so that the color appears on the whole line"""
1221 maxy, maxx = win.getmaxyx()
1221 maxy, maxx = win.getmaxyx()
1222 length = maxx - 1 - x
1222 length = maxx - 1 - x
1223 line = bytes(line).ljust(length)[:length]
1223 line = bytes(line).ljust(length)[:length]
1224 if y < 0:
1224 if y < 0:
1225 y = maxy + y
1225 y = maxy + y
1226 if x < 0:
1226 if x < 0:
1227 x = maxx + x
1227 x = maxx + x
1228 if color:
1228 if color:
1229 win.addstr(y, x, line, color)
1229 win.addstr(y, x, line, color)
1230 else:
1230 else:
1231 win.addstr(y, x, line)
1231 win.addstr(y, x, line)
1232
1232
1233
1233
1234 def _trunc_head(line, n):
1234 def _trunc_head(line, n):
1235 if len(line) <= n:
1235 if len(line) <= n:
1236 return line
1236 return line
1237 return b'> ' + line[-(n - 2) :]
1237 return b'> ' + line[-(n - 2) :]
1238
1238
1239
1239
1240 def _trunc_tail(line, n):
1240 def _trunc_tail(line, n):
1241 if len(line) <= n:
1241 if len(line) <= n:
1242 return line
1242 return line
1243 return line[: n - 2] + b' >'
1243 return line[: n - 2] + b' >'
1244
1244
1245
1245
1246 class _chistedit_state(object):
1246 class _chistedit_state(object):
1247 def __init__(
1247 def __init__(
1248 self,
1248 self,
1249 repo,
1249 repo,
1250 rules,
1250 rules,
1251 stdscr,
1251 stdscr,
1252 ):
1252 ):
1253 self.repo = repo
1253 self.repo = repo
1254 self.rules = rules
1254 self.rules = rules
1255 self.stdscr = stdscr
1255 self.stdscr = stdscr
1256 self.later_on_top = repo.ui.configbool(
1256 self.later_on_top = repo.ui.configbool(
1257 b'histedit', b'later-commits-first'
1257 b'histedit', b'later-commits-first'
1258 )
1258 )
1259 # The current item in display order, initialized to point to the top
1259 # The current item in display order, initialized to point to the top
1260 # of the screen.
1260 # of the screen.
1261 self.pos = 0
1261 self.pos = 0
1262 self.selected = None
1262 self.selected = None
1263 self.mode = (MODE_INIT, MODE_INIT)
1263 self.mode = (MODE_INIT, MODE_INIT)
1264 self.page_height = None
1264 self.page_height = None
1265 self.modes = {
1265 self.modes = {
1266 MODE_RULES: {
1266 MODE_RULES: {
1267 b'line_offset': 0,
1267 b'line_offset': 0,
1268 },
1268 },
1269 MODE_PATCH: {
1269 MODE_PATCH: {
1270 b'line_offset': 0,
1270 b'line_offset': 0,
1271 },
1271 },
1272 }
1272 }
1273
1273
1274 def render_commit(self, win):
1274 def render_commit(self, win):
1275 """Renders the commit window that shows the log of the current selected
1275 """Renders the commit window that shows the log of the current selected
1276 commit"""
1276 commit"""
1277 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1277 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1278
1278
1279 ctx = rule.ctx
1279 ctx = rule.ctx
1280 win.box()
1280 win.box()
1281
1281
1282 maxy, maxx = win.getmaxyx()
1282 maxy, maxx = win.getmaxyx()
1283 length = maxx - 3
1283 length = maxx - 3
1284
1284
1285 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1285 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1286 win.addstr(1, 1, line[:length])
1286 win.addstr(1, 1, line[:length])
1287
1287
1288 line = b"user: %s" % ctx.user()
1288 line = b"user: %s" % ctx.user()
1289 win.addstr(2, 1, line[:length])
1289 win.addstr(2, 1, line[:length])
1290
1290
1291 bms = self.repo.nodebookmarks(ctx.node())
1291 bms = self.repo.nodebookmarks(ctx.node())
1292 line = b"bookmark: %s" % b' '.join(bms)
1292 line = b"bookmark: %s" % b' '.join(bms)
1293 win.addstr(3, 1, line[:length])
1293 win.addstr(3, 1, line[:length])
1294
1294
1295 line = b"summary: %s" % (ctx.description().splitlines()[0])
1295 line = b"summary: %s" % (ctx.description().splitlines()[0])
1296 win.addstr(4, 1, line[:length])
1296 win.addstr(4, 1, line[:length])
1297
1297
1298 line = b"files: "
1298 line = b"files: "
1299 win.addstr(5, 1, line)
1299 win.addstr(5, 1, line)
1300 fnx = 1 + len(line)
1300 fnx = 1 + len(line)
1301 fnmaxx = length - fnx + 1
1301 fnmaxx = length - fnx + 1
1302 y = 5
1302 y = 5
1303 fnmaxn = maxy - (1 + y) - 1
1303 fnmaxn = maxy - (1 + y) - 1
1304 files = ctx.files()
1304 files = ctx.files()
1305 for i, line1 in enumerate(files):
1305 for i, line1 in enumerate(files):
1306 if len(files) > fnmaxn and i == fnmaxn - 1:
1306 if len(files) > fnmaxn and i == fnmaxn - 1:
1307 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1307 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1308 y = y + 1
1308 y = y + 1
1309 break
1309 break
1310 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1310 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1311 y = y + 1
1311 y = y + 1
1312
1312
1313 conflicts = rule.conflicts
1313 conflicts = rule.conflicts
1314 if len(conflicts) > 0:
1314 if len(conflicts) > 0:
1315 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1315 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1316 conflictstr = b"changed files overlap with %s" % conflictstr
1316 conflictstr = b"changed files overlap with %s" % conflictstr
1317 else:
1317 else:
1318 conflictstr = b'no overlap'
1318 conflictstr = b'no overlap'
1319
1319
1320 win.addstr(y, 1, conflictstr[:length])
1320 win.addstr(y, 1, conflictstr[:length])
1321 win.noutrefresh()
1321 win.noutrefresh()
1322
1322
1323 def helplines(self):
1323 def helplines(self):
1324 if self.mode[0] == MODE_PATCH:
1324 if self.mode[0] == MODE_PATCH:
1325 help = b"""\
1325 help = b"""\
1326 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1326 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1327 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1327 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1328 """
1328 """
1329 else:
1329 else:
1330 help = b"""\
1330 help = b"""\
1331 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1331 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1332 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1332 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1333 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1333 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1334 """
1334 """
1335 if self.later_on_top:
1335 if self.later_on_top:
1336 help += b"Newer commits are shown above older commits.\n"
1336 help += b"Newer commits are shown above older commits.\n"
1337 else:
1337 else:
1338 help += b"Older commits are shown above newer commits.\n"
1338 help += b"Older commits are shown above newer commits.\n"
1339 return help.splitlines()
1339 return help.splitlines()
1340
1340
1341 def render_help(self, win):
1341 def render_help(self, win):
1342 maxy, maxx = win.getmaxyx()
1342 maxy, maxx = win.getmaxyx()
1343 for y, line in enumerate(self.helplines()):
1343 for y, line in enumerate(self.helplines()):
1344 if y >= maxy:
1344 if y >= maxy:
1345 break
1345 break
1346 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1346 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1347 win.noutrefresh()
1347 win.noutrefresh()
1348
1348
1349 def layout(self):
1349 def layout(self):
1350 maxy, maxx = self.stdscr.getmaxyx()
1350 maxy, maxx = self.stdscr.getmaxyx()
1351 helplen = len(self.helplines())
1351 helplen = len(self.helplines())
1352 mainlen = maxy - helplen - 12
1352 mainlen = maxy - helplen - 12
1353 if mainlen < 1:
1353 if mainlen < 1:
1354 raise error.Abort(
1354 raise error.Abort(
1355 _(b"terminal dimensions %d by %d too small for curses histedit")
1355 _(b"terminal dimensions %d by %d too small for curses histedit")
1356 % (maxy, maxx),
1356 % (maxy, maxx),
1357 hint=_(
1357 hint=_(
1358 b"enlarge your terminal or use --config ui.interface=text"
1358 b"enlarge your terminal or use --config ui.interface=text"
1359 ),
1359 ),
1360 )
1360 )
1361 return {
1361 return {
1362 b'commit': (12, maxx),
1362 b'commit': (12, maxx),
1363 b'help': (helplen, maxx),
1363 b'help': (helplen, maxx),
1364 b'main': (mainlen, maxx),
1364 b'main': (mainlen, maxx),
1365 }
1365 }
1366
1366
1367 def display_pos_to_rule_pos(self, display_pos):
1367 def display_pos_to_rule_pos(self, display_pos):
1368 """Converts a position in display order to rule order.
1368 """Converts a position in display order to rule order.
1369
1369
1370 The `display_pos` is the order from the top in display order, not
1370 The `display_pos` is the order from the top in display order, not
1371 considering which items are currently visible on the screen. Thus,
1371 considering which items are currently visible on the screen. Thus,
1372 `display_pos=0` is the item at the top (possibly after scrolling to
1372 `display_pos=0` is the item at the top (possibly after scrolling to
1373 the top)
1373 the top)
1374 """
1374 """
1375 if self.later_on_top:
1375 if self.later_on_top:
1376 return len(self.rules) - 1 - display_pos
1376 return len(self.rules) - 1 - display_pos
1377 else:
1377 else:
1378 return display_pos
1378 return display_pos
1379
1379
1380 def render_rules(self, rulesscr):
1380 def render_rules(self, rulesscr):
1381 start = self.modes[MODE_RULES][b'line_offset']
1381 start = self.modes[MODE_RULES][b'line_offset']
1382
1382
1383 conflicts = [r.ctx for r in self.rules if r.conflicts]
1383 conflicts = [r.ctx for r in self.rules if r.conflicts]
1384 if len(conflicts) > 0:
1384 if len(conflicts) > 0:
1385 line = b"potential conflict in %s" % b','.join(
1385 line = b"potential conflict in %s" % b','.join(
1386 map(pycompat.bytestr, conflicts)
1386 map(pycompat.bytestr, conflicts)
1387 )
1387 )
1388 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1388 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1389
1389
1390 for display_pos in range(start, len(self.rules)):
1390 for display_pos in range(start, len(self.rules)):
1391 y = display_pos - start
1391 y = display_pos - start
1392 if y < 0 or y >= self.page_height:
1392 if y < 0 or y >= self.page_height:
1393 continue
1393 continue
1394 rule_pos = self.display_pos_to_rule_pos(display_pos)
1394 rule_pos = self.display_pos_to_rule_pos(display_pos)
1395 rule = self.rules[rule_pos]
1395 rule = self.rules[rule_pos]
1396 if len(rule.conflicts) > 0:
1396 if len(rule.conflicts) > 0:
1397 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1397 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1398 else:
1398 else:
1399 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1399 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1400
1400
1401 if display_pos == self.selected:
1401 if display_pos == self.selected:
1402 rollcolor = COLOR_ROLL_SELECTED
1402 rollcolor = COLOR_ROLL_SELECTED
1403 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1403 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1404 elif display_pos == self.pos:
1404 elif display_pos == self.pos:
1405 rollcolor = COLOR_ROLL_CURRENT
1405 rollcolor = COLOR_ROLL_CURRENT
1406 addln(
1406 addln(
1407 rulesscr,
1407 rulesscr,
1408 y,
1408 y,
1409 2,
1409 2,
1410 rule,
1410 rule,
1411 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1411 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1412 )
1412 )
1413 else:
1413 else:
1414 rollcolor = COLOR_ROLL
1414 rollcolor = COLOR_ROLL
1415 addln(rulesscr, y, 2, rule)
1415 addln(rulesscr, y, 2, rule)
1416
1416
1417 if rule.action == b'roll':
1417 if rule.action == b'roll':
1418 rulesscr.addstr(
1418 rulesscr.addstr(
1419 y,
1419 y,
1420 2 + len(rule.prefix),
1420 2 + len(rule.prefix),
1421 rule.desc,
1421 rule.desc,
1422 curses.color_pair(rollcolor),
1422 curses.color_pair(rollcolor),
1423 )
1423 )
1424
1424
1425 rulesscr.noutrefresh()
1425 rulesscr.noutrefresh()
1426
1426
1427 def render_string(self, win, output, diffcolors=False):
1427 def render_string(self, win, output, diffcolors=False):
1428 maxy, maxx = win.getmaxyx()
1428 maxy, maxx = win.getmaxyx()
1429 length = min(maxy - 1, len(output))
1429 length = min(maxy - 1, len(output))
1430 for y in range(0, length):
1430 for y in range(0, length):
1431 line = output[y]
1431 line = output[y]
1432 if diffcolors:
1432 if diffcolors:
1433 if line and line[0] == b'+':
1433 if line and line[0] == b'+':
1434 win.addstr(
1434 win.addstr(
1435 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1435 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1436 )
1436 )
1437 elif line and line[0] == b'-':
1437 elif line and line[0] == b'-':
1438 win.addstr(
1438 win.addstr(
1439 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1439 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1440 )
1440 )
1441 elif line.startswith(b'@@ '):
1441 elif line.startswith(b'@@ '):
1442 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1442 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1443 else:
1443 else:
1444 win.addstr(y, 0, line)
1444 win.addstr(y, 0, line)
1445 else:
1445 else:
1446 win.addstr(y, 0, line)
1446 win.addstr(y, 0, line)
1447 win.noutrefresh()
1447 win.noutrefresh()
1448
1448
1449 def render_patch(self, win):
1449 def render_patch(self, win):
1450 start = self.modes[MODE_PATCH][b'line_offset']
1450 start = self.modes[MODE_PATCH][b'line_offset']
1451 content = self.modes[MODE_PATCH][b'patchcontents']
1451 content = self.modes[MODE_PATCH][b'patchcontents']
1452 self.render_string(win, content[start:], diffcolors=True)
1452 self.render_string(win, content[start:], diffcolors=True)
1453
1453
1454 def event(self, ch):
1454 def event(self, ch):
1455 """Change state based on the current character input
1455 """Change state based on the current character input
1456
1456
1457 This takes the current state and based on the current character input from
1457 This takes the current state and based on the current character input from
1458 the user we change the state.
1458 the user we change the state.
1459 """
1459 """
1460 oldpos = self.pos
1460 oldpos = self.pos
1461
1461
1462 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1462 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1463 return E_RESIZE
1463 return E_RESIZE
1464
1464
1465 lookup_ch = ch
1465 lookup_ch = ch
1466 if ch is not None and b'0' <= ch <= b'9':
1466 if ch is not None and b'0' <= ch <= b'9':
1467 lookup_ch = b'0'
1467 lookup_ch = b'0'
1468
1468
1469 curmode, prevmode = self.mode
1469 curmode, prevmode = self.mode
1470 action = KEYTABLE[curmode].get(
1470 action = KEYTABLE[curmode].get(
1471 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1471 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1472 )
1472 )
1473 if action is None:
1473 if action is None:
1474 return
1474 return
1475 if action in (b'down', b'move-down'):
1475 if action in (b'down', b'move-down'):
1476 newpos = min(oldpos + 1, len(self.rules) - 1)
1476 newpos = min(oldpos + 1, len(self.rules) - 1)
1477 self.move_cursor(oldpos, newpos)
1477 self.move_cursor(oldpos, newpos)
1478 if self.selected is not None or action == b'move-down':
1478 if self.selected is not None or action == b'move-down':
1479 self.swap(oldpos, newpos)
1479 self.swap(oldpos, newpos)
1480 elif action in (b'up', b'move-up'):
1480 elif action in (b'up', b'move-up'):
1481 newpos = max(0, oldpos - 1)
1481 newpos = max(0, oldpos - 1)
1482 self.move_cursor(oldpos, newpos)
1482 self.move_cursor(oldpos, newpos)
1483 if self.selected is not None or action == b'move-up':
1483 if self.selected is not None or action == b'move-up':
1484 self.swap(oldpos, newpos)
1484 self.swap(oldpos, newpos)
1485 elif action == b'next-action':
1485 elif action == b'next-action':
1486 self.cycle_action(oldpos, next=True)
1486 self.cycle_action(oldpos, next=True)
1487 elif action == b'prev-action':
1487 elif action == b'prev-action':
1488 self.cycle_action(oldpos, next=False)
1488 self.cycle_action(oldpos, next=False)
1489 elif action == b'select':
1489 elif action == b'select':
1490 self.selected = oldpos if self.selected is None else None
1490 self.selected = oldpos if self.selected is None else None
1491 self.make_selection(self.selected)
1491 self.make_selection(self.selected)
1492 elif action == b'goto' and int(ch) < len(self.rules) <= 10:
1492 elif action == b'goto' and int(ch) < len(self.rules) <= 10:
1493 newrule = next((r for r in self.rules if r.origpos == int(ch)))
1493 newrule = next((r for r in self.rules if r.origpos == int(ch)))
1494 self.move_cursor(oldpos, newrule.pos)
1494 self.move_cursor(oldpos, newrule.pos)
1495 if self.selected is not None:
1495 if self.selected is not None:
1496 self.swap(oldpos, newrule.pos)
1496 self.swap(oldpos, newrule.pos)
1497 elif action.startswith(b'action-'):
1497 elif action.startswith(b'action-'):
1498 self.change_action(oldpos, action[7:])
1498 self.change_action(oldpos, action[7:])
1499 elif action == b'showpatch':
1499 elif action == b'showpatch':
1500 self.change_mode(MODE_PATCH if curmode != MODE_PATCH else prevmode)
1500 self.change_mode(MODE_PATCH if curmode != MODE_PATCH else prevmode)
1501 elif action == b'help':
1501 elif action == b'help':
1502 self.change_mode(MODE_HELP if curmode != MODE_HELP else prevmode)
1502 self.change_mode(MODE_HELP if curmode != MODE_HELP else prevmode)
1503 elif action == b'quit':
1503 elif action == b'quit':
1504 return E_QUIT
1504 return E_QUIT
1505 elif action == b'histedit':
1505 elif action == b'histedit':
1506 return E_HISTEDIT
1506 return E_HISTEDIT
1507 elif action == b'page-down':
1507 elif action == b'page-down':
1508 return E_PAGEDOWN
1508 return E_PAGEDOWN
1509 elif action == b'page-up':
1509 elif action == b'page-up':
1510 return E_PAGEUP
1510 return E_PAGEUP
1511 elif action == b'line-down':
1511 elif action == b'line-down':
1512 return E_LINEDOWN
1512 return E_LINEDOWN
1513 elif action == b'line-up':
1513 elif action == b'line-up':
1514 return E_LINEUP
1514 return E_LINEUP
1515
1515
1516 def patch_contents(self):
1516 def patch_contents(self):
1517 repo = self.repo
1517 repo = self.repo
1518 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1518 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1519 displayer = logcmdutil.changesetdisplayer(
1519 displayer = logcmdutil.changesetdisplayer(
1520 repo.ui,
1520 repo.ui,
1521 repo,
1521 repo,
1522 {b"patch": True, b"template": b"status"},
1522 {b"patch": True, b"template": b"status"},
1523 buffered=True,
1523 buffered=True,
1524 )
1524 )
1525 overrides = {(b'ui', b'verbose'): True}
1525 overrides = {(b'ui', b'verbose'): True}
1526 with repo.ui.configoverride(overrides, source=b'histedit'):
1526 with repo.ui.configoverride(overrides, source=b'histedit'):
1527 displayer.show(rule.ctx)
1527 displayer.show(rule.ctx)
1528 displayer.close()
1528 displayer.close()
1529 return displayer.hunk[rule.ctx.rev()].splitlines()
1529 return displayer.hunk[rule.ctx.rev()].splitlines()
1530
1530
1531 def move_cursor(self, oldpos, newpos):
1531 def move_cursor(self, oldpos, newpos):
1532 """Change the rule/changeset that the cursor is pointing to, regardless of
1532 """Change the rule/changeset that the cursor is pointing to, regardless of
1533 current mode (you can switch between patches from the view patch window)."""
1533 current mode (you can switch between patches from the view patch window)."""
1534 self.pos = newpos
1534 self.pos = newpos
1535
1535
1536 mode, _ = self.mode
1536 mode, _ = self.mode
1537 if mode == MODE_RULES:
1537 if mode == MODE_RULES:
1538 # Scroll through the list by updating the view for MODE_RULES, so that
1538 # Scroll through the list by updating the view for MODE_RULES, so that
1539 # even if we are not currently viewing the rules, switching back will
1539 # even if we are not currently viewing the rules, switching back will
1540 # result in the cursor's rule being visible.
1540 # result in the cursor's rule being visible.
1541 modestate = self.modes[MODE_RULES]
1541 modestate = self.modes[MODE_RULES]
1542 if newpos < modestate[b'line_offset']:
1542 if newpos < modestate[b'line_offset']:
1543 modestate[b'line_offset'] = newpos
1543 modestate[b'line_offset'] = newpos
1544 elif newpos > modestate[b'line_offset'] + self.page_height - 1:
1544 elif newpos > modestate[b'line_offset'] + self.page_height - 1:
1545 modestate[b'line_offset'] = newpos - self.page_height + 1
1545 modestate[b'line_offset'] = newpos - self.page_height + 1
1546
1546
1547 # Reset the patch view region to the top of the new patch.
1547 # Reset the patch view region to the top of the new patch.
1548 self.modes[MODE_PATCH][b'line_offset'] = 0
1548 self.modes[MODE_PATCH][b'line_offset'] = 0
1549
1549
1550 def change_mode(self, mode):
1550 def change_mode(self, mode):
1551 curmode, _ = self.mode
1551 curmode, _ = self.mode
1552 self.mode = (mode, curmode)
1552 self.mode = (mode, curmode)
1553 if mode == MODE_PATCH:
1553 if mode == MODE_PATCH:
1554 self.modes[MODE_PATCH][b'patchcontents'] = self.patch_contents()
1554 self.modes[MODE_PATCH][b'patchcontents'] = self.patch_contents()
1555
1555
1556 def make_selection(self, pos):
1556 def make_selection(self, pos):
1557 self.selected = pos
1557 self.selected = pos
1558
1558
1559 def swap(self, oldpos, newpos):
1559 def swap(self, oldpos, newpos):
1560 """Swap two positions and calculate necessary conflicts in
1560 """Swap two positions and calculate necessary conflicts in
1561 O(|newpos-oldpos|) time"""
1561 O(|newpos-oldpos|) time"""
1562 old_rule_pos = self.display_pos_to_rule_pos(oldpos)
1562 old_rule_pos = self.display_pos_to_rule_pos(oldpos)
1563 new_rule_pos = self.display_pos_to_rule_pos(newpos)
1563 new_rule_pos = self.display_pos_to_rule_pos(newpos)
1564
1564
1565 rules = self.rules
1565 rules = self.rules
1566 assert 0 <= old_rule_pos < len(rules) and 0 <= new_rule_pos < len(rules)
1566 assert 0 <= old_rule_pos < len(rules) and 0 <= new_rule_pos < len(rules)
1567
1567
1568 rules[old_rule_pos], rules[new_rule_pos] = (
1568 rules[old_rule_pos], rules[new_rule_pos] = (
1569 rules[new_rule_pos],
1569 rules[new_rule_pos],
1570 rules[old_rule_pos],
1570 rules[old_rule_pos],
1571 )
1571 )
1572
1572
1573 # TODO: swap should not know about histeditrule's internals
1573 # TODO: swap should not know about histeditrule's internals
1574 rules[new_rule_pos].pos = new_rule_pos
1574 rules[new_rule_pos].pos = new_rule_pos
1575 rules[old_rule_pos].pos = old_rule_pos
1575 rules[old_rule_pos].pos = old_rule_pos
1576
1576
1577 start = min(old_rule_pos, new_rule_pos)
1577 start = min(old_rule_pos, new_rule_pos)
1578 end = max(old_rule_pos, new_rule_pos)
1578 end = max(old_rule_pos, new_rule_pos)
1579 for r in pycompat.xrange(start, end + 1):
1579 for r in pycompat.xrange(start, end + 1):
1580 rules[new_rule_pos].checkconflicts(rules[r])
1580 rules[new_rule_pos].checkconflicts(rules[r])
1581 rules[old_rule_pos].checkconflicts(rules[r])
1581 rules[old_rule_pos].checkconflicts(rules[r])
1582
1582
1583 if self.selected:
1583 if self.selected:
1584 self.make_selection(newpos)
1584 self.make_selection(newpos)
1585
1585
1586 def change_action(self, pos, action):
1586 def change_action(self, pos, action):
1587 """Change the action state on the given position to the new action"""
1587 """Change the action state on the given position to the new action"""
1588 assert 0 <= pos < len(self.rules)
1588 assert 0 <= pos < len(self.rules)
1589 self.rules[pos].action = action
1589 self.rules[pos].action = action
1590
1590
1591 def cycle_action(self, pos, next=False):
1591 def cycle_action(self, pos, next=False):
1592 """Changes the action state the next or the previous action from
1592 """Changes the action state the next or the previous action from
1593 the action list"""
1593 the action list"""
1594 assert 0 <= pos < len(self.rules)
1594 assert 0 <= pos < len(self.rules)
1595 current = self.rules[pos].action
1595 current = self.rules[pos].action
1596
1596
1597 assert current in KEY_LIST
1597 assert current in KEY_LIST
1598
1598
1599 index = KEY_LIST.index(current)
1599 index = KEY_LIST.index(current)
1600 if next:
1600 if next:
1601 index += 1
1601 index += 1
1602 else:
1602 else:
1603 index -= 1
1603 index -= 1
1604 self.change_action(pos, KEY_LIST[index % len(KEY_LIST)])
1604 self.change_action(pos, KEY_LIST[index % len(KEY_LIST)])
1605
1605
1606 def change_view(self, delta, unit):
1606 def change_view(self, delta, unit):
1607 """Change the region of whatever is being viewed (a patch or the list of
1607 """Change the region of whatever is being viewed (a patch or the list of
1608 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
1608 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
1609 mode, _ = self.mode
1609 mode, _ = self.mode
1610 if mode != MODE_PATCH:
1610 if mode != MODE_PATCH:
1611 return
1611 return
1612 mode_state = self.modes[mode]
1612 mode_state = self.modes[mode]
1613 num_lines = len(mode_state[b'patchcontents'])
1613 num_lines = len(mode_state[b'patchcontents'])
1614 page_height = self.page_height
1614 page_height = self.page_height
1615 unit = page_height if unit == b'page' else 1
1615 unit = page_height if unit == b'page' else 1
1616 num_pages = 1 + (num_lines - 1) // page_height
1616 num_pages = 1 + (num_lines - 1) // page_height
1617 max_offset = (num_pages - 1) * page_height
1617 max_offset = (num_pages - 1) * page_height
1618 newline = mode_state[b'line_offset'] + delta * unit
1618 newline = mode_state[b'line_offset'] + delta * unit
1619 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1619 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1620
1620
1621
1621
1622 def _chisteditmain(repo, rules, stdscr):
1622 def _chisteditmain(repo, rules, stdscr):
1623 try:
1623 try:
1624 curses.use_default_colors()
1624 curses.use_default_colors()
1625 except curses.error:
1625 except curses.error:
1626 pass
1626 pass
1627
1627
1628 # initialize color pattern
1628 # initialize color pattern
1629 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1629 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1630 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1630 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1631 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1631 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1632 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1632 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1633 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1633 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1634 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1634 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1635 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1635 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1636 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1636 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1637 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1637 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1638 curses.init_pair(
1638 curses.init_pair(
1639 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1639 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1640 )
1640 )
1641 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1641 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1642
1642
1643 # don't display the cursor
1643 # don't display the cursor
1644 try:
1644 try:
1645 curses.curs_set(0)
1645 curses.curs_set(0)
1646 except curses.error:
1646 except curses.error:
1647 pass
1647 pass
1648
1648
1649 def drawvertwin(size, y, x):
1649 def drawvertwin(size, y, x):
1650 win = curses.newwin(size[0], size[1], y, x)
1650 win = curses.newwin(size[0], size[1], y, x)
1651 y += size[0]
1651 y += size[0]
1652 return win, y, x
1652 return win, y, x
1653
1653
1654 state = _chistedit_state(repo, rules, stdscr)
1654 state = _chistedit_state(repo, rules, stdscr)
1655
1655
1656 # eventloop
1656 # eventloop
1657 ch = None
1657 ch = None
1658 stdscr.clear()
1658 stdscr.clear()
1659 stdscr.refresh()
1659 stdscr.refresh()
1660 while True:
1660 while True:
1661 oldmode, unused = state.mode
1661 oldmode, unused = state.mode
1662 if oldmode == MODE_INIT:
1662 if oldmode == MODE_INIT:
1663 state.change_mode(MODE_RULES)
1663 state.change_mode(MODE_RULES)
1664 e = state.event(ch)
1664 e = state.event(ch)
1665
1665
1666 if e == E_QUIT:
1666 if e == E_QUIT:
1667 return False
1667 return False
1668 if e == E_HISTEDIT:
1668 if e == E_HISTEDIT:
1669 return state.rules
1669 return state.rules
1670 else:
1670 else:
1671 if e == E_RESIZE:
1671 if e == E_RESIZE:
1672 size = screen_size()
1672 size = screen_size()
1673 if size != stdscr.getmaxyx():
1673 if size != stdscr.getmaxyx():
1674 curses.resizeterm(*size)
1674 curses.resizeterm(*size)
1675
1675
1676 sizes = state.layout()
1676 sizes = state.layout()
1677 curmode, unused = state.mode
1677 curmode, unused = state.mode
1678 if curmode != oldmode:
1678 if curmode != oldmode:
1679 state.page_height = sizes[b'main'][0]
1679 state.page_height = sizes[b'main'][0]
1680 # Adjust the view to fit the current screen size.
1680 # Adjust the view to fit the current screen size.
1681 state.move_cursor(state.pos, state.pos)
1681 state.move_cursor(state.pos, state.pos)
1682
1682
1683 # Pack the windows against the top, each pane spread across the
1683 # Pack the windows against the top, each pane spread across the
1684 # full width of the screen.
1684 # full width of the screen.
1685 y, x = (0, 0)
1685 y, x = (0, 0)
1686 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1686 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1687 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1687 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1688 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1688 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1689
1689
1690 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1690 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1691 if e == E_PAGEDOWN:
1691 if e == E_PAGEDOWN:
1692 state.change_view(+1, b'page')
1692 state.change_view(+1, b'page')
1693 elif e == E_PAGEUP:
1693 elif e == E_PAGEUP:
1694 state.change_view(-1, b'page')
1694 state.change_view(-1, b'page')
1695 elif e == E_LINEDOWN:
1695 elif e == E_LINEDOWN:
1696 state.change_view(+1, b'line')
1696 state.change_view(+1, b'line')
1697 elif e == E_LINEUP:
1697 elif e == E_LINEUP:
1698 state.change_view(-1, b'line')
1698 state.change_view(-1, b'line')
1699
1699
1700 # start rendering
1700 # start rendering
1701 commitwin.erase()
1701 commitwin.erase()
1702 helpwin.erase()
1702 helpwin.erase()
1703 mainwin.erase()
1703 mainwin.erase()
1704 if curmode == MODE_PATCH:
1704 if curmode == MODE_PATCH:
1705 state.render_patch(mainwin)
1705 state.render_patch(mainwin)
1706 elif curmode == MODE_HELP:
1706 elif curmode == MODE_HELP:
1707 state.render_string(mainwin, __doc__.strip().splitlines())
1707 state.render_string(mainwin, __doc__.strip().splitlines())
1708 else:
1708 else:
1709 state.render_rules(mainwin)
1709 state.render_rules(mainwin)
1710 state.render_commit(commitwin)
1710 state.render_commit(commitwin)
1711 state.render_help(helpwin)
1711 state.render_help(helpwin)
1712 curses.doupdate()
1712 curses.doupdate()
1713 # done rendering
1713 # done rendering
1714 ch = encoding.strtolocal(stdscr.getkey())
1714 ch = encoding.strtolocal(stdscr.getkey())
1715
1715
1716
1716
1717 def _chistedit(ui, repo, freeargs, opts):
1717 def _chistedit(ui, repo, freeargs, opts):
1718 """interactively edit changeset history via a curses interface
1718 """interactively edit changeset history via a curses interface
1719
1719
1720 Provides a ncurses interface to histedit. Press ? in chistedit mode
1720 Provides a ncurses interface to histedit. Press ? in chistedit mode
1721 to see an extensive help. Requires python-curses to be installed."""
1721 to see an extensive help. Requires python-curses to be installed."""
1722
1722
1723 if curses is None:
1723 if curses is None:
1724 raise error.Abort(_(b"Python curses library required"))
1724 raise error.Abort(_(b"Python curses library required"))
1725
1725
1726 # disable color
1726 # disable color
1727 ui._colormode = None
1727 ui._colormode = None
1728
1728
1729 try:
1729 try:
1730 keep = opts.get(b'keep')
1730 keep = opts.get(b'keep')
1731 revs = opts.get(b'rev', [])[:]
1731 revs = opts.get(b'rev', [])[:]
1732 cmdutil.checkunfinished(repo)
1732 cmdutil.checkunfinished(repo)
1733 cmdutil.bailifchanged(repo)
1733 cmdutil.bailifchanged(repo)
1734
1734
1735 revs.extend(freeargs)
1735 revs.extend(freeargs)
1736 if not revs:
1736 if not revs:
1737 defaultrev = destutil.desthistedit(ui, repo)
1737 defaultrev = destutil.desthistedit(ui, repo)
1738 if defaultrev is not None:
1738 if defaultrev is not None:
1739 revs.append(defaultrev)
1739 revs.append(defaultrev)
1740 if len(revs) != 1:
1740 if len(revs) != 1:
1741 raise error.InputError(
1741 raise error.InputError(
1742 _(b'histedit requires exactly one ancestor revision')
1742 _(b'histedit requires exactly one ancestor revision')
1743 )
1743 )
1744
1744
1745 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
1745 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
1746 if len(rr) != 1:
1746 if len(rr) != 1:
1747 raise error.InputError(
1747 raise error.InputError(
1748 _(
1748 _(
1749 b'The specified revisions must have '
1749 b'The specified revisions must have '
1750 b'exactly one common root'
1750 b'exactly one common root'
1751 )
1751 )
1752 )
1752 )
1753 root = rr[0].node()
1753 root = rr[0].node()
1754
1754
1755 topmost = repo.dirstate.p1()
1755 topmost = repo.dirstate.p1()
1756 revs = between(repo, root, topmost, keep)
1756 revs = between(repo, root, topmost, keep)
1757 if not revs:
1757 if not revs:
1758 raise error.InputError(
1758 raise error.InputError(
1759 _(b'%s is not an ancestor of working directory') % short(root)
1759 _(b'%s is not an ancestor of working directory') % short(root)
1760 )
1760 )
1761
1761
1762 rules = []
1762 rules = []
1763 for i, r in enumerate(revs):
1763 for i, r in enumerate(revs):
1764 rules.append(histeditrule(ui, repo[r], i))
1764 rules.append(histeditrule(ui, repo[r], i))
1765 with util.with_lc_ctype():
1765 with util.with_lc_ctype():
1766 rc = curses.wrapper(functools.partial(_chisteditmain, repo, rules))
1766 rc = curses.wrapper(functools.partial(_chisteditmain, repo, rules))
1767 curses.echo()
1767 curses.echo()
1768 curses.endwin()
1768 curses.endwin()
1769 if rc is False:
1769 if rc is False:
1770 ui.write(_(b"histedit aborted\n"))
1770 ui.write(_(b"histedit aborted\n"))
1771 return 0
1771 return 0
1772 if type(rc) is list:
1772 if type(rc) is list:
1773 ui.status(_(b"performing changes\n"))
1773 ui.status(_(b"performing changes\n"))
1774 rules = makecommands(rc)
1774 rules = makecommands(rc)
1775 with repo.vfs(b'chistedit', b'w+') as fp:
1775 with repo.vfs(b'chistedit', b'w+') as fp:
1776 for r in rules:
1776 for r in rules:
1777 fp.write(r)
1777 fp.write(r)
1778 opts[b'commands'] = fp.name
1778 opts[b'commands'] = fp.name
1779 return _texthistedit(ui, repo, freeargs, opts)
1779 return _texthistedit(ui, repo, freeargs, opts)
1780 except KeyboardInterrupt:
1780 except KeyboardInterrupt:
1781 pass
1781 pass
1782 return -1
1782 return -1
1783
1783
1784
1784
1785 @command(
1785 @command(
1786 b'histedit',
1786 b'histedit',
1787 [
1787 [
1788 (
1788 (
1789 b'',
1789 b'',
1790 b'commands',
1790 b'commands',
1791 b'',
1791 b'',
1792 _(b'read history edits from the specified file'),
1792 _(b'read history edits from the specified file'),
1793 _(b'FILE'),
1793 _(b'FILE'),
1794 ),
1794 ),
1795 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1795 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1796 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1796 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1797 (
1797 (
1798 b'k',
1798 b'k',
1799 b'keep',
1799 b'keep',
1800 False,
1800 False,
1801 _(b"don't strip old nodes after edit is complete"),
1801 _(b"don't strip old nodes after edit is complete"),
1802 ),
1802 ),
1803 (b'', b'abort', False, _(b'abort an edit in progress')),
1803 (b'', b'abort', False, _(b'abort an edit in progress')),
1804 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1804 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1805 (
1805 (
1806 b'f',
1806 b'f',
1807 b'force',
1807 b'force',
1808 False,
1808 False,
1809 _(b'force outgoing even for unrelated repositories'),
1809 _(b'force outgoing even for unrelated repositories'),
1810 ),
1810 ),
1811 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1811 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1812 ]
1812 ]
1813 + cmdutil.formatteropts,
1813 + cmdutil.formatteropts,
1814 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1814 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1815 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1815 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1816 )
1816 )
1817 def histedit(ui, repo, *freeargs, **opts):
1817 def histedit(ui, repo, *freeargs, **opts):
1818 """interactively edit changeset history
1818 """interactively edit changeset history
1819
1819
1820 This command lets you edit a linear series of changesets (up to
1820 This command lets you edit a linear series of changesets (up to
1821 and including the working directory, which should be clean).
1821 and including the working directory, which should be clean).
1822 You can:
1822 You can:
1823
1823
1824 - `pick` to [re]order a changeset
1824 - `pick` to [re]order a changeset
1825
1825
1826 - `drop` to omit changeset
1826 - `drop` to omit changeset
1827
1827
1828 - `mess` to reword the changeset commit message
1828 - `mess` to reword the changeset commit message
1829
1829
1830 - `fold` to combine it with the preceding changeset (using the later date)
1830 - `fold` to combine it with the preceding changeset (using the later date)
1831
1831
1832 - `roll` like fold, but discarding this commit's description and date
1832 - `roll` like fold, but discarding this commit's description and date
1833
1833
1834 - `edit` to edit this changeset (preserving date)
1834 - `edit` to edit this changeset (preserving date)
1835
1835
1836 - `base` to checkout changeset and apply further changesets from there
1836 - `base` to checkout changeset and apply further changesets from there
1837
1837
1838 There are a number of ways to select the root changeset:
1838 There are a number of ways to select the root changeset:
1839
1839
1840 - Specify ANCESTOR directly
1840 - Specify ANCESTOR directly
1841
1841
1842 - Use --outgoing -- it will be the first linear changeset not
1842 - Use --outgoing -- it will be the first linear changeset not
1843 included in destination. (See :hg:`help config.paths.default-push`)
1843 included in destination. (See :hg:`help config.paths.default-push`)
1844
1844
1845 - Otherwise, the value from the "histedit.defaultrev" config option
1845 - Otherwise, the value from the "histedit.defaultrev" config option
1846 is used as a revset to select the base revision when ANCESTOR is not
1846 is used as a revset to select the base revision when ANCESTOR is not
1847 specified. The first revision returned by the revset is used. By
1847 specified. The first revision returned by the revset is used. By
1848 default, this selects the editable history that is unique to the
1848 default, this selects the editable history that is unique to the
1849 ancestry of the working directory.
1849 ancestry of the working directory.
1850
1850
1851 .. container:: verbose
1851 .. container:: verbose
1852
1852
1853 If you use --outgoing, this command will abort if there are ambiguous
1853 If you use --outgoing, this command will abort if there are ambiguous
1854 outgoing revisions. For example, if there are multiple branches
1854 outgoing revisions. For example, if there are multiple branches
1855 containing outgoing revisions.
1855 containing outgoing revisions.
1856
1856
1857 Use "min(outgoing() and ::.)" or similar revset specification
1857 Use "min(outgoing() and ::.)" or similar revset specification
1858 instead of --outgoing to specify edit target revision exactly in
1858 instead of --outgoing to specify edit target revision exactly in
1859 such ambiguous situation. See :hg:`help revsets` for detail about
1859 such ambiguous situation. See :hg:`help revsets` for detail about
1860 selecting revisions.
1860 selecting revisions.
1861
1861
1862 .. container:: verbose
1862 .. container:: verbose
1863
1863
1864 Examples:
1864 Examples:
1865
1865
1866 - A number of changes have been made.
1866 - A number of changes have been made.
1867 Revision 3 is no longer needed.
1867 Revision 3 is no longer needed.
1868
1868
1869 Start history editing from revision 3::
1869 Start history editing from revision 3::
1870
1870
1871 hg histedit -r 3
1871 hg histedit -r 3
1872
1872
1873 An editor opens, containing the list of revisions,
1873 An editor opens, containing the list of revisions,
1874 with specific actions specified::
1874 with specific actions specified::
1875
1875
1876 pick 5339bf82f0ca 3 Zworgle the foobar
1876 pick 5339bf82f0ca 3 Zworgle the foobar
1877 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1877 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1878 pick 0a9639fcda9d 5 Morgify the cromulancy
1878 pick 0a9639fcda9d 5 Morgify the cromulancy
1879
1879
1880 Additional information about the possible actions
1880 Additional information about the possible actions
1881 to take appears below the list of revisions.
1881 to take appears below the list of revisions.
1882
1882
1883 To remove revision 3 from the history,
1883 To remove revision 3 from the history,
1884 its action (at the beginning of the relevant line)
1884 its action (at the beginning of the relevant line)
1885 is changed to 'drop'::
1885 is changed to 'drop'::
1886
1886
1887 drop 5339bf82f0ca 3 Zworgle the foobar
1887 drop 5339bf82f0ca 3 Zworgle the foobar
1888 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1888 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1889 pick 0a9639fcda9d 5 Morgify the cromulancy
1889 pick 0a9639fcda9d 5 Morgify the cromulancy
1890
1890
1891 - A number of changes have been made.
1891 - A number of changes have been made.
1892 Revision 2 and 4 need to be swapped.
1892 Revision 2 and 4 need to be swapped.
1893
1893
1894 Start history editing from revision 2::
1894 Start history editing from revision 2::
1895
1895
1896 hg histedit -r 2
1896 hg histedit -r 2
1897
1897
1898 An editor opens, containing the list of revisions,
1898 An editor opens, containing the list of revisions,
1899 with specific actions specified::
1899 with specific actions specified::
1900
1900
1901 pick 252a1af424ad 2 Blorb a morgwazzle
1901 pick 252a1af424ad 2 Blorb a morgwazzle
1902 pick 5339bf82f0ca 3 Zworgle the foobar
1902 pick 5339bf82f0ca 3 Zworgle the foobar
1903 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1903 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1904
1904
1905 To swap revision 2 and 4, its lines are swapped
1905 To swap revision 2 and 4, its lines are swapped
1906 in the editor::
1906 in the editor::
1907
1907
1908 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1908 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1909 pick 5339bf82f0ca 3 Zworgle the foobar
1909 pick 5339bf82f0ca 3 Zworgle the foobar
1910 pick 252a1af424ad 2 Blorb a morgwazzle
1910 pick 252a1af424ad 2 Blorb a morgwazzle
1911
1911
1912 Returns 0 on success, 1 if user intervention is required (not only
1912 Returns 0 on success, 1 if user intervention is required (not only
1913 for intentional "edit" command, but also for resolving unexpected
1913 for intentional "edit" command, but also for resolving unexpected
1914 conflicts).
1914 conflicts).
1915 """
1915 """
1916 opts = pycompat.byteskwargs(opts)
1916 opts = pycompat.byteskwargs(opts)
1917
1917
1918 # kludge: _chistedit only works for starting an edit, not aborting
1918 # kludge: _chistedit only works for starting an edit, not aborting
1919 # or continuing, so fall back to regular _texthistedit for those
1919 # or continuing, so fall back to regular _texthistedit for those
1920 # operations.
1920 # operations.
1921 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1921 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1922 return _chistedit(ui, repo, freeargs, opts)
1922 return _chistedit(ui, repo, freeargs, opts)
1923 return _texthistedit(ui, repo, freeargs, opts)
1923 return _texthistedit(ui, repo, freeargs, opts)
1924
1924
1925
1925
1926 def _texthistedit(ui, repo, freeargs, opts):
1926 def _texthistedit(ui, repo, freeargs, opts):
1927 state = histeditstate(repo)
1927 state = histeditstate(repo)
1928 with repo.wlock() as wlock, repo.lock() as lock:
1928 with repo.wlock() as wlock, repo.lock() as lock:
1929 state.wlock = wlock
1929 state.wlock = wlock
1930 state.lock = lock
1930 state.lock = lock
1931 _histedit(ui, repo, state, freeargs, opts)
1931 _histedit(ui, repo, state, freeargs, opts)
1932
1932
1933
1933
1934 goalcontinue = b'continue'
1934 goalcontinue = b'continue'
1935 goalabort = b'abort'
1935 goalabort = b'abort'
1936 goaleditplan = b'edit-plan'
1936 goaleditplan = b'edit-plan'
1937 goalnew = b'new'
1937 goalnew = b'new'
1938
1938
1939
1939
1940 def _getgoal(opts):
1940 def _getgoal(opts):
1941 if opts.get(b'continue'):
1941 if opts.get(b'continue'):
1942 return goalcontinue
1942 return goalcontinue
1943 if opts.get(b'abort'):
1943 if opts.get(b'abort'):
1944 return goalabort
1944 return goalabort
1945 if opts.get(b'edit_plan'):
1945 if opts.get(b'edit_plan'):
1946 return goaleditplan
1946 return goaleditplan
1947 return goalnew
1947 return goalnew
1948
1948
1949
1949
1950 def _readfile(ui, path):
1950 def _readfile(ui, path):
1951 if path == b'-':
1951 if path == b'-':
1952 with ui.timeblockedsection(b'histedit'):
1952 with ui.timeblockedsection(b'histedit'):
1953 return ui.fin.read()
1953 return ui.fin.read()
1954 else:
1954 else:
1955 with open(path, b'rb') as f:
1955 with open(path, b'rb') as f:
1956 return f.read()
1956 return f.read()
1957
1957
1958
1958
1959 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1959 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1960 # TODO only abort if we try to histedit mq patches, not just
1960 # TODO only abort if we try to histedit mq patches, not just
1961 # blanket if mq patches are applied somewhere
1961 # blanket if mq patches are applied somewhere
1962 mq = getattr(repo, 'mq', None)
1962 mq = getattr(repo, 'mq', None)
1963 if mq and mq.applied:
1963 if mq and mq.applied:
1964 raise error.StateError(_(b'source has mq patches applied'))
1964 raise error.StateError(_(b'source has mq patches applied'))
1965
1965
1966 # basic argument incompatibility processing
1966 # basic argument incompatibility processing
1967 outg = opts.get(b'outgoing')
1967 outg = opts.get(b'outgoing')
1968 editplan = opts.get(b'edit_plan')
1968 editplan = opts.get(b'edit_plan')
1969 abort = opts.get(b'abort')
1969 abort = opts.get(b'abort')
1970 force = opts.get(b'force')
1970 force = opts.get(b'force')
1971 if force and not outg:
1971 if force and not outg:
1972 raise error.InputError(_(b'--force only allowed with --outgoing'))
1972 raise error.InputError(_(b'--force only allowed with --outgoing'))
1973 if goal == b'continue':
1973 if goal == b'continue':
1974 if any((outg, abort, revs, freeargs, rules, editplan)):
1974 if any((outg, abort, revs, freeargs, rules, editplan)):
1975 raise error.InputError(_(b'no arguments allowed with --continue'))
1975 raise error.InputError(_(b'no arguments allowed with --continue'))
1976 elif goal == b'abort':
1976 elif goal == b'abort':
1977 if any((outg, revs, freeargs, rules, editplan)):
1977 if any((outg, revs, freeargs, rules, editplan)):
1978 raise error.InputError(_(b'no arguments allowed with --abort'))
1978 raise error.InputError(_(b'no arguments allowed with --abort'))
1979 elif goal == b'edit-plan':
1979 elif goal == b'edit-plan':
1980 if any((outg, revs, freeargs)):
1980 if any((outg, revs, freeargs)):
1981 raise error.InputError(
1981 raise error.InputError(
1982 _(b'only --commands argument allowed with --edit-plan')
1982 _(b'only --commands argument allowed with --edit-plan')
1983 )
1983 )
1984 else:
1984 else:
1985 if outg:
1985 if outg:
1986 if revs:
1986 if revs:
1987 raise error.InputError(
1987 raise error.InputError(
1988 _(b'no revisions allowed with --outgoing')
1988 _(b'no revisions allowed with --outgoing')
1989 )
1989 )
1990 if len(freeargs) > 1:
1990 if len(freeargs) > 1:
1991 raise error.InputError(
1991 raise error.InputError(
1992 _(b'only one repo argument allowed with --outgoing')
1992 _(b'only one repo argument allowed with --outgoing')
1993 )
1993 )
1994 else:
1994 else:
1995 revs.extend(freeargs)
1995 revs.extend(freeargs)
1996 if len(revs) == 0:
1996 if len(revs) == 0:
1997 defaultrev = destutil.desthistedit(ui, repo)
1997 defaultrev = destutil.desthistedit(ui, repo)
1998 if defaultrev is not None:
1998 if defaultrev is not None:
1999 revs.append(defaultrev)
1999 revs.append(defaultrev)
2000
2000
2001 if len(revs) != 1:
2001 if len(revs) != 1:
2002 raise error.InputError(
2002 raise error.InputError(
2003 _(b'histedit requires exactly one ancestor revision')
2003 _(b'histedit requires exactly one ancestor revision')
2004 )
2004 )
2005
2005
2006
2006
2007 def _histedit(ui, repo, state, freeargs, opts):
2007 def _histedit(ui, repo, state, freeargs, opts):
2008 fm = ui.formatter(b'histedit', opts)
2008 fm = ui.formatter(b'histedit', opts)
2009 fm.startitem()
2009 fm.startitem()
2010 goal = _getgoal(opts)
2010 goal = _getgoal(opts)
2011 revs = opts.get(b'rev', [])
2011 revs = opts.get(b'rev', [])
2012 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2012 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2013 rules = opts.get(b'commands', b'')
2013 rules = opts.get(b'commands', b'')
2014 state.keep = opts.get(b'keep', False)
2014 state.keep = opts.get(b'keep', False)
2015
2015
2016 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
2016 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
2017
2017
2018 hastags = False
2018 hastags = False
2019 if revs:
2019 if revs:
2020 revs = logcmdutil.revrange(repo, revs)
2020 revs = logcmdutil.revrange(repo, revs)
2021 ctxs = [repo[rev] for rev in revs]
2021 ctxs = [repo[rev] for rev in revs]
2022 for ctx in ctxs:
2022 for ctx in ctxs:
2023 tags = [tag for tag in ctx.tags() if tag != b'tip']
2023 tags = [tag for tag in ctx.tags() if tag != b'tip']
2024 if not hastags:
2024 if not hastags:
2025 hastags = len(tags)
2025 hastags = len(tags)
2026 if hastags:
2026 if hastags:
2027 if ui.promptchoice(
2027 if ui.promptchoice(
2028 _(
2028 _(
2029 b'warning: tags associated with the given'
2029 b'warning: tags associated with the given'
2030 b' changeset will be lost after histedit.\n'
2030 b' changeset will be lost after histedit.\n'
2031 b'do you want to continue (yN)? $$ &Yes $$ &No'
2031 b'do you want to continue (yN)? $$ &Yes $$ &No'
2032 ),
2032 ),
2033 default=1,
2033 default=1,
2034 ):
2034 ):
2035 raise error.CanceledError(_(b'histedit cancelled\n'))
2035 raise error.CanceledError(_(b'histedit cancelled\n'))
2036 # rebuild state
2036 # rebuild state
2037 if goal == goalcontinue:
2037 if goal == goalcontinue:
2038 state.read()
2038 state.read()
2039 state = bootstrapcontinue(ui, state, opts)
2039 state = bootstrapcontinue(ui, state, opts)
2040 elif goal == goaleditplan:
2040 elif goal == goaleditplan:
2041 _edithisteditplan(ui, repo, state, rules)
2041 _edithisteditplan(ui, repo, state, rules)
2042 return
2042 return
2043 elif goal == goalabort:
2043 elif goal == goalabort:
2044 _aborthistedit(ui, repo, state, nobackup=nobackup)
2044 _aborthistedit(ui, repo, state, nobackup=nobackup)
2045 return
2045 return
2046 else:
2046 else:
2047 # goal == goalnew
2047 # goal == goalnew
2048 _newhistedit(ui, repo, state, revs, freeargs, opts)
2048 _newhistedit(ui, repo, state, revs, freeargs, opts)
2049
2049
2050 _continuehistedit(ui, repo, state)
2050 _continuehistedit(ui, repo, state)
2051 _finishhistedit(ui, repo, state, fm)
2051 _finishhistedit(ui, repo, state, fm)
2052 fm.end()
2052 fm.end()
2053
2053
2054
2054
2055 def _continuehistedit(ui, repo, state):
2055 def _continuehistedit(ui, repo, state):
2056 """This function runs after either:
2056 """This function runs after either:
2057 - bootstrapcontinue (if the goal is 'continue')
2057 - bootstrapcontinue (if the goal is 'continue')
2058 - _newhistedit (if the goal is 'new')
2058 - _newhistedit (if the goal is 'new')
2059 """
2059 """
2060 # preprocess rules so that we can hide inner folds from the user
2060 # preprocess rules so that we can hide inner folds from the user
2061 # and only show one editor
2061 # and only show one editor
2062 actions = state.actions[:]
2062 actions = state.actions[:]
2063 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2063 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2064 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2064 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2065 state.actions[idx].__class__ = _multifold
2065 state.actions[idx].__class__ = _multifold
2066
2066
2067 # Force an initial state file write, so the user can run --abort/continue
2067 # Force an initial state file write, so the user can run --abort/continue
2068 # even if there's an exception before the first transaction serialize.
2068 # even if there's an exception before the first transaction serialize.
2069 state.write()
2069 state.write()
2070
2070
2071 tr = None
2071 tr = None
2072 # Don't use singletransaction by default since it rolls the entire
2072 # Don't use singletransaction by default since it rolls the entire
2073 # transaction back if an unexpected exception happens (like a
2073 # transaction back if an unexpected exception happens (like a
2074 # pretxncommit hook throws, or the user aborts the commit msg editor).
2074 # pretxncommit hook throws, or the user aborts the commit msg editor).
2075 if ui.configbool(b"histedit", b"singletransaction"):
2075 if ui.configbool(b"histedit", b"singletransaction"):
2076 # Don't use a 'with' for the transaction, since actions may close
2076 # Don't use a 'with' for the transaction, since actions may close
2077 # and reopen a transaction. For example, if the action executes an
2077 # and reopen a transaction. For example, if the action executes an
2078 # external process it may choose to commit the transaction first.
2078 # external process it may choose to commit the transaction first.
2079 tr = repo.transaction(b'histedit')
2079 tr = repo.transaction(b'histedit')
2080 progress = ui.makeprogress(
2080 progress = ui.makeprogress(
2081 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2081 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2082 )
2082 )
2083 with progress, util.acceptintervention(tr):
2083 with progress, util.acceptintervention(tr):
2084 while state.actions:
2084 while state.actions:
2085 state.write(tr=tr)
2085 state.write(tr=tr)
2086 actobj = state.actions[0]
2086 actobj = state.actions[0]
2087 progress.increment(item=actobj.torule())
2087 progress.increment(item=actobj.torule())
2088 ui.debug(
2088 ui.debug(
2089 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2089 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2090 )
2090 )
2091 parentctx, replacement_ = actobj.run()
2091 parentctx, replacement_ = actobj.run()
2092 state.parentctxnode = parentctx.node()
2092 state.parentctxnode = parentctx.node()
2093 state.replacements.extend(replacement_)
2093 state.replacements.extend(replacement_)
2094 state.actions.pop(0)
2094 state.actions.pop(0)
2095
2095
2096 state.write()
2096 state.write()
2097
2097
2098
2098
2099 def _finishhistedit(ui, repo, state, fm):
2099 def _finishhistedit(ui, repo, state, fm):
2100 """This action runs when histedit is finishing its session"""
2100 """This action runs when histedit is finishing its session"""
2101 mergemod.update(repo[state.parentctxnode])
2101 mergemod.update(repo[state.parentctxnode])
2102
2102
2103 mapping, tmpnodes, created, ntm = processreplacement(state)
2103 mapping, tmpnodes, created, ntm = processreplacement(state)
2104 if mapping:
2104 if mapping:
2105 for prec, succs in pycompat.iteritems(mapping):
2105 for prec, succs in pycompat.iteritems(mapping):
2106 if not succs:
2106 if not succs:
2107 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2107 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2108 else:
2108 else:
2109 ui.debug(
2109 ui.debug(
2110 b'histedit: %s is replaced by %s\n'
2110 b'histedit: %s is replaced by %s\n'
2111 % (short(prec), short(succs[0]))
2111 % (short(prec), short(succs[0]))
2112 )
2112 )
2113 if len(succs) > 1:
2113 if len(succs) > 1:
2114 m = b'histedit: %s'
2114 m = b'histedit: %s'
2115 for n in succs[1:]:
2115 for n in succs[1:]:
2116 ui.debug(m % short(n))
2116 ui.debug(m % short(n))
2117
2117
2118 if not state.keep:
2118 if not state.keep:
2119 if mapping:
2119 if mapping:
2120 movetopmostbookmarks(repo, state.topmost, ntm)
2120 movetopmostbookmarks(repo, state.topmost, ntm)
2121 # TODO update mq state
2121 # TODO update mq state
2122 else:
2122 else:
2123 mapping = {}
2123 mapping = {}
2124
2124
2125 for n in tmpnodes:
2125 for n in tmpnodes:
2126 if n in repo:
2126 if n in repo:
2127 mapping[n] = ()
2127 mapping[n] = ()
2128
2128
2129 # remove entries about unknown nodes
2129 # remove entries about unknown nodes
2130 has_node = repo.unfiltered().changelog.index.has_node
2130 has_node = repo.unfiltered().changelog.index.has_node
2131 mapping = {
2131 mapping = {
2132 k: v
2132 k: v
2133 for k, v in mapping.items()
2133 for k, v in mapping.items()
2134 if has_node(k) and all(has_node(n) for n in v)
2134 if has_node(k) and all(has_node(n) for n in v)
2135 }
2135 }
2136 scmutil.cleanupnodes(repo, mapping, b'histedit')
2136 scmutil.cleanupnodes(repo, mapping, b'histedit')
2137 hf = fm.hexfunc
2137 hf = fm.hexfunc
2138 fl = fm.formatlist
2138 fl = fm.formatlist
2139 fd = fm.formatdict
2139 fd = fm.formatdict
2140 nodechanges = fd(
2140 nodechanges = fd(
2141 {
2141 {
2142 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2142 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2143 for oldn, newn in pycompat.iteritems(mapping)
2143 for oldn, newn in pycompat.iteritems(mapping)
2144 },
2144 },
2145 key=b"oldnode",
2145 key=b"oldnode",
2146 value=b"newnodes",
2146 value=b"newnodes",
2147 )
2147 )
2148 fm.data(nodechanges=nodechanges)
2148 fm.data(nodechanges=nodechanges)
2149
2149
2150 state.clear()
2150 state.clear()
2151 if os.path.exists(repo.sjoin(b'undo')):
2151 if os.path.exists(repo.sjoin(b'undo')):
2152 os.unlink(repo.sjoin(b'undo'))
2152 os.unlink(repo.sjoin(b'undo'))
2153 if repo.vfs.exists(b'histedit-last-edit.txt'):
2153 if repo.vfs.exists(b'histedit-last-edit.txt'):
2154 repo.vfs.unlink(b'histedit-last-edit.txt')
2154 repo.vfs.unlink(b'histedit-last-edit.txt')
2155
2155
2156
2156
2157 def _aborthistedit(ui, repo, state, nobackup=False):
2157 def _aborthistedit(ui, repo, state, nobackup=False):
2158 try:
2158 try:
2159 state.read()
2159 state.read()
2160 __, leafs, tmpnodes, __ = processreplacement(state)
2160 __, leafs, tmpnodes, __ = processreplacement(state)
2161 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2161 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2162
2162
2163 # Recover our old commits if necessary
2163 # Recover our old commits if necessary
2164 if not state.topmost in repo and state.backupfile:
2164 if not state.topmost in repo and state.backupfile:
2165 backupfile = repo.vfs.join(state.backupfile)
2165 backupfile = repo.vfs.join(state.backupfile)
2166 f = hg.openpath(ui, backupfile)
2166 f = hg.openpath(ui, backupfile)
2167 gen = exchange.readbundle(ui, f, backupfile)
2167 gen = exchange.readbundle(ui, f, backupfile)
2168 with repo.transaction(b'histedit.abort') as tr:
2168 with repo.transaction(b'histedit.abort') as tr:
2169 bundle2.applybundle(
2169 bundle2.applybundle(
2170 repo,
2170 repo,
2171 gen,
2171 gen,
2172 tr,
2172 tr,
2173 source=b'histedit',
2173 source=b'histedit',
2174 url=b'bundle:' + backupfile,
2174 url=b'bundle:' + backupfile,
2175 )
2175 )
2176
2176
2177 os.remove(backupfile)
2177 os.remove(backupfile)
2178
2178
2179 # check whether we should update away
2179 # check whether we should update away
2180 if repo.unfiltered().revs(
2180 if repo.unfiltered().revs(
2181 b'parents() and (%n or %ln::)',
2181 b'parents() and (%n or %ln::)',
2182 state.parentctxnode,
2182 state.parentctxnode,
2183 leafs | tmpnodes,
2183 leafs | tmpnodes,
2184 ):
2184 ):
2185 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2185 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2186 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2186 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2187 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2187 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2188 except Exception:
2188 except Exception:
2189 if state.inprogress():
2189 if state.inprogress():
2190 ui.warn(
2190 ui.warn(
2191 _(
2191 _(
2192 b'warning: encountered an exception during histedit '
2192 b'warning: encountered an exception during histedit '
2193 b'--abort; the repository may not have been completely '
2193 b'--abort; the repository may not have been completely '
2194 b'cleaned up\n'
2194 b'cleaned up\n'
2195 )
2195 )
2196 )
2196 )
2197 raise
2197 raise
2198 finally:
2198 finally:
2199 state.clear()
2199 state.clear()
2200
2200
2201
2201
2202 def hgaborthistedit(ui, repo):
2202 def hgaborthistedit(ui, repo):
2203 state = histeditstate(repo)
2203 state = histeditstate(repo)
2204 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2204 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2205 with repo.wlock() as wlock, repo.lock() as lock:
2205 with repo.wlock() as wlock, repo.lock() as lock:
2206 state.wlock = wlock
2206 state.wlock = wlock
2207 state.lock = lock
2207 state.lock = lock
2208 _aborthistedit(ui, repo, state, nobackup=nobackup)
2208 _aborthistedit(ui, repo, state, nobackup=nobackup)
2209
2209
2210
2210
2211 def _edithisteditplan(ui, repo, state, rules):
2211 def _edithisteditplan(ui, repo, state, rules):
2212 state.read()
2212 state.read()
2213 if not rules:
2213 if not rules:
2214 comment = geteditcomment(
2214 comment = geteditcomment(
2215 ui, short(state.parentctxnode), short(state.topmost)
2215 ui, short(state.parentctxnode), short(state.topmost)
2216 )
2216 )
2217 rules = ruleeditor(repo, ui, state.actions, comment)
2217 rules = ruleeditor(repo, ui, state.actions, comment)
2218 else:
2218 else:
2219 rules = _readfile(ui, rules)
2219 rules = _readfile(ui, rules)
2220 actions = parserules(rules, state)
2220 actions = parserules(rules, state)
2221 ctxs = [repo[act.node] for act in state.actions if act.node]
2221 ctxs = [repo[act.node] for act in state.actions if act.node]
2222 warnverifyactions(ui, repo, actions, state, ctxs)
2222 warnverifyactions(ui, repo, actions, state, ctxs)
2223 state.actions = actions
2223 state.actions = actions
2224 state.write()
2224 state.write()
2225
2225
2226
2226
2227 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2227 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2228 outg = opts.get(b'outgoing')
2228 outg = opts.get(b'outgoing')
2229 rules = opts.get(b'commands', b'')
2229 rules = opts.get(b'commands', b'')
2230 force = opts.get(b'force')
2230 force = opts.get(b'force')
2231
2231
2232 cmdutil.checkunfinished(repo)
2232 cmdutil.checkunfinished(repo)
2233 cmdutil.bailifchanged(repo)
2233 cmdutil.bailifchanged(repo)
2234
2234
2235 topmost = repo.dirstate.p1()
2235 topmost = repo.dirstate.p1()
2236 if outg:
2236 if outg:
2237 if freeargs:
2237 if freeargs:
2238 remote = freeargs[0]
2238 remote = freeargs[0]
2239 else:
2239 else:
2240 remote = None
2240 remote = None
2241 root = findoutgoing(ui, repo, remote, force, opts)
2241 root = findoutgoing(ui, repo, remote, force, opts)
2242 else:
2242 else:
2243 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
2243 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
2244 if len(rr) != 1:
2244 if len(rr) != 1:
2245 raise error.InputError(
2245 raise error.InputError(
2246 _(
2246 _(
2247 b'The specified revisions must have '
2247 b'The specified revisions must have '
2248 b'exactly one common root'
2248 b'exactly one common root'
2249 )
2249 )
2250 )
2250 )
2251 root = rr[0].node()
2251 root = rr[0].node()
2252
2252
2253 revs = between(repo, root, topmost, state.keep)
2253 revs = between(repo, root, topmost, state.keep)
2254 if not revs:
2254 if not revs:
2255 raise error.InputError(
2255 raise error.InputError(
2256 _(b'%s is not an ancestor of working directory') % short(root)
2256 _(b'%s is not an ancestor of working directory') % short(root)
2257 )
2257 )
2258
2258
2259 ctxs = [repo[r] for r in revs]
2259 ctxs = [repo[r] for r in revs]
2260
2260
2261 wctx = repo[None]
2261 wctx = repo[None]
2262 # Please don't ask me why `ancestors` is this value. I figured it
2262 # Please don't ask me why `ancestors` is this value. I figured it
2263 # out with print-debugging, not by actually understanding what the
2263 # out with print-debugging, not by actually understanding what the
2264 # merge code is doing. :(
2264 # merge code is doing. :(
2265 ancs = [repo[b'.']]
2265 ancs = [repo[b'.']]
2266 # Sniff-test to make sure we won't collide with untracked files in
2266 # Sniff-test to make sure we won't collide with untracked files in
2267 # the working directory. If we don't do this, we can get a
2267 # the working directory. If we don't do this, we can get a
2268 # collision after we've started histedit and backing out gets ugly
2268 # collision after we've started histedit and backing out gets ugly
2269 # for everyone, especially the user.
2269 # for everyone, especially the user.
2270 for c in [ctxs[0].p1()] + ctxs:
2270 for c in [ctxs[0].p1()] + ctxs:
2271 try:
2271 try:
2272 mergemod.calculateupdates(
2272 mergemod.calculateupdates(
2273 repo,
2273 repo,
2274 wctx,
2274 wctx,
2275 c,
2275 c,
2276 ancs,
2276 ancs,
2277 # These parameters were determined by print-debugging
2277 # These parameters were determined by print-debugging
2278 # what happens later on inside histedit.
2278 # what happens later on inside histedit.
2279 branchmerge=False,
2279 branchmerge=False,
2280 force=False,
2280 force=False,
2281 acceptremote=False,
2281 acceptremote=False,
2282 followcopies=False,
2282 followcopies=False,
2283 )
2283 )
2284 except error.Abort:
2284 except error.Abort:
2285 raise error.StateError(
2285 raise error.StateError(
2286 _(
2286 _(
2287 b"untracked files in working directory conflict with files in %s"
2287 b"untracked files in working directory conflict with files in %s"
2288 )
2288 )
2289 % c
2289 % c
2290 )
2290 )
2291
2291
2292 if not rules:
2292 if not rules:
2293 comment = geteditcomment(ui, short(root), short(topmost))
2293 comment = geteditcomment(ui, short(root), short(topmost))
2294 actions = [pick(state, r) for r in revs]
2294 actions = [pick(state, r) for r in revs]
2295 rules = ruleeditor(repo, ui, actions, comment)
2295 rules = ruleeditor(repo, ui, actions, comment)
2296 else:
2296 else:
2297 rules = _readfile(ui, rules)
2297 rules = _readfile(ui, rules)
2298 actions = parserules(rules, state)
2298 actions = parserules(rules, state)
2299 warnverifyactions(ui, repo, actions, state, ctxs)
2299 warnverifyactions(ui, repo, actions, state, ctxs)
2300
2300
2301 parentctxnode = repo[root].p1().node()
2301 parentctxnode = repo[root].p1().node()
2302
2302
2303 state.parentctxnode = parentctxnode
2303 state.parentctxnode = parentctxnode
2304 state.actions = actions
2304 state.actions = actions
2305 state.topmost = topmost
2305 state.topmost = topmost
2306 state.replacements = []
2306 state.replacements = []
2307
2307
2308 ui.log(
2308 ui.log(
2309 b"histedit",
2309 b"histedit",
2310 b"%d actions to histedit\n",
2310 b"%d actions to histedit\n",
2311 len(actions),
2311 len(actions),
2312 histedit_num_actions=len(actions),
2312 histedit_num_actions=len(actions),
2313 )
2313 )
2314
2314
2315 # Create a backup so we can always abort completely.
2315 # Create a backup so we can always abort completely.
2316 backupfile = None
2316 backupfile = None
2317 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2317 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2318 backupfile = repair.backupbundle(
2318 backupfile = repair.backupbundle(
2319 repo, [parentctxnode], [topmost], root, b'histedit'
2319 repo, [parentctxnode], [topmost], root, b'histedit'
2320 )
2320 )
2321 state.backupfile = backupfile
2321 state.backupfile = backupfile
2322
2322
2323
2323
2324 def _getsummary(ctx):
2324 def _getsummary(ctx):
2325 # a common pattern is to extract the summary but default to the empty
2325 # a common pattern is to extract the summary but default to the empty
2326 # string
2326 # string
2327 summary = ctx.description() or b''
2327 summary = ctx.description() or b''
2328 if summary:
2328 if summary:
2329 summary = summary.splitlines()[0]
2329 summary = summary.splitlines()[0]
2330 return summary
2330 return summary
2331
2331
2332
2332
2333 def bootstrapcontinue(ui, state, opts):
2333 def bootstrapcontinue(ui, state, opts):
2334 repo = state.repo
2334 repo = state.repo
2335
2335
2336 ms = mergestatemod.mergestate.read(repo)
2336 ms = mergestatemod.mergestate.read(repo)
2337 mergeutil.checkunresolved(ms)
2337 mergeutil.checkunresolved(ms)
2338
2338
2339 if state.actions:
2339 if state.actions:
2340 actobj = state.actions.pop(0)
2340 actobj = state.actions.pop(0)
2341
2341
2342 if _isdirtywc(repo):
2342 if _isdirtywc(repo):
2343 actobj.continuedirty()
2343 actobj.continuedirty()
2344 if _isdirtywc(repo):
2344 if _isdirtywc(repo):
2345 abortdirty()
2345 abortdirty()
2346
2346
2347 parentctx, replacements = actobj.continueclean()
2347 parentctx, replacements = actobj.continueclean()
2348
2348
2349 state.parentctxnode = parentctx.node()
2349 state.parentctxnode = parentctx.node()
2350 state.replacements.extend(replacements)
2350 state.replacements.extend(replacements)
2351
2351
2352 return state
2352 return state
2353
2353
2354
2354
2355 def between(repo, old, new, keep):
2355 def between(repo, old, new, keep):
2356 """select and validate the set of revision to edit
2356 """select and validate the set of revision to edit
2357
2357
2358 When keep is false, the specified set can't have children."""
2358 When keep is false, the specified set can't have children."""
2359 revs = repo.revs(b'%n::%n', old, new)
2359 revs = repo.revs(b'%n::%n', old, new)
2360 if revs and not keep:
2360 if revs and not keep:
2361 rewriteutil.precheck(repo, revs, b'edit')
2361 rewriteutil.precheck(repo, revs, b'edit')
2362 if repo.revs(b'(%ld) and merge()', revs):
2362 if repo.revs(b'(%ld) and merge()', revs):
2363 raise error.StateError(
2363 raise error.StateError(
2364 _(b'cannot edit history that contains merges')
2364 _(b'cannot edit history that contains merges')
2365 )
2365 )
2366 return pycompat.maplist(repo.changelog.node, revs)
2366 return pycompat.maplist(repo.changelog.node, revs)
2367
2367
2368
2368
2369 def ruleeditor(repo, ui, actions, editcomment=b""):
2369 def ruleeditor(repo, ui, actions, editcomment=b""):
2370 """open an editor to edit rules
2370 """open an editor to edit rules
2371
2371
2372 rules are in the format [ [act, ctx], ...] like in state.rules
2372 rules are in the format [ [act, ctx], ...] like in state.rules
2373 """
2373 """
2374 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2374 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2375 newact = util.sortdict()
2375 newact = util.sortdict()
2376 for act in actions:
2376 for act in actions:
2377 ctx = repo[act.node]
2377 ctx = repo[act.node]
2378 summary = _getsummary(ctx)
2378 summary = _getsummary(ctx)
2379 fword = summary.split(b' ', 1)[0].lower()
2379 fword = summary.split(b' ', 1)[0].lower()
2380 added = False
2380 added = False
2381
2381
2382 # if it doesn't end with the special character '!' just skip this
2382 # if it doesn't end with the special character '!' just skip this
2383 if fword.endswith(b'!'):
2383 if fword.endswith(b'!'):
2384 fword = fword[:-1]
2384 fword = fword[:-1]
2385 if fword in primaryactions | secondaryactions | tertiaryactions:
2385 if fword in primaryactions | secondaryactions | tertiaryactions:
2386 act.verb = fword
2386 act.verb = fword
2387 # get the target summary
2387 # get the target summary
2388 tsum = summary[len(fword) + 1 :].lstrip()
2388 tsum = summary[len(fword) + 1 :].lstrip()
2389 # safe but slow: reverse iterate over the actions so we
2389 # safe but slow: reverse iterate over the actions so we
2390 # don't clash on two commits having the same summary
2390 # don't clash on two commits having the same summary
2391 for na, l in reversed(list(pycompat.iteritems(newact))):
2391 for na, l in reversed(list(pycompat.iteritems(newact))):
2392 actx = repo[na.node]
2392 actx = repo[na.node]
2393 asum = _getsummary(actx)
2393 asum = _getsummary(actx)
2394 if asum == tsum:
2394 if asum == tsum:
2395 added = True
2395 added = True
2396 l.append(act)
2396 l.append(act)
2397 break
2397 break
2398
2398
2399 if not added:
2399 if not added:
2400 newact[act] = []
2400 newact[act] = []
2401
2401
2402 # copy over and flatten the new list
2402 # copy over and flatten the new list
2403 actions = []
2403 actions = []
2404 for na, l in pycompat.iteritems(newact):
2404 for na, l in pycompat.iteritems(newact):
2405 actions.append(na)
2405 actions.append(na)
2406 actions += l
2406 actions += l
2407
2407
2408 rules = b'\n'.join([act.torule() for act in actions])
2408 rules = b'\n'.join([act.torule() for act in actions])
2409 rules += b'\n\n'
2409 rules += b'\n\n'
2410 rules += editcomment
2410 rules += editcomment
2411 rules = ui.edit(
2411 rules = ui.edit(
2412 rules,
2412 rules,
2413 ui.username(),
2413 ui.username(),
2414 {b'prefix': b'histedit'},
2414 {b'prefix': b'histedit'},
2415 repopath=repo.path,
2415 repopath=repo.path,
2416 action=b'histedit',
2416 action=b'histedit',
2417 )
2417 )
2418
2418
2419 # Save edit rules in .hg/histedit-last-edit.txt in case
2419 # Save edit rules in .hg/histedit-last-edit.txt in case
2420 # the user needs to ask for help after something
2420 # the user needs to ask for help after something
2421 # surprising happens.
2421 # surprising happens.
2422 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2422 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2423 f.write(rules)
2423 f.write(rules)
2424
2424
2425 return rules
2425 return rules
2426
2426
2427
2427
2428 def parserules(rules, state):
2428 def parserules(rules, state):
2429 """Read the histedit rules string and return list of action objects"""
2429 """Read the histedit rules string and return list of action objects"""
2430 rules = [
2430 rules = [
2431 l
2431 l
2432 for l in (r.strip() for r in rules.splitlines())
2432 for l in (r.strip() for r in rules.splitlines())
2433 if l and not l.startswith(b'#')
2433 if l and not l.startswith(b'#')
2434 ]
2434 ]
2435 actions = []
2435 actions = []
2436 for r in rules:
2436 for r in rules:
2437 if b' ' not in r:
2437 if b' ' not in r:
2438 raise error.ParseError(_(b'malformed line "%s"') % r)
2438 raise error.ParseError(_(b'malformed line "%s"') % r)
2439 verb, rest = r.split(b' ', 1)
2439 verb, rest = r.split(b' ', 1)
2440
2440
2441 if verb not in actiontable:
2441 if verb not in actiontable:
2442 raise error.ParseError(_(b'unknown action "%s"') % verb)
2442 raise error.ParseError(_(b'unknown action "%s"') % verb)
2443
2443
2444 action = actiontable[verb].fromrule(state, rest)
2444 action = actiontable[verb].fromrule(state, rest)
2445 actions.append(action)
2445 actions.append(action)
2446 return actions
2446 return actions
2447
2447
2448
2448
2449 def warnverifyactions(ui, repo, actions, state, ctxs):
2449 def warnverifyactions(ui, repo, actions, state, ctxs):
2450 try:
2450 try:
2451 verifyactions(actions, state, ctxs)
2451 verifyactions(actions, state, ctxs)
2452 except error.ParseError:
2452 except error.ParseError:
2453 if repo.vfs.exists(b'histedit-last-edit.txt'):
2453 if repo.vfs.exists(b'histedit-last-edit.txt'):
2454 ui.warn(
2454 ui.warn(
2455 _(
2455 _(
2456 b'warning: histedit rules saved '
2456 b'warning: histedit rules saved '
2457 b'to: .hg/histedit-last-edit.txt\n'
2457 b'to: .hg/histedit-last-edit.txt\n'
2458 )
2458 )
2459 )
2459 )
2460 raise
2460 raise
2461
2461
2462
2462
2463 def verifyactions(actions, state, ctxs):
2463 def verifyactions(actions, state, ctxs):
2464 """Verify that there exists exactly one action per given changeset and
2464 """Verify that there exists exactly one action per given changeset and
2465 other constraints.
2465 other constraints.
2466
2466
2467 Will abort if there are to many or too few rules, a malformed rule,
2467 Will abort if there are to many or too few rules, a malformed rule,
2468 or a rule on a changeset outside of the user-given range.
2468 or a rule on a changeset outside of the user-given range.
2469 """
2469 """
2470 expected = {c.node() for c in ctxs}
2470 expected = {c.node() for c in ctxs}
2471 seen = set()
2471 seen = set()
2472 prev = None
2472 prev = None
2473
2473
2474 if actions and actions[0].verb in [b'roll', b'fold']:
2474 if actions and actions[0].verb in [b'roll', b'fold']:
2475 raise error.ParseError(
2475 raise error.ParseError(
2476 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2476 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2477 )
2477 )
2478
2478
2479 for action in actions:
2479 for action in actions:
2480 action.verify(prev, expected, seen)
2480 action.verify(prev, expected, seen)
2481 prev = action
2481 prev = action
2482 if action.node is not None:
2482 if action.node is not None:
2483 seen.add(action.node)
2483 seen.add(action.node)
2484 missing = sorted(expected - seen) # sort to stabilize output
2484 missing = sorted(expected - seen) # sort to stabilize output
2485
2485
2486 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2486 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2487 if len(actions) == 0:
2487 if len(actions) == 0:
2488 raise error.ParseError(
2488 raise error.ParseError(
2489 _(b'no rules provided'),
2489 _(b'no rules provided'),
2490 hint=_(b'use strip extension to remove commits'),
2490 hint=_(b'use strip extension to remove commits'),
2491 )
2491 )
2492
2492
2493 drops = [drop(state, n) for n in missing]
2493 drops = [drop(state, n) for n in missing]
2494 # put the in the beginning so they execute immediately and
2494 # put the in the beginning so they execute immediately and
2495 # don't show in the edit-plan in the future
2495 # don't show in the edit-plan in the future
2496 actions[:0] = drops
2496 actions[:0] = drops
2497 elif missing:
2497 elif missing:
2498 raise error.ParseError(
2498 raise error.ParseError(
2499 _(b'missing rules for changeset %s') % short(missing[0]),
2499 _(b'missing rules for changeset %s') % short(missing[0]),
2500 hint=_(
2500 hint=_(
2501 b'use "drop %s" to discard, see also: '
2501 b'use "drop %s" to discard, see also: '
2502 b"'hg help -e histedit.config'"
2502 b"'hg help -e histedit.config'"
2503 )
2503 )
2504 % short(missing[0]),
2504 % short(missing[0]),
2505 )
2505 )
2506
2506
2507
2507
2508 def adjustreplacementsfrommarkers(repo, oldreplacements):
2508 def adjustreplacementsfrommarkers(repo, oldreplacements):
2509 """Adjust replacements from obsolescence markers
2509 """Adjust replacements from obsolescence markers
2510
2510
2511 Replacements structure is originally generated based on
2511 Replacements structure is originally generated based on
2512 histedit's state and does not account for changes that are
2512 histedit's state and does not account for changes that are
2513 not recorded there. This function fixes that by adding
2513 not recorded there. This function fixes that by adding
2514 data read from obsolescence markers"""
2514 data read from obsolescence markers"""
2515 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2515 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2516 return oldreplacements
2516 return oldreplacements
2517
2517
2518 unfi = repo.unfiltered()
2518 unfi = repo.unfiltered()
2519 get_rev = unfi.changelog.index.get_rev
2519 get_rev = unfi.changelog.index.get_rev
2520 obsstore = repo.obsstore
2520 obsstore = repo.obsstore
2521 newreplacements = list(oldreplacements)
2521 newreplacements = list(oldreplacements)
2522 oldsuccs = [r[1] for r in oldreplacements]
2522 oldsuccs = [r[1] for r in oldreplacements]
2523 # successors that have already been added to succstocheck once
2523 # successors that have already been added to succstocheck once
2524 seensuccs = set().union(
2524 seensuccs = set().union(
2525 *oldsuccs
2525 *oldsuccs
2526 ) # create a set from an iterable of tuples
2526 ) # create a set from an iterable of tuples
2527 succstocheck = list(seensuccs)
2527 succstocheck = list(seensuccs)
2528 while succstocheck:
2528 while succstocheck:
2529 n = succstocheck.pop()
2529 n = succstocheck.pop()
2530 missing = get_rev(n) is None
2530 missing = get_rev(n) is None
2531 markers = obsstore.successors.get(n, ())
2531 markers = obsstore.successors.get(n, ())
2532 if missing and not markers:
2532 if missing and not markers:
2533 # dead end, mark it as such
2533 # dead end, mark it as such
2534 newreplacements.append((n, ()))
2534 newreplacements.append((n, ()))
2535 for marker in markers:
2535 for marker in markers:
2536 nsuccs = marker[1]
2536 nsuccs = marker[1]
2537 newreplacements.append((n, nsuccs))
2537 newreplacements.append((n, nsuccs))
2538 for nsucc in nsuccs:
2538 for nsucc in nsuccs:
2539 if nsucc not in seensuccs:
2539 if nsucc not in seensuccs:
2540 seensuccs.add(nsucc)
2540 seensuccs.add(nsucc)
2541 succstocheck.append(nsucc)
2541 succstocheck.append(nsucc)
2542
2542
2543 return newreplacements
2543 return newreplacements
2544
2544
2545
2545
2546 def processreplacement(state):
2546 def processreplacement(state):
2547 """process the list of replacements to return
2547 """process the list of replacements to return
2548
2548
2549 1) the final mapping between original and created nodes
2549 1) the final mapping between original and created nodes
2550 2) the list of temporary node created by histedit
2550 2) the list of temporary node created by histedit
2551 3) the list of new commit created by histedit"""
2551 3) the list of new commit created by histedit"""
2552 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2552 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2553 allsuccs = set()
2553 allsuccs = set()
2554 replaced = set()
2554 replaced = set()
2555 fullmapping = {}
2555 fullmapping = {}
2556 # initialize basic set
2556 # initialize basic set
2557 # fullmapping records all operations recorded in replacement
2557 # fullmapping records all operations recorded in replacement
2558 for rep in replacements:
2558 for rep in replacements:
2559 allsuccs.update(rep[1])
2559 allsuccs.update(rep[1])
2560 replaced.add(rep[0])
2560 replaced.add(rep[0])
2561 fullmapping.setdefault(rep[0], set()).update(rep[1])
2561 fullmapping.setdefault(rep[0], set()).update(rep[1])
2562 new = allsuccs - replaced
2562 new = allsuccs - replaced
2563 tmpnodes = allsuccs & replaced
2563 tmpnodes = allsuccs & replaced
2564 # Reduce content fullmapping into direct relation between original nodes
2564 # Reduce content fullmapping into direct relation between original nodes
2565 # and final node created during history edition
2565 # and final node created during history edition
2566 # Dropped changeset are replaced by an empty list
2566 # Dropped changeset are replaced by an empty list
2567 toproceed = set(fullmapping)
2567 toproceed = set(fullmapping)
2568 final = {}
2568 final = {}
2569 while toproceed:
2569 while toproceed:
2570 for x in list(toproceed):
2570 for x in list(toproceed):
2571 succs = fullmapping[x]
2571 succs = fullmapping[x]
2572 for s in list(succs):
2572 for s in list(succs):
2573 if s in toproceed:
2573 if s in toproceed:
2574 # non final node with unknown closure
2574 # non final node with unknown closure
2575 # We can't process this now
2575 # We can't process this now
2576 break
2576 break
2577 elif s in final:
2577 elif s in final:
2578 # non final node, replace with closure
2578 # non final node, replace with closure
2579 succs.remove(s)
2579 succs.remove(s)
2580 succs.update(final[s])
2580 succs.update(final[s])
2581 else:
2581 else:
2582 final[x] = succs
2582 final[x] = succs
2583 toproceed.remove(x)
2583 toproceed.remove(x)
2584 # remove tmpnodes from final mapping
2584 # remove tmpnodes from final mapping
2585 for n in tmpnodes:
2585 for n in tmpnodes:
2586 del final[n]
2586 del final[n]
2587 # we expect all changes involved in final to exist in the repo
2587 # we expect all changes involved in final to exist in the repo
2588 # turn `final` into list (topologically sorted)
2588 # turn `final` into list (topologically sorted)
2589 get_rev = state.repo.changelog.index.get_rev
2589 get_rev = state.repo.changelog.index.get_rev
2590 for prec, succs in final.items():
2590 for prec, succs in final.items():
2591 final[prec] = sorted(succs, key=get_rev)
2591 final[prec] = sorted(succs, key=get_rev)
2592
2592
2593 # computed topmost element (necessary for bookmark)
2593 # computed topmost element (necessary for bookmark)
2594 if new:
2594 if new:
2595 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2595 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2596 elif not final:
2596 elif not final:
2597 # Nothing rewritten at all. we won't need `newtopmost`
2597 # Nothing rewritten at all. we won't need `newtopmost`
2598 # It is the same as `oldtopmost` and `processreplacement` know it
2598 # It is the same as `oldtopmost` and `processreplacement` know it
2599 newtopmost = None
2599 newtopmost = None
2600 else:
2600 else:
2601 # every body died. The newtopmost is the parent of the root.
2601 # every body died. The newtopmost is the parent of the root.
2602 r = state.repo.changelog.rev
2602 r = state.repo.changelog.rev
2603 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2603 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2604
2604
2605 return final, tmpnodes, new, newtopmost
2605 return final, tmpnodes, new, newtopmost
2606
2606
2607
2607
2608 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2608 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2609 """Move bookmark from oldtopmost to newly created topmost
2609 """Move bookmark from oldtopmost to newly created topmost
2610
2610
2611 This is arguably a feature and we may only want that for the active
2611 This is arguably a feature and we may only want that for the active
2612 bookmark. But the behavior is kept compatible with the old version for now.
2612 bookmark. But the behavior is kept compatible with the old version for now.
2613 """
2613 """
2614 if not oldtopmost or not newtopmost:
2614 if not oldtopmost or not newtopmost:
2615 return
2615 return
2616 oldbmarks = repo.nodebookmarks(oldtopmost)
2616 oldbmarks = repo.nodebookmarks(oldtopmost)
2617 if oldbmarks:
2617 if oldbmarks:
2618 with repo.lock(), repo.transaction(b'histedit') as tr:
2618 with repo.lock(), repo.transaction(b'histedit') as tr:
2619 marks = repo._bookmarks
2619 marks = repo._bookmarks
2620 changes = []
2620 changes = []
2621 for name in oldbmarks:
2621 for name in oldbmarks:
2622 changes.append((name, newtopmost))
2622 changes.append((name, newtopmost))
2623 marks.applychanges(repo, tr, changes)
2623 marks.applychanges(repo, tr, changes)
2624
2624
2625
2625
2626 def cleanupnode(ui, repo, nodes, nobackup=False):
2626 def cleanupnode(ui, repo, nodes, nobackup=False):
2627 """strip a group of nodes from the repository
2627 """strip a group of nodes from the repository
2628
2628
2629 The set of node to strip may contains unknown nodes."""
2629 The set of node to strip may contains unknown nodes."""
2630 with repo.lock():
2630 with repo.lock():
2631 # do not let filtering get in the way of the cleanse
2631 # do not let filtering get in the way of the cleanse
2632 # we should probably get rid of obsolescence marker created during the
2632 # we should probably get rid of obsolescence marker created during the
2633 # histedit, but we currently do not have such information.
2633 # histedit, but we currently do not have such information.
2634 repo = repo.unfiltered()
2634 repo = repo.unfiltered()
2635 # Find all nodes that need to be stripped
2635 # Find all nodes that need to be stripped
2636 # (we use %lr instead of %ln to silently ignore unknown items)
2636 # (we use %lr instead of %ln to silently ignore unknown items)
2637 has_node = repo.changelog.index.has_node
2637 has_node = repo.changelog.index.has_node
2638 nodes = sorted(n for n in nodes if has_node(n))
2638 nodes = sorted(n for n in nodes if has_node(n))
2639 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2639 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2640 if roots:
2640 if roots:
2641 backup = not nobackup
2641 backup = not nobackup
2642 repair.strip(ui, repo, roots, backup=backup)
2642 repair.strip(ui, repo, roots, backup=backup)
2643
2643
2644
2644
2645 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2645 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2646 if isinstance(nodelist, bytes):
2646 if isinstance(nodelist, bytes):
2647 nodelist = [nodelist]
2647 nodelist = [nodelist]
2648 state = histeditstate(repo)
2648 state = histeditstate(repo)
2649 if state.inprogress():
2649 if state.inprogress():
2650 state.read()
2650 state.read()
2651 histedit_nodes = {
2651 histedit_nodes = {
2652 action.node for action in state.actions if action.node
2652 action.node for action in state.actions if action.node
2653 }
2653 }
2654 common_nodes = histedit_nodes & set(nodelist)
2654 common_nodes = histedit_nodes & set(nodelist)
2655 if common_nodes:
2655 if common_nodes:
2656 raise error.Abort(
2656 raise error.Abort(
2657 _(b"histedit in progress, can't strip %s")
2657 _(b"histedit in progress, can't strip %s")
2658 % b', '.join(short(x) for x in common_nodes)
2658 % b', '.join(short(x) for x in common_nodes)
2659 )
2659 )
2660 return orig(ui, repo, nodelist, *args, **kwargs)
2660 return orig(ui, repo, nodelist, *args, **kwargs)
2661
2661
2662
2662
2663 extensions.wrapfunction(repair, b'strip', stripwrapper)
2663 extensions.wrapfunction(repair, b'strip', stripwrapper)
2664
2664
2665
2665
2666 def summaryhook(ui, repo):
2666 def summaryhook(ui, repo):
2667 state = histeditstate(repo)
2667 state = histeditstate(repo)
2668 if not state.inprogress():
2668 if not state.inprogress():
2669 return
2669 return
2670 state.read()
2670 state.read()
2671 if state.actions:
2671 if state.actions:
2672 # i18n: column positioning for "hg summary"
2672 # i18n: column positioning for "hg summary"
2673 ui.write(
2673 ui.write(
2674 _(b'hist: %s (histedit --continue)\n')
2674 _(b'hist: %s (histedit --continue)\n')
2675 % (
2675 % (
2676 ui.label(_(b'%d remaining'), b'histedit.remaining')
2676 ui.label(_(b'%d remaining'), b'histedit.remaining')
2677 % len(state.actions)
2677 % len(state.actions)
2678 )
2678 )
2679 )
2679 )
2680
2680
2681
2681
2682 def extsetup(ui):
2682 def extsetup(ui):
2683 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2683 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2684 statemod.addunfinished(
2684 statemod.addunfinished(
2685 b'histedit',
2685 b'histedit',
2686 fname=b'histedit-state',
2686 fname=b'histedit-state',
2687 allowcommit=True,
2687 allowcommit=True,
2688 continueflag=True,
2688 continueflag=True,
2689 abortfunc=hgaborthistedit,
2689 abortfunc=hgaborthistedit,
2690 )
2690 )
@@ -1,873 +1,872 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2012 Olivia Mackall <olivia@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 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import contextlib
110 import contextlib
111 import itertools
111 import itertools
112 import os
112 import os
113 import pickle
113
114
114 from .i18n import _
115 from .i18n import _
115 from .node import (
116 from .node import (
116 hex,
117 hex,
117 short,
118 short,
118 )
119 )
119 from .thirdparty import attr
120 from .thirdparty import attr
120
121
121 from . import (
122 from . import (
122 error,
123 error,
123 pycompat,
124 pycompat,
124 templatefilters,
125 templatefilters,
125 templatekw,
126 templatekw,
126 templater,
127 templater,
127 templateutil,
128 templateutil,
128 util,
129 util,
129 )
130 )
130 from .utils import (
131 from .utils import (
131 cborutil,
132 cborutil,
132 dateutil,
133 dateutil,
133 stringutil,
134 stringutil,
134 )
135 )
135
136
136 pickle = util.pickle
137
138
137
139 def isprintable(obj):
138 def isprintable(obj):
140 """Check if the given object can be directly passed in to formatter's
139 """Check if the given object can be directly passed in to formatter's
141 write() and data() functions
140 write() and data() functions
142
141
143 Returns False if the object is unsupported or must be pre-processed by
142 Returns False if the object is unsupported or must be pre-processed by
144 formatdate(), formatdict(), or formatlist().
143 formatdate(), formatdict(), or formatlist().
145 """
144 """
146 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
145 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
147
146
148
147
149 class _nullconverter(object):
148 class _nullconverter(object):
150 '''convert non-primitive data types to be processed by formatter'''
149 '''convert non-primitive data types to be processed by formatter'''
151
150
152 # set to True if context object should be stored as item
151 # set to True if context object should be stored as item
153 storecontext = False
152 storecontext = False
154
153
155 @staticmethod
154 @staticmethod
156 def wrapnested(data, tmpl, sep):
155 def wrapnested(data, tmpl, sep):
157 '''wrap nested data by appropriate type'''
156 '''wrap nested data by appropriate type'''
158 return data
157 return data
159
158
160 @staticmethod
159 @staticmethod
161 def formatdate(date, fmt):
160 def formatdate(date, fmt):
162 '''convert date tuple to appropriate format'''
161 '''convert date tuple to appropriate format'''
163 # timestamp can be float, but the canonical form should be int
162 # timestamp can be float, but the canonical form should be int
164 ts, tz = date
163 ts, tz = date
165 return (int(ts), tz)
164 return (int(ts), tz)
166
165
167 @staticmethod
166 @staticmethod
168 def formatdict(data, key, value, fmt, sep):
167 def formatdict(data, key, value, fmt, sep):
169 '''convert dict or key-value pairs to appropriate dict format'''
168 '''convert dict or key-value pairs to appropriate dict format'''
170 # use plain dict instead of util.sortdict so that data can be
169 # use plain dict instead of util.sortdict so that data can be
171 # serialized as a builtin dict in pickle output
170 # serialized as a builtin dict in pickle output
172 return dict(data)
171 return dict(data)
173
172
174 @staticmethod
173 @staticmethod
175 def formatlist(data, name, fmt, sep):
174 def formatlist(data, name, fmt, sep):
176 '''convert iterable to appropriate list format'''
175 '''convert iterable to appropriate list format'''
177 return list(data)
176 return list(data)
178
177
179
178
180 class baseformatter(object):
179 class baseformatter(object):
181
180
182 # set to True if the formater output a strict format that does not support
181 # set to True if the formater output a strict format that does not support
183 # arbitrary output in the stream.
182 # arbitrary output in the stream.
184 strict_format = False
183 strict_format = False
185
184
186 def __init__(self, ui, topic, opts, converter):
185 def __init__(self, ui, topic, opts, converter):
187 self._ui = ui
186 self._ui = ui
188 self._topic = topic
187 self._topic = topic
189 self._opts = opts
188 self._opts = opts
190 self._converter = converter
189 self._converter = converter
191 self._item = None
190 self._item = None
192 # function to convert node to string suitable for this output
191 # function to convert node to string suitable for this output
193 self.hexfunc = hex
192 self.hexfunc = hex
194
193
195 def __enter__(self):
194 def __enter__(self):
196 return self
195 return self
197
196
198 def __exit__(self, exctype, excvalue, traceback):
197 def __exit__(self, exctype, excvalue, traceback):
199 if exctype is None:
198 if exctype is None:
200 self.end()
199 self.end()
201
200
202 def _showitem(self):
201 def _showitem(self):
203 '''show a formatted item once all data is collected'''
202 '''show a formatted item once all data is collected'''
204
203
205 def startitem(self):
204 def startitem(self):
206 '''begin an item in the format list'''
205 '''begin an item in the format list'''
207 if self._item is not None:
206 if self._item is not None:
208 self._showitem()
207 self._showitem()
209 self._item = {}
208 self._item = {}
210
209
211 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
210 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
212 '''convert date tuple to appropriate format'''
211 '''convert date tuple to appropriate format'''
213 return self._converter.formatdate(date, fmt)
212 return self._converter.formatdate(date, fmt)
214
213
215 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
214 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
216 '''convert dict or key-value pairs to appropriate dict format'''
215 '''convert dict or key-value pairs to appropriate dict format'''
217 return self._converter.formatdict(data, key, value, fmt, sep)
216 return self._converter.formatdict(data, key, value, fmt, sep)
218
217
219 def formatlist(self, data, name, fmt=None, sep=b' '):
218 def formatlist(self, data, name, fmt=None, sep=b' '):
220 '''convert iterable to appropriate list format'''
219 '''convert iterable to appropriate list format'''
221 # name is mandatory argument for now, but it could be optional if
220 # name is mandatory argument for now, but it could be optional if
222 # we have default template keyword, e.g. {item}
221 # we have default template keyword, e.g. {item}
223 return self._converter.formatlist(data, name, fmt, sep)
222 return self._converter.formatlist(data, name, fmt, sep)
224
223
225 def context(self, **ctxs):
224 def context(self, **ctxs):
226 '''insert context objects to be used to render template keywords'''
225 '''insert context objects to be used to render template keywords'''
227 ctxs = pycompat.byteskwargs(ctxs)
226 ctxs = pycompat.byteskwargs(ctxs)
228 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
227 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
229 if self._converter.storecontext:
228 if self._converter.storecontext:
230 # populate missing resources in fctx -> ctx -> repo order
229 # populate missing resources in fctx -> ctx -> repo order
231 if b'fctx' in ctxs and b'ctx' not in ctxs:
230 if b'fctx' in ctxs and b'ctx' not in ctxs:
232 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
231 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
233 if b'ctx' in ctxs and b'repo' not in ctxs:
232 if b'ctx' in ctxs and b'repo' not in ctxs:
234 ctxs[b'repo'] = ctxs[b'ctx'].repo()
233 ctxs[b'repo'] = ctxs[b'ctx'].repo()
235 self._item.update(ctxs)
234 self._item.update(ctxs)
236
235
237 def datahint(self):
236 def datahint(self):
238 '''set of field names to be referenced'''
237 '''set of field names to be referenced'''
239 return set()
238 return set()
240
239
241 def data(self, **data):
240 def data(self, **data):
242 '''insert data into item that's not shown in default output'''
241 '''insert data into item that's not shown in default output'''
243 data = pycompat.byteskwargs(data)
242 data = pycompat.byteskwargs(data)
244 self._item.update(data)
243 self._item.update(data)
245
244
246 def write(self, fields, deftext, *fielddata, **opts):
245 def write(self, fields, deftext, *fielddata, **opts):
247 '''do default text output while assigning data to item'''
246 '''do default text output while assigning data to item'''
248 fieldkeys = fields.split()
247 fieldkeys = fields.split()
249 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
248 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
250 self._item.update(zip(fieldkeys, fielddata))
249 self._item.update(zip(fieldkeys, fielddata))
251
250
252 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
251 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
253 '''do conditional write (primarily for plain formatter)'''
252 '''do conditional write (primarily for plain formatter)'''
254 fieldkeys = fields.split()
253 fieldkeys = fields.split()
255 assert len(fieldkeys) == len(fielddata)
254 assert len(fieldkeys) == len(fielddata)
256 self._item.update(zip(fieldkeys, fielddata))
255 self._item.update(zip(fieldkeys, fielddata))
257
256
258 def plain(self, text, **opts):
257 def plain(self, text, **opts):
259 '''show raw text for non-templated mode'''
258 '''show raw text for non-templated mode'''
260
259
261 def isplain(self):
260 def isplain(self):
262 '''check for plain formatter usage'''
261 '''check for plain formatter usage'''
263 return False
262 return False
264
263
265 def nested(self, field, tmpl=None, sep=b''):
264 def nested(self, field, tmpl=None, sep=b''):
266 '''sub formatter to store nested data in the specified field'''
265 '''sub formatter to store nested data in the specified field'''
267 data = []
266 data = []
268 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
267 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
269 return _nestedformatter(self._ui, self._converter, data)
268 return _nestedformatter(self._ui, self._converter, data)
270
269
271 def end(self):
270 def end(self):
272 '''end output for the formatter'''
271 '''end output for the formatter'''
273 if self._item is not None:
272 if self._item is not None:
274 self._showitem()
273 self._showitem()
275
274
276
275
277 def nullformatter(ui, topic, opts):
276 def nullformatter(ui, topic, opts):
278 '''formatter that prints nothing'''
277 '''formatter that prints nothing'''
279 return baseformatter(ui, topic, opts, converter=_nullconverter)
278 return baseformatter(ui, topic, opts, converter=_nullconverter)
280
279
281
280
282 class _nestedformatter(baseformatter):
281 class _nestedformatter(baseformatter):
283 '''build sub items and store them in the parent formatter'''
282 '''build sub items and store them in the parent formatter'''
284
283
285 def __init__(self, ui, converter, data):
284 def __init__(self, ui, converter, data):
286 baseformatter.__init__(
285 baseformatter.__init__(
287 self, ui, topic=b'', opts={}, converter=converter
286 self, ui, topic=b'', opts={}, converter=converter
288 )
287 )
289 self._data = data
288 self._data = data
290
289
291 def _showitem(self):
290 def _showitem(self):
292 self._data.append(self._item)
291 self._data.append(self._item)
293
292
294
293
295 def _iteritems(data):
294 def _iteritems(data):
296 '''iterate key-value pairs in stable order'''
295 '''iterate key-value pairs in stable order'''
297 if isinstance(data, dict):
296 if isinstance(data, dict):
298 return sorted(pycompat.iteritems(data))
297 return sorted(pycompat.iteritems(data))
299 return data
298 return data
300
299
301
300
302 class _plainconverter(object):
301 class _plainconverter(object):
303 '''convert non-primitive data types to text'''
302 '''convert non-primitive data types to text'''
304
303
305 storecontext = False
304 storecontext = False
306
305
307 @staticmethod
306 @staticmethod
308 def wrapnested(data, tmpl, sep):
307 def wrapnested(data, tmpl, sep):
309 raise error.ProgrammingError(b'plainformatter should never be nested')
308 raise error.ProgrammingError(b'plainformatter should never be nested')
310
309
311 @staticmethod
310 @staticmethod
312 def formatdate(date, fmt):
311 def formatdate(date, fmt):
313 '''stringify date tuple in the given format'''
312 '''stringify date tuple in the given format'''
314 return dateutil.datestr(date, fmt)
313 return dateutil.datestr(date, fmt)
315
314
316 @staticmethod
315 @staticmethod
317 def formatdict(data, key, value, fmt, sep):
316 def formatdict(data, key, value, fmt, sep):
318 '''stringify key-value pairs separated by sep'''
317 '''stringify key-value pairs separated by sep'''
319 prefmt = pycompat.identity
318 prefmt = pycompat.identity
320 if fmt is None:
319 if fmt is None:
321 fmt = b'%s=%s'
320 fmt = b'%s=%s'
322 prefmt = pycompat.bytestr
321 prefmt = pycompat.bytestr
323 return sep.join(
322 return sep.join(
324 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
323 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
325 )
324 )
326
325
327 @staticmethod
326 @staticmethod
328 def formatlist(data, name, fmt, sep):
327 def formatlist(data, name, fmt, sep):
329 '''stringify iterable separated by sep'''
328 '''stringify iterable separated by sep'''
330 prefmt = pycompat.identity
329 prefmt = pycompat.identity
331 if fmt is None:
330 if fmt is None:
332 fmt = b'%s'
331 fmt = b'%s'
333 prefmt = pycompat.bytestr
332 prefmt = pycompat.bytestr
334 return sep.join(fmt % prefmt(e) for e in data)
333 return sep.join(fmt % prefmt(e) for e in data)
335
334
336
335
337 class plainformatter(baseformatter):
336 class plainformatter(baseformatter):
338 '''the default text output scheme'''
337 '''the default text output scheme'''
339
338
340 def __init__(self, ui, out, topic, opts):
339 def __init__(self, ui, out, topic, opts):
341 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
340 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
342 if ui.debugflag:
341 if ui.debugflag:
343 self.hexfunc = hex
342 self.hexfunc = hex
344 else:
343 else:
345 self.hexfunc = short
344 self.hexfunc = short
346 if ui is out:
345 if ui is out:
347 self._write = ui.write
346 self._write = ui.write
348 else:
347 else:
349 self._write = lambda s, **opts: out.write(s)
348 self._write = lambda s, **opts: out.write(s)
350
349
351 def startitem(self):
350 def startitem(self):
352 pass
351 pass
353
352
354 def data(self, **data):
353 def data(self, **data):
355 pass
354 pass
356
355
357 def write(self, fields, deftext, *fielddata, **opts):
356 def write(self, fields, deftext, *fielddata, **opts):
358 self._write(deftext % fielddata, **opts)
357 self._write(deftext % fielddata, **opts)
359
358
360 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
359 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
361 '''do conditional write'''
360 '''do conditional write'''
362 if cond:
361 if cond:
363 self._write(deftext % fielddata, **opts)
362 self._write(deftext % fielddata, **opts)
364
363
365 def plain(self, text, **opts):
364 def plain(self, text, **opts):
366 self._write(text, **opts)
365 self._write(text, **opts)
367
366
368 def isplain(self):
367 def isplain(self):
369 return True
368 return True
370
369
371 def nested(self, field, tmpl=None, sep=b''):
370 def nested(self, field, tmpl=None, sep=b''):
372 # nested data will be directly written to ui
371 # nested data will be directly written to ui
373 return self
372 return self
374
373
375 def end(self):
374 def end(self):
376 pass
375 pass
377
376
378
377
379 class debugformatter(baseformatter):
378 class debugformatter(baseformatter):
380 def __init__(self, ui, out, topic, opts):
379 def __init__(self, ui, out, topic, opts):
381 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
380 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
382 self._out = out
381 self._out = out
383 self._out.write(b"%s = [\n" % self._topic)
382 self._out.write(b"%s = [\n" % self._topic)
384
383
385 def _showitem(self):
384 def _showitem(self):
386 self._out.write(
385 self._out.write(
387 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
386 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
388 )
387 )
389
388
390 def end(self):
389 def end(self):
391 baseformatter.end(self)
390 baseformatter.end(self)
392 self._out.write(b"]\n")
391 self._out.write(b"]\n")
393
392
394
393
395 class pickleformatter(baseformatter):
394 class pickleformatter(baseformatter):
396 def __init__(self, ui, out, topic, opts):
395 def __init__(self, ui, out, topic, opts):
397 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
396 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
398 self._out = out
397 self._out = out
399 self._data = []
398 self._data = []
400
399
401 def _showitem(self):
400 def _showitem(self):
402 self._data.append(self._item)
401 self._data.append(self._item)
403
402
404 def end(self):
403 def end(self):
405 baseformatter.end(self)
404 baseformatter.end(self)
406 self._out.write(pickle.dumps(self._data))
405 self._out.write(pickle.dumps(self._data))
407
406
408
407
409 class cborformatter(baseformatter):
408 class cborformatter(baseformatter):
410 '''serialize items as an indefinite-length CBOR array'''
409 '''serialize items as an indefinite-length CBOR array'''
411
410
412 def __init__(self, ui, out, topic, opts):
411 def __init__(self, ui, out, topic, opts):
413 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
412 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
414 self._out = out
413 self._out = out
415 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
414 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
416
415
417 def _showitem(self):
416 def _showitem(self):
418 self._out.write(b''.join(cborutil.streamencode(self._item)))
417 self._out.write(b''.join(cborutil.streamencode(self._item)))
419
418
420 def end(self):
419 def end(self):
421 baseformatter.end(self)
420 baseformatter.end(self)
422 self._out.write(cborutil.BREAK)
421 self._out.write(cborutil.BREAK)
423
422
424
423
425 class jsonformatter(baseformatter):
424 class jsonformatter(baseformatter):
426
425
427 strict_format = True
426 strict_format = True
428
427
429 def __init__(self, ui, out, topic, opts):
428 def __init__(self, ui, out, topic, opts):
430 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
429 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
431 self._out = out
430 self._out = out
432 self._out.write(b"[")
431 self._out.write(b"[")
433 self._first = True
432 self._first = True
434
433
435 def _showitem(self):
434 def _showitem(self):
436 if self._first:
435 if self._first:
437 self._first = False
436 self._first = False
438 else:
437 else:
439 self._out.write(b",")
438 self._out.write(b",")
440
439
441 self._out.write(b"\n {\n")
440 self._out.write(b"\n {\n")
442 first = True
441 first = True
443 for k, v in sorted(self._item.items()):
442 for k, v in sorted(self._item.items()):
444 if first:
443 if first:
445 first = False
444 first = False
446 else:
445 else:
447 self._out.write(b",\n")
446 self._out.write(b",\n")
448 u = templatefilters.json(v, paranoid=False)
447 u = templatefilters.json(v, paranoid=False)
449 self._out.write(b' "%s": %s' % (k, u))
448 self._out.write(b' "%s": %s' % (k, u))
450 self._out.write(b"\n }")
449 self._out.write(b"\n }")
451
450
452 def end(self):
451 def end(self):
453 baseformatter.end(self)
452 baseformatter.end(self)
454 self._out.write(b"\n]\n")
453 self._out.write(b"\n]\n")
455
454
456
455
457 class _templateconverter(object):
456 class _templateconverter(object):
458 '''convert non-primitive data types to be processed by templater'''
457 '''convert non-primitive data types to be processed by templater'''
459
458
460 storecontext = True
459 storecontext = True
461
460
462 @staticmethod
461 @staticmethod
463 def wrapnested(data, tmpl, sep):
462 def wrapnested(data, tmpl, sep):
464 '''wrap nested data by templatable type'''
463 '''wrap nested data by templatable type'''
465 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
464 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
466
465
467 @staticmethod
466 @staticmethod
468 def formatdate(date, fmt):
467 def formatdate(date, fmt):
469 '''return date tuple'''
468 '''return date tuple'''
470 return templateutil.date(date)
469 return templateutil.date(date)
471
470
472 @staticmethod
471 @staticmethod
473 def formatdict(data, key, value, fmt, sep):
472 def formatdict(data, key, value, fmt, sep):
474 '''build object that can be evaluated as either plain string or dict'''
473 '''build object that can be evaluated as either plain string or dict'''
475 data = util.sortdict(_iteritems(data))
474 data = util.sortdict(_iteritems(data))
476
475
477 def f():
476 def f():
478 yield _plainconverter.formatdict(data, key, value, fmt, sep)
477 yield _plainconverter.formatdict(data, key, value, fmt, sep)
479
478
480 return templateutil.hybriddict(
479 return templateutil.hybriddict(
481 data, key=key, value=value, fmt=fmt, gen=f
480 data, key=key, value=value, fmt=fmt, gen=f
482 )
481 )
483
482
484 @staticmethod
483 @staticmethod
485 def formatlist(data, name, fmt, sep):
484 def formatlist(data, name, fmt, sep):
486 '''build object that can be evaluated as either plain string or list'''
485 '''build object that can be evaluated as either plain string or list'''
487 data = list(data)
486 data = list(data)
488
487
489 def f():
488 def f():
490 yield _plainconverter.formatlist(data, name, fmt, sep)
489 yield _plainconverter.formatlist(data, name, fmt, sep)
491
490
492 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
491 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
493
492
494
493
495 class templateformatter(baseformatter):
494 class templateformatter(baseformatter):
496 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
495 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
497 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
496 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
498 self._out = out
497 self._out = out
499 self._tref = spec.ref
498 self._tref = spec.ref
500 self._t = loadtemplater(
499 self._t = loadtemplater(
501 ui,
500 ui,
502 spec,
501 spec,
503 defaults=templatekw.keywords,
502 defaults=templatekw.keywords,
504 resources=templateresources(ui),
503 resources=templateresources(ui),
505 cache=templatekw.defaulttempl,
504 cache=templatekw.defaulttempl,
506 )
505 )
507 if overridetemplates:
506 if overridetemplates:
508 self._t.cache.update(overridetemplates)
507 self._t.cache.update(overridetemplates)
509 self._parts = templatepartsmap(
508 self._parts = templatepartsmap(
510 spec, self._t, [b'docheader', b'docfooter', b'separator']
509 spec, self._t, [b'docheader', b'docfooter', b'separator']
511 )
510 )
512 self._counter = itertools.count()
511 self._counter = itertools.count()
513 self._renderitem(b'docheader', {})
512 self._renderitem(b'docheader', {})
514
513
515 def _showitem(self):
514 def _showitem(self):
516 item = self._item.copy()
515 item = self._item.copy()
517 item[b'index'] = index = next(self._counter)
516 item[b'index'] = index = next(self._counter)
518 if index > 0:
517 if index > 0:
519 self._renderitem(b'separator', {})
518 self._renderitem(b'separator', {})
520 self._renderitem(self._tref, item)
519 self._renderitem(self._tref, item)
521
520
522 def _renderitem(self, part, item):
521 def _renderitem(self, part, item):
523 if part not in self._parts:
522 if part not in self._parts:
524 return
523 return
525 ref = self._parts[part]
524 ref = self._parts[part]
526 # None can't be put in the mapping dict since it means <unset>
525 # None can't be put in the mapping dict since it means <unset>
527 for k, v in item.items():
526 for k, v in item.items():
528 if v is None:
527 if v is None:
529 item[k] = templateutil.wrappedvalue(v)
528 item[k] = templateutil.wrappedvalue(v)
530 self._out.write(self._t.render(ref, item))
529 self._out.write(self._t.render(ref, item))
531
530
532 @util.propertycache
531 @util.propertycache
533 def _symbolsused(self):
532 def _symbolsused(self):
534 return self._t.symbolsused(self._tref)
533 return self._t.symbolsused(self._tref)
535
534
536 def datahint(self):
535 def datahint(self):
537 '''set of field names to be referenced from the template'''
536 '''set of field names to be referenced from the template'''
538 return self._symbolsused[0]
537 return self._symbolsused[0]
539
538
540 def end(self):
539 def end(self):
541 baseformatter.end(self)
540 baseformatter.end(self)
542 self._renderitem(b'docfooter', {})
541 self._renderitem(b'docfooter', {})
543
542
544
543
545 @attr.s(frozen=True)
544 @attr.s(frozen=True)
546 class templatespec(object):
545 class templatespec(object):
547 ref = attr.ib()
546 ref = attr.ib()
548 tmpl = attr.ib()
547 tmpl = attr.ib()
549 mapfile = attr.ib()
548 mapfile = attr.ib()
550 refargs = attr.ib(default=None)
549 refargs = attr.ib(default=None)
551 fp = attr.ib(default=None)
550 fp = attr.ib(default=None)
552
551
553
552
554 def empty_templatespec():
553 def empty_templatespec():
555 return templatespec(None, None, None)
554 return templatespec(None, None, None)
556
555
557
556
558 def reference_templatespec(ref, refargs=None):
557 def reference_templatespec(ref, refargs=None):
559 return templatespec(ref, None, None, refargs)
558 return templatespec(ref, None, None, refargs)
560
559
561
560
562 def literal_templatespec(tmpl):
561 def literal_templatespec(tmpl):
563 if pycompat.ispy3:
562 if pycompat.ispy3:
564 assert not isinstance(tmpl, str), b'tmpl must not be a str'
563 assert not isinstance(tmpl, str), b'tmpl must not be a str'
565 return templatespec(b'', tmpl, None)
564 return templatespec(b'', tmpl, None)
566
565
567
566
568 def mapfile_templatespec(topic, mapfile, fp=None):
567 def mapfile_templatespec(topic, mapfile, fp=None):
569 return templatespec(topic, None, mapfile, fp=fp)
568 return templatespec(topic, None, mapfile, fp=fp)
570
569
571
570
572 def lookuptemplate(ui, topic, tmpl):
571 def lookuptemplate(ui, topic, tmpl):
573 """Find the template matching the given -T/--template spec 'tmpl'
572 """Find the template matching the given -T/--template spec 'tmpl'
574
573
575 'tmpl' can be any of the following:
574 'tmpl' can be any of the following:
576
575
577 - a literal template (e.g. '{rev}')
576 - a literal template (e.g. '{rev}')
578 - a reference to built-in template (i.e. formatter)
577 - a reference to built-in template (i.e. formatter)
579 - a map-file name or path (e.g. 'changelog')
578 - a map-file name or path (e.g. 'changelog')
580 - a reference to [templates] in config file
579 - a reference to [templates] in config file
581 - a path to raw template file
580 - a path to raw template file
582
581
583 A map file defines a stand-alone template environment. If a map file
582 A map file defines a stand-alone template environment. If a map file
584 selected, all templates defined in the file will be loaded, and the
583 selected, all templates defined in the file will be loaded, and the
585 template matching the given topic will be rendered. Aliases won't be
584 template matching the given topic will be rendered. Aliases won't be
586 loaded from user config, but from the map file.
585 loaded from user config, but from the map file.
587
586
588 If no map file selected, all templates in [templates] section will be
587 If no map file selected, all templates in [templates] section will be
589 available as well as aliases in [templatealias].
588 available as well as aliases in [templatealias].
590 """
589 """
591
590
592 if not tmpl:
591 if not tmpl:
593 return empty_templatespec()
592 return empty_templatespec()
594
593
595 # looks like a literal template?
594 # looks like a literal template?
596 if b'{' in tmpl:
595 if b'{' in tmpl:
597 return literal_templatespec(tmpl)
596 return literal_templatespec(tmpl)
598
597
599 # a reference to built-in (formatter) template
598 # a reference to built-in (formatter) template
600 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
599 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
601 return reference_templatespec(tmpl)
600 return reference_templatespec(tmpl)
602
601
603 # a function-style reference to built-in template
602 # a function-style reference to built-in template
604 func, fsep, ftail = tmpl.partition(b'(')
603 func, fsep, ftail = tmpl.partition(b'(')
605 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
604 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
606 templater.parseexpr(tmpl) # make sure syntax errors are confined
605 templater.parseexpr(tmpl) # make sure syntax errors are confined
607 return reference_templatespec(func, refargs=ftail[:-1])
606 return reference_templatespec(func, refargs=ftail[:-1])
608
607
609 # perhaps a stock style?
608 # perhaps a stock style?
610 if not os.path.split(tmpl)[0]:
609 if not os.path.split(tmpl)[0]:
611 (mapname, fp) = templater.try_open_template(
610 (mapname, fp) = templater.try_open_template(
612 b'map-cmdline.' + tmpl
611 b'map-cmdline.' + tmpl
613 ) or templater.try_open_template(tmpl)
612 ) or templater.try_open_template(tmpl)
614 if mapname:
613 if mapname:
615 return mapfile_templatespec(topic, mapname, fp)
614 return mapfile_templatespec(topic, mapname, fp)
616
615
617 # perhaps it's a reference to [templates]
616 # perhaps it's a reference to [templates]
618 if ui.config(b'templates', tmpl):
617 if ui.config(b'templates', tmpl):
619 return reference_templatespec(tmpl)
618 return reference_templatespec(tmpl)
620
619
621 if tmpl == b'list':
620 if tmpl == b'list':
622 ui.write(_(b"available styles: %s\n") % templater.stylelist())
621 ui.write(_(b"available styles: %s\n") % templater.stylelist())
623 raise error.Abort(_(b"specify a template"))
622 raise error.Abort(_(b"specify a template"))
624
623
625 # perhaps it's a path to a map or a template
624 # perhaps it's a path to a map or a template
626 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
625 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
627 # is it a mapfile for a style?
626 # is it a mapfile for a style?
628 if os.path.basename(tmpl).startswith(b"map-"):
627 if os.path.basename(tmpl).startswith(b"map-"):
629 return mapfile_templatespec(topic, os.path.realpath(tmpl))
628 return mapfile_templatespec(topic, os.path.realpath(tmpl))
630 with util.posixfile(tmpl, b'rb') as f:
629 with util.posixfile(tmpl, b'rb') as f:
631 tmpl = f.read()
630 tmpl = f.read()
632 return literal_templatespec(tmpl)
631 return literal_templatespec(tmpl)
633
632
634 # constant string?
633 # constant string?
635 return literal_templatespec(tmpl)
634 return literal_templatespec(tmpl)
636
635
637
636
638 def templatepartsmap(spec, t, partnames):
637 def templatepartsmap(spec, t, partnames):
639 """Create a mapping of {part: ref}"""
638 """Create a mapping of {part: ref}"""
640 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
639 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
641 if spec.mapfile:
640 if spec.mapfile:
642 partsmap.update((p, p) for p in partnames if p in t)
641 partsmap.update((p, p) for p in partnames if p in t)
643 elif spec.ref:
642 elif spec.ref:
644 for part in partnames:
643 for part in partnames:
645 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
644 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
646 if ref in t:
645 if ref in t:
647 partsmap[part] = ref
646 partsmap[part] = ref
648 return partsmap
647 return partsmap
649
648
650
649
651 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
650 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
652 """Create a templater from either a literal template or loading from
651 """Create a templater from either a literal template or loading from
653 a map file"""
652 a map file"""
654 assert not (spec.tmpl and spec.mapfile)
653 assert not (spec.tmpl and spec.mapfile)
655 if spec.mapfile:
654 if spec.mapfile:
656 return templater.templater.frommapfile(
655 return templater.templater.frommapfile(
657 spec.mapfile,
656 spec.mapfile,
658 spec.fp,
657 spec.fp,
659 defaults=defaults,
658 defaults=defaults,
660 resources=resources,
659 resources=resources,
661 cache=cache,
660 cache=cache,
662 )
661 )
663 return maketemplater(
662 return maketemplater(
664 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
663 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
665 )
664 )
666
665
667
666
668 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
667 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
669 """Create a templater from a string template 'tmpl'"""
668 """Create a templater from a string template 'tmpl'"""
670 aliases = ui.configitems(b'templatealias')
669 aliases = ui.configitems(b'templatealias')
671 t = templater.templater(
670 t = templater.templater(
672 defaults=defaults, resources=resources, cache=cache, aliases=aliases
671 defaults=defaults, resources=resources, cache=cache, aliases=aliases
673 )
672 )
674 t.cache.update(
673 t.cache.update(
675 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
674 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
676 )
675 )
677 if tmpl:
676 if tmpl:
678 t.cache[b''] = tmpl
677 t.cache[b''] = tmpl
679 return t
678 return t
680
679
681
680
682 # marker to denote a resource to be loaded on demand based on mapping values
681 # marker to denote a resource to be loaded on demand based on mapping values
683 # (e.g. (ctx, path) -> fctx)
682 # (e.g. (ctx, path) -> fctx)
684 _placeholder = object()
683 _placeholder = object()
685
684
686
685
687 class templateresources(templater.resourcemapper):
686 class templateresources(templater.resourcemapper):
688 """Resource mapper designed for the default templatekw and function"""
687 """Resource mapper designed for the default templatekw and function"""
689
688
690 def __init__(self, ui, repo=None):
689 def __init__(self, ui, repo=None):
691 self._resmap = {
690 self._resmap = {
692 b'cache': {}, # for templatekw/funcs to store reusable data
691 b'cache': {}, # for templatekw/funcs to store reusable data
693 b'repo': repo,
692 b'repo': repo,
694 b'ui': ui,
693 b'ui': ui,
695 }
694 }
696
695
697 def availablekeys(self, mapping):
696 def availablekeys(self, mapping):
698 return {
697 return {
699 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
698 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
700 }
699 }
701
700
702 def knownkeys(self):
701 def knownkeys(self):
703 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
702 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
704
703
705 def lookup(self, mapping, key):
704 def lookup(self, mapping, key):
706 if key not in self.knownkeys():
705 if key not in self.knownkeys():
707 return None
706 return None
708 v = self._getsome(mapping, key)
707 v = self._getsome(mapping, key)
709 if v is _placeholder:
708 if v is _placeholder:
710 v = mapping[key] = self._loadermap[key](self, mapping)
709 v = mapping[key] = self._loadermap[key](self, mapping)
711 return v
710 return v
712
711
713 def populatemap(self, context, origmapping, newmapping):
712 def populatemap(self, context, origmapping, newmapping):
714 mapping = {}
713 mapping = {}
715 if self._hasnodespec(newmapping):
714 if self._hasnodespec(newmapping):
716 mapping[b'revcache'] = {} # per-ctx cache
715 mapping[b'revcache'] = {} # per-ctx cache
717 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
716 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
718 orignode = templateutil.runsymbol(context, origmapping, b'node')
717 orignode = templateutil.runsymbol(context, origmapping, b'node')
719 mapping[b'originalnode'] = orignode
718 mapping[b'originalnode'] = orignode
720 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
719 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
721 # its existence to be reported by availablekeys()
720 # its existence to be reported by availablekeys()
722 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
721 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
723 mapping[b'ctx'] = _placeholder
722 mapping[b'ctx'] = _placeholder
724 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
723 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
725 mapping[b'fctx'] = _placeholder
724 mapping[b'fctx'] = _placeholder
726 return mapping
725 return mapping
727
726
728 def _getsome(self, mapping, key):
727 def _getsome(self, mapping, key):
729 v = mapping.get(key)
728 v = mapping.get(key)
730 if v is not None:
729 if v is not None:
731 return v
730 return v
732 return self._resmap.get(key)
731 return self._resmap.get(key)
733
732
734 def _hasliteral(self, mapping, key):
733 def _hasliteral(self, mapping, key):
735 """Test if a literal value is set or unset in the given mapping"""
734 """Test if a literal value is set or unset in the given mapping"""
736 return key in mapping and not callable(mapping[key])
735 return key in mapping and not callable(mapping[key])
737
736
738 def _getliteral(self, mapping, key):
737 def _getliteral(self, mapping, key):
739 """Return value of the given name if it is a literal"""
738 """Return value of the given name if it is a literal"""
740 v = mapping.get(key)
739 v = mapping.get(key)
741 if callable(v):
740 if callable(v):
742 return None
741 return None
743 return v
742 return v
744
743
745 def _hasnodespec(self, mapping):
744 def _hasnodespec(self, mapping):
746 """Test if context revision is set or unset in the given mapping"""
745 """Test if context revision is set or unset in the given mapping"""
747 return b'node' in mapping or b'ctx' in mapping
746 return b'node' in mapping or b'ctx' in mapping
748
747
749 def _loadctx(self, mapping):
748 def _loadctx(self, mapping):
750 repo = self._getsome(mapping, b'repo')
749 repo = self._getsome(mapping, b'repo')
751 node = self._getliteral(mapping, b'node')
750 node = self._getliteral(mapping, b'node')
752 if repo is None or node is None:
751 if repo is None or node is None:
753 return
752 return
754 try:
753 try:
755 return repo[node]
754 return repo[node]
756 except error.RepoLookupError:
755 except error.RepoLookupError:
757 return None # maybe hidden/non-existent node
756 return None # maybe hidden/non-existent node
758
757
759 def _loadfctx(self, mapping):
758 def _loadfctx(self, mapping):
760 ctx = self._getsome(mapping, b'ctx')
759 ctx = self._getsome(mapping, b'ctx')
761 path = self._getliteral(mapping, b'path')
760 path = self._getliteral(mapping, b'path')
762 if ctx is None or path is None:
761 if ctx is None or path is None:
763 return None
762 return None
764 try:
763 try:
765 return ctx[path]
764 return ctx[path]
766 except error.LookupError:
765 except error.LookupError:
767 return None # maybe removed file?
766 return None # maybe removed file?
768
767
769 _loadermap = {
768 _loadermap = {
770 b'ctx': _loadctx,
769 b'ctx': _loadctx,
771 b'fctx': _loadfctx,
770 b'fctx': _loadfctx,
772 }
771 }
773
772
774
773
775 def _internaltemplateformatter(
774 def _internaltemplateformatter(
776 ui,
775 ui,
777 out,
776 out,
778 topic,
777 topic,
779 opts,
778 opts,
780 spec,
779 spec,
781 tmpl,
780 tmpl,
782 docheader=b'',
781 docheader=b'',
783 docfooter=b'',
782 docfooter=b'',
784 separator=b'',
783 separator=b'',
785 ):
784 ):
786 """Build template formatter that handles customizable built-in templates
785 """Build template formatter that handles customizable built-in templates
787 such as -Tjson(...)"""
786 such as -Tjson(...)"""
788 templates = {spec.ref: tmpl}
787 templates = {spec.ref: tmpl}
789 if docheader:
788 if docheader:
790 templates[b'%s:docheader' % spec.ref] = docheader
789 templates[b'%s:docheader' % spec.ref] = docheader
791 if docfooter:
790 if docfooter:
792 templates[b'%s:docfooter' % spec.ref] = docfooter
791 templates[b'%s:docfooter' % spec.ref] = docfooter
793 if separator:
792 if separator:
794 templates[b'%s:separator' % spec.ref] = separator
793 templates[b'%s:separator' % spec.ref] = separator
795 return templateformatter(
794 return templateformatter(
796 ui, out, topic, opts, spec, overridetemplates=templates
795 ui, out, topic, opts, spec, overridetemplates=templates
797 )
796 )
798
797
799
798
800 def formatter(ui, out, topic, opts):
799 def formatter(ui, out, topic, opts):
801 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
800 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
802 if spec.ref == b"cbor" and spec.refargs is not None:
801 if spec.ref == b"cbor" and spec.refargs is not None:
803 return _internaltemplateformatter(
802 return _internaltemplateformatter(
804 ui,
803 ui,
805 out,
804 out,
806 topic,
805 topic,
807 opts,
806 opts,
808 spec,
807 spec,
809 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
808 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
810 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
809 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
811 docfooter=cborutil.BREAK,
810 docfooter=cborutil.BREAK,
812 )
811 )
813 elif spec.ref == b"cbor":
812 elif spec.ref == b"cbor":
814 return cborformatter(ui, out, topic, opts)
813 return cborformatter(ui, out, topic, opts)
815 elif spec.ref == b"json" and spec.refargs is not None:
814 elif spec.ref == b"json" and spec.refargs is not None:
816 return _internaltemplateformatter(
815 return _internaltemplateformatter(
817 ui,
816 ui,
818 out,
817 out,
819 topic,
818 topic,
820 opts,
819 opts,
821 spec,
820 spec,
822 tmpl=b'{dict(%s)|json}' % spec.refargs,
821 tmpl=b'{dict(%s)|json}' % spec.refargs,
823 docheader=b'[\n ',
822 docheader=b'[\n ',
824 docfooter=b'\n]\n',
823 docfooter=b'\n]\n',
825 separator=b',\n ',
824 separator=b',\n ',
826 )
825 )
827 elif spec.ref == b"json":
826 elif spec.ref == b"json":
828 return jsonformatter(ui, out, topic, opts)
827 return jsonformatter(ui, out, topic, opts)
829 elif spec.ref == b"pickle":
828 elif spec.ref == b"pickle":
830 assert spec.refargs is None, r'function-style not supported'
829 assert spec.refargs is None, r'function-style not supported'
831 return pickleformatter(ui, out, topic, opts)
830 return pickleformatter(ui, out, topic, opts)
832 elif spec.ref == b"debug":
831 elif spec.ref == b"debug":
833 assert spec.refargs is None, r'function-style not supported'
832 assert spec.refargs is None, r'function-style not supported'
834 return debugformatter(ui, out, topic, opts)
833 return debugformatter(ui, out, topic, opts)
835 elif spec.ref or spec.tmpl or spec.mapfile:
834 elif spec.ref or spec.tmpl or spec.mapfile:
836 assert spec.refargs is None, r'function-style not supported'
835 assert spec.refargs is None, r'function-style not supported'
837 return templateformatter(ui, out, topic, opts, spec)
836 return templateformatter(ui, out, topic, opts, spec)
838 # developer config: ui.formatdebug
837 # developer config: ui.formatdebug
839 elif ui.configbool(b'ui', b'formatdebug'):
838 elif ui.configbool(b'ui', b'formatdebug'):
840 return debugformatter(ui, out, topic, opts)
839 return debugformatter(ui, out, topic, opts)
841 # deprecated config: ui.formatjson
840 # deprecated config: ui.formatjson
842 elif ui.configbool(b'ui', b'formatjson'):
841 elif ui.configbool(b'ui', b'formatjson'):
843 return jsonformatter(ui, out, topic, opts)
842 return jsonformatter(ui, out, topic, opts)
844 return plainformatter(ui, out, topic, opts)
843 return plainformatter(ui, out, topic, opts)
845
844
846
845
847 @contextlib.contextmanager
846 @contextlib.contextmanager
848 def openformatter(ui, filename, topic, opts):
847 def openformatter(ui, filename, topic, opts):
849 """Create a formatter that writes outputs to the specified file
848 """Create a formatter that writes outputs to the specified file
850
849
851 Must be invoked using the 'with' statement.
850 Must be invoked using the 'with' statement.
852 """
851 """
853 with util.posixfile(filename, b'wb') as out:
852 with util.posixfile(filename, b'wb') as out:
854 with formatter(ui, out, topic, opts) as fm:
853 with formatter(ui, out, topic, opts) as fm:
855 yield fm
854 yield fm
856
855
857
856
858 @contextlib.contextmanager
857 @contextlib.contextmanager
859 def _neverending(fm):
858 def _neverending(fm):
860 yield fm
859 yield fm
861
860
862
861
863 def maybereopen(fm, filename):
862 def maybereopen(fm, filename):
864 """Create a formatter backed by file if filename specified, else return
863 """Create a formatter backed by file if filename specified, else return
865 the given formatter
864 the given formatter
866
865
867 Must be invoked using the 'with' statement. This will never call fm.end()
866 Must be invoked using the 'with' statement. This will never call fm.end()
868 of the given formatter.
867 of the given formatter.
869 """
868 """
870 if filename:
869 if filename:
871 return openformatter(fm._ui, filename, fm._topic, fm._opts)
870 return openformatter(fm._ui, filename, fm._topic, fm._opts)
872 else:
871 else:
873 return _neverending(fm)
872 return _neverending(fm)
@@ -1,3361 +1,3360 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@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 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import, print_function
16 from __future__ import absolute_import, print_function
17
17
18 import abc
18 import abc
19 import collections
19 import collections
20 import contextlib
20 import contextlib
21 import errno
21 import errno
22 import gc
22 import gc
23 import hashlib
23 import hashlib
24 import itertools
24 import itertools
25 import locale
25 import locale
26 import mmap
26 import mmap
27 import os
27 import os
28 import platform as pyplatform
28 import platform as pyplatform
29 import re as remod
29 import re as remod
30 import shutil
30 import shutil
31 import stat
31 import stat
32 import sys
32 import sys
33 import time
33 import time
34 import traceback
34 import traceback
35 import warnings
35 import warnings
36
36
37 from .node import hex
37 from .node import hex
38 from .thirdparty import attr
38 from .thirdparty import attr
39 from .pycompat import (
39 from .pycompat import (
40 delattr,
40 delattr,
41 getattr,
41 getattr,
42 open,
42 open,
43 setattr,
43 setattr,
44 )
44 )
45 from .node import hex
45 from .node import hex
46 from hgdemandimport import tracing
46 from hgdemandimport import tracing
47 from . import (
47 from . import (
48 encoding,
48 encoding,
49 error,
49 error,
50 i18n,
50 i18n,
51 policy,
51 policy,
52 pycompat,
52 pycompat,
53 urllibcompat,
53 urllibcompat,
54 )
54 )
55 from .utils import (
55 from .utils import (
56 compression,
56 compression,
57 hashutil,
57 hashutil,
58 procutil,
58 procutil,
59 stringutil,
59 stringutil,
60 )
60 )
61
61
62 if pycompat.TYPE_CHECKING:
62 if pycompat.TYPE_CHECKING:
63 from typing import (
63 from typing import (
64 Iterator,
64 Iterator,
65 List,
65 List,
66 Optional,
66 Optional,
67 Tuple,
67 Tuple,
68 )
68 )
69
69
70
70
71 base85 = policy.importmod('base85')
71 base85 = policy.importmod('base85')
72 osutil = policy.importmod('osutil')
72 osutil = policy.importmod('osutil')
73
73
74 b85decode = base85.b85decode
74 b85decode = base85.b85decode
75 b85encode = base85.b85encode
75 b85encode = base85.b85encode
76
76
77 cookielib = pycompat.cookielib
77 cookielib = pycompat.cookielib
78 httplib = pycompat.httplib
78 httplib = pycompat.httplib
79 pickle = pycompat.pickle
80 safehasattr = pycompat.safehasattr
79 safehasattr = pycompat.safehasattr
81 socketserver = pycompat.socketserver
80 socketserver = pycompat.socketserver
82 bytesio = pycompat.bytesio
81 bytesio = pycompat.bytesio
83 # TODO deprecate stringio name, as it is a lie on Python 3.
82 # TODO deprecate stringio name, as it is a lie on Python 3.
84 stringio = bytesio
83 stringio = bytesio
85 xmlrpclib = pycompat.xmlrpclib
84 xmlrpclib = pycompat.xmlrpclib
86
85
87 httpserver = urllibcompat.httpserver
86 httpserver = urllibcompat.httpserver
88 urlerr = urllibcompat.urlerr
87 urlerr = urllibcompat.urlerr
89 urlreq = urllibcompat.urlreq
88 urlreq = urllibcompat.urlreq
90
89
91 # workaround for win32mbcs
90 # workaround for win32mbcs
92 _filenamebytestr = pycompat.bytestr
91 _filenamebytestr = pycompat.bytestr
93
92
94 if pycompat.iswindows:
93 if pycompat.iswindows:
95 from . import windows as platform
94 from . import windows as platform
96 else:
95 else:
97 from . import posix as platform
96 from . import posix as platform
98
97
99 _ = i18n._
98 _ = i18n._
100
99
101 abspath = platform.abspath
100 abspath = platform.abspath
102 bindunixsocket = platform.bindunixsocket
101 bindunixsocket = platform.bindunixsocket
103 cachestat = platform.cachestat
102 cachestat = platform.cachestat
104 checkexec = platform.checkexec
103 checkexec = platform.checkexec
105 checklink = platform.checklink
104 checklink = platform.checklink
106 copymode = platform.copymode
105 copymode = platform.copymode
107 expandglobs = platform.expandglobs
106 expandglobs = platform.expandglobs
108 getfsmountpoint = platform.getfsmountpoint
107 getfsmountpoint = platform.getfsmountpoint
109 getfstype = platform.getfstype
108 getfstype = platform.getfstype
110 get_password = platform.get_password
109 get_password = platform.get_password
111 groupmembers = platform.groupmembers
110 groupmembers = platform.groupmembers
112 groupname = platform.groupname
111 groupname = platform.groupname
113 isexec = platform.isexec
112 isexec = platform.isexec
114 isowner = platform.isowner
113 isowner = platform.isowner
115 listdir = osutil.listdir
114 listdir = osutil.listdir
116 localpath = platform.localpath
115 localpath = platform.localpath
117 lookupreg = platform.lookupreg
116 lookupreg = platform.lookupreg
118 makedir = platform.makedir
117 makedir = platform.makedir
119 nlinks = platform.nlinks
118 nlinks = platform.nlinks
120 normpath = platform.normpath
119 normpath = platform.normpath
121 normcase = platform.normcase
120 normcase = platform.normcase
122 normcasespec = platform.normcasespec
121 normcasespec = platform.normcasespec
123 normcasefallback = platform.normcasefallback
122 normcasefallback = platform.normcasefallback
124 openhardlinks = platform.openhardlinks
123 openhardlinks = platform.openhardlinks
125 oslink = platform.oslink
124 oslink = platform.oslink
126 parsepatchoutput = platform.parsepatchoutput
125 parsepatchoutput = platform.parsepatchoutput
127 pconvert = platform.pconvert
126 pconvert = platform.pconvert
128 poll = platform.poll
127 poll = platform.poll
129 posixfile = platform.posixfile
128 posixfile = platform.posixfile
130 readlink = platform.readlink
129 readlink = platform.readlink
131 rename = platform.rename
130 rename = platform.rename
132 removedirs = platform.removedirs
131 removedirs = platform.removedirs
133 samedevice = platform.samedevice
132 samedevice = platform.samedevice
134 samefile = platform.samefile
133 samefile = platform.samefile
135 samestat = platform.samestat
134 samestat = platform.samestat
136 setflags = platform.setflags
135 setflags = platform.setflags
137 split = platform.split
136 split = platform.split
138 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
137 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
139 statisexec = platform.statisexec
138 statisexec = platform.statisexec
140 statislink = platform.statislink
139 statislink = platform.statislink
141 umask = platform.umask
140 umask = platform.umask
142 unlink = platform.unlink
141 unlink = platform.unlink
143 username = platform.username
142 username = platform.username
144
143
145
144
146 def setumask(val):
145 def setumask(val):
147 # type: (int) -> None
146 # type: (int) -> None
148 '''updates the umask. used by chg server'''
147 '''updates the umask. used by chg server'''
149 if pycompat.iswindows:
148 if pycompat.iswindows:
150 return
149 return
151 os.umask(val)
150 os.umask(val)
152 global umask
151 global umask
153 platform.umask = umask = val & 0o777
152 platform.umask = umask = val & 0o777
154
153
155
154
156 # small compat layer
155 # small compat layer
157 compengines = compression.compengines
156 compengines = compression.compengines
158 SERVERROLE = compression.SERVERROLE
157 SERVERROLE = compression.SERVERROLE
159 CLIENTROLE = compression.CLIENTROLE
158 CLIENTROLE = compression.CLIENTROLE
160
159
161 try:
160 try:
162 recvfds = osutil.recvfds
161 recvfds = osutil.recvfds
163 except AttributeError:
162 except AttributeError:
164 pass
163 pass
165
164
166 # Python compatibility
165 # Python compatibility
167
166
168 _notset = object()
167 _notset = object()
169
168
170
169
171 def bitsfrom(container):
170 def bitsfrom(container):
172 bits = 0
171 bits = 0
173 for bit in container:
172 for bit in container:
174 bits |= bit
173 bits |= bit
175 return bits
174 return bits
176
175
177
176
178 # python 2.6 still have deprecation warning enabled by default. We do not want
177 # python 2.6 still have deprecation warning enabled by default. We do not want
179 # to display anything to standard user so detect if we are running test and
178 # to display anything to standard user so detect if we are running test and
180 # only use python deprecation warning in this case.
179 # only use python deprecation warning in this case.
181 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
180 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
182 if _dowarn:
181 if _dowarn:
183 # explicitly unfilter our warning for python 2.7
182 # explicitly unfilter our warning for python 2.7
184 #
183 #
185 # The option of setting PYTHONWARNINGS in the test runner was investigated.
184 # The option of setting PYTHONWARNINGS in the test runner was investigated.
186 # However, module name set through PYTHONWARNINGS was exactly matched, so
185 # However, module name set through PYTHONWARNINGS was exactly matched, so
187 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
186 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
188 # makes the whole PYTHONWARNINGS thing useless for our usecase.
187 # makes the whole PYTHONWARNINGS thing useless for our usecase.
189 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
188 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
190 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
189 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
191 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
190 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
192 if _dowarn and pycompat.ispy3:
191 if _dowarn and pycompat.ispy3:
193 # silence warning emitted by passing user string to re.sub()
192 # silence warning emitted by passing user string to re.sub()
194 warnings.filterwarnings(
193 warnings.filterwarnings(
195 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
194 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
196 )
195 )
197 warnings.filterwarnings(
196 warnings.filterwarnings(
198 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
197 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
199 )
198 )
200 # TODO: reinvent imp.is_frozen()
199 # TODO: reinvent imp.is_frozen()
201 warnings.filterwarnings(
200 warnings.filterwarnings(
202 'ignore',
201 'ignore',
203 'the imp module is deprecated',
202 'the imp module is deprecated',
204 DeprecationWarning,
203 DeprecationWarning,
205 'mercurial',
204 'mercurial',
206 )
205 )
207
206
208
207
209 def nouideprecwarn(msg, version, stacklevel=1):
208 def nouideprecwarn(msg, version, stacklevel=1):
210 """Issue an python native deprecation warning
209 """Issue an python native deprecation warning
211
210
212 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
211 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
213 """
212 """
214 if _dowarn:
213 if _dowarn:
215 msg += (
214 msg += (
216 b"\n(compatibility will be dropped after Mercurial-%s,"
215 b"\n(compatibility will be dropped after Mercurial-%s,"
217 b" update your code.)"
216 b" update your code.)"
218 ) % version
217 ) % version
219 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
218 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
220 # on python 3 with chg, we will need to explicitly flush the output
219 # on python 3 with chg, we will need to explicitly flush the output
221 sys.stderr.flush()
220 sys.stderr.flush()
222
221
223
222
224 DIGESTS = {
223 DIGESTS = {
225 b'md5': hashlib.md5,
224 b'md5': hashlib.md5,
226 b'sha1': hashutil.sha1,
225 b'sha1': hashutil.sha1,
227 b'sha512': hashlib.sha512,
226 b'sha512': hashlib.sha512,
228 }
227 }
229 # List of digest types from strongest to weakest
228 # List of digest types from strongest to weakest
230 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
229 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
231
230
232 for k in DIGESTS_BY_STRENGTH:
231 for k in DIGESTS_BY_STRENGTH:
233 assert k in DIGESTS
232 assert k in DIGESTS
234
233
235
234
236 class digester(object):
235 class digester(object):
237 """helper to compute digests.
236 """helper to compute digests.
238
237
239 This helper can be used to compute one or more digests given their name.
238 This helper can be used to compute one or more digests given their name.
240
239
241 >>> d = digester([b'md5', b'sha1'])
240 >>> d = digester([b'md5', b'sha1'])
242 >>> d.update(b'foo')
241 >>> d.update(b'foo')
243 >>> [k for k in sorted(d)]
242 >>> [k for k in sorted(d)]
244 ['md5', 'sha1']
243 ['md5', 'sha1']
245 >>> d[b'md5']
244 >>> d[b'md5']
246 'acbd18db4cc2f85cedef654fccc4a4d8'
245 'acbd18db4cc2f85cedef654fccc4a4d8'
247 >>> d[b'sha1']
246 >>> d[b'sha1']
248 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
247 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
249 >>> digester.preferred([b'md5', b'sha1'])
248 >>> digester.preferred([b'md5', b'sha1'])
250 'sha1'
249 'sha1'
251 """
250 """
252
251
253 def __init__(self, digests, s=b''):
252 def __init__(self, digests, s=b''):
254 self._hashes = {}
253 self._hashes = {}
255 for k in digests:
254 for k in digests:
256 if k not in DIGESTS:
255 if k not in DIGESTS:
257 raise error.Abort(_(b'unknown digest type: %s') % k)
256 raise error.Abort(_(b'unknown digest type: %s') % k)
258 self._hashes[k] = DIGESTS[k]()
257 self._hashes[k] = DIGESTS[k]()
259 if s:
258 if s:
260 self.update(s)
259 self.update(s)
261
260
262 def update(self, data):
261 def update(self, data):
263 for h in self._hashes.values():
262 for h in self._hashes.values():
264 h.update(data)
263 h.update(data)
265
264
266 def __getitem__(self, key):
265 def __getitem__(self, key):
267 if key not in DIGESTS:
266 if key not in DIGESTS:
268 raise error.Abort(_(b'unknown digest type: %s') % k)
267 raise error.Abort(_(b'unknown digest type: %s') % k)
269 return hex(self._hashes[key].digest())
268 return hex(self._hashes[key].digest())
270
269
271 def __iter__(self):
270 def __iter__(self):
272 return iter(self._hashes)
271 return iter(self._hashes)
273
272
274 @staticmethod
273 @staticmethod
275 def preferred(supported):
274 def preferred(supported):
276 """returns the strongest digest type in both supported and DIGESTS."""
275 """returns the strongest digest type in both supported and DIGESTS."""
277
276
278 for k in DIGESTS_BY_STRENGTH:
277 for k in DIGESTS_BY_STRENGTH:
279 if k in supported:
278 if k in supported:
280 return k
279 return k
281 return None
280 return None
282
281
283
282
284 class digestchecker(object):
283 class digestchecker(object):
285 """file handle wrapper that additionally checks content against a given
284 """file handle wrapper that additionally checks content against a given
286 size and digests.
285 size and digests.
287
286
288 d = digestchecker(fh, size, {'md5': '...'})
287 d = digestchecker(fh, size, {'md5': '...'})
289
288
290 When multiple digests are given, all of them are validated.
289 When multiple digests are given, all of them are validated.
291 """
290 """
292
291
293 def __init__(self, fh, size, digests):
292 def __init__(self, fh, size, digests):
294 self._fh = fh
293 self._fh = fh
295 self._size = size
294 self._size = size
296 self._got = 0
295 self._got = 0
297 self._digests = dict(digests)
296 self._digests = dict(digests)
298 self._digester = digester(self._digests.keys())
297 self._digester = digester(self._digests.keys())
299
298
300 def read(self, length=-1):
299 def read(self, length=-1):
301 content = self._fh.read(length)
300 content = self._fh.read(length)
302 self._digester.update(content)
301 self._digester.update(content)
303 self._got += len(content)
302 self._got += len(content)
304 return content
303 return content
305
304
306 def validate(self):
305 def validate(self):
307 if self._size != self._got:
306 if self._size != self._got:
308 raise error.Abort(
307 raise error.Abort(
309 _(b'size mismatch: expected %d, got %d')
308 _(b'size mismatch: expected %d, got %d')
310 % (self._size, self._got)
309 % (self._size, self._got)
311 )
310 )
312 for k, v in self._digests.items():
311 for k, v in self._digests.items():
313 if v != self._digester[k]:
312 if v != self._digester[k]:
314 # i18n: first parameter is a digest name
313 # i18n: first parameter is a digest name
315 raise error.Abort(
314 raise error.Abort(
316 _(b'%s mismatch: expected %s, got %s')
315 _(b'%s mismatch: expected %s, got %s')
317 % (k, v, self._digester[k])
316 % (k, v, self._digester[k])
318 )
317 )
319
318
320
319
321 try:
320 try:
322 buffer = buffer # pytype: disable=name-error
321 buffer = buffer # pytype: disable=name-error
323 except NameError:
322 except NameError:
324
323
325 def buffer(sliceable, offset=0, length=None):
324 def buffer(sliceable, offset=0, length=None):
326 if length is not None:
325 if length is not None:
327 return memoryview(sliceable)[offset : offset + length]
326 return memoryview(sliceable)[offset : offset + length]
328 return memoryview(sliceable)[offset:]
327 return memoryview(sliceable)[offset:]
329
328
330
329
331 _chunksize = 4096
330 _chunksize = 4096
332
331
333
332
334 class bufferedinputpipe(object):
333 class bufferedinputpipe(object):
335 """a manually buffered input pipe
334 """a manually buffered input pipe
336
335
337 Python will not let us use buffered IO and lazy reading with 'polling' at
336 Python will not let us use buffered IO and lazy reading with 'polling' at
338 the same time. We cannot probe the buffer state and select will not detect
337 the same time. We cannot probe the buffer state and select will not detect
339 that data are ready to read if they are already buffered.
338 that data are ready to read if they are already buffered.
340
339
341 This class let us work around that by implementing its own buffering
340 This class let us work around that by implementing its own buffering
342 (allowing efficient readline) while offering a way to know if the buffer is
341 (allowing efficient readline) while offering a way to know if the buffer is
343 empty from the output (allowing collaboration of the buffer with polling).
342 empty from the output (allowing collaboration of the buffer with polling).
344
343
345 This class lives in the 'util' module because it makes use of the 'os'
344 This class lives in the 'util' module because it makes use of the 'os'
346 module from the python stdlib.
345 module from the python stdlib.
347 """
346 """
348
347
349 def __new__(cls, fh):
348 def __new__(cls, fh):
350 # If we receive a fileobjectproxy, we need to use a variation of this
349 # If we receive a fileobjectproxy, we need to use a variation of this
351 # class that notifies observers about activity.
350 # class that notifies observers about activity.
352 if isinstance(fh, fileobjectproxy):
351 if isinstance(fh, fileobjectproxy):
353 cls = observedbufferedinputpipe
352 cls = observedbufferedinputpipe
354
353
355 return super(bufferedinputpipe, cls).__new__(cls)
354 return super(bufferedinputpipe, cls).__new__(cls)
356
355
357 def __init__(self, input):
356 def __init__(self, input):
358 self._input = input
357 self._input = input
359 self._buffer = []
358 self._buffer = []
360 self._eof = False
359 self._eof = False
361 self._lenbuf = 0
360 self._lenbuf = 0
362
361
363 @property
362 @property
364 def hasbuffer(self):
363 def hasbuffer(self):
365 """True is any data is currently buffered
364 """True is any data is currently buffered
366
365
367 This will be used externally a pre-step for polling IO. If there is
366 This will be used externally a pre-step for polling IO. If there is
368 already data then no polling should be set in place."""
367 already data then no polling should be set in place."""
369 return bool(self._buffer)
368 return bool(self._buffer)
370
369
371 @property
370 @property
372 def closed(self):
371 def closed(self):
373 return self._input.closed
372 return self._input.closed
374
373
375 def fileno(self):
374 def fileno(self):
376 return self._input.fileno()
375 return self._input.fileno()
377
376
378 def close(self):
377 def close(self):
379 return self._input.close()
378 return self._input.close()
380
379
381 def read(self, size):
380 def read(self, size):
382 while (not self._eof) and (self._lenbuf < size):
381 while (not self._eof) and (self._lenbuf < size):
383 self._fillbuffer()
382 self._fillbuffer()
384 return self._frombuffer(size)
383 return self._frombuffer(size)
385
384
386 def unbufferedread(self, size):
385 def unbufferedread(self, size):
387 if not self._eof and self._lenbuf == 0:
386 if not self._eof and self._lenbuf == 0:
388 self._fillbuffer(max(size, _chunksize))
387 self._fillbuffer(max(size, _chunksize))
389 return self._frombuffer(min(self._lenbuf, size))
388 return self._frombuffer(min(self._lenbuf, size))
390
389
391 def readline(self, *args, **kwargs):
390 def readline(self, *args, **kwargs):
392 if len(self._buffer) > 1:
391 if len(self._buffer) > 1:
393 # this should not happen because both read and readline end with a
392 # this should not happen because both read and readline end with a
394 # _frombuffer call that collapse it.
393 # _frombuffer call that collapse it.
395 self._buffer = [b''.join(self._buffer)]
394 self._buffer = [b''.join(self._buffer)]
396 self._lenbuf = len(self._buffer[0])
395 self._lenbuf = len(self._buffer[0])
397 lfi = -1
396 lfi = -1
398 if self._buffer:
397 if self._buffer:
399 lfi = self._buffer[-1].find(b'\n')
398 lfi = self._buffer[-1].find(b'\n')
400 while (not self._eof) and lfi < 0:
399 while (not self._eof) and lfi < 0:
401 self._fillbuffer()
400 self._fillbuffer()
402 if self._buffer:
401 if self._buffer:
403 lfi = self._buffer[-1].find(b'\n')
402 lfi = self._buffer[-1].find(b'\n')
404 size = lfi + 1
403 size = lfi + 1
405 if lfi < 0: # end of file
404 if lfi < 0: # end of file
406 size = self._lenbuf
405 size = self._lenbuf
407 elif len(self._buffer) > 1:
406 elif len(self._buffer) > 1:
408 # we need to take previous chunks into account
407 # we need to take previous chunks into account
409 size += self._lenbuf - len(self._buffer[-1])
408 size += self._lenbuf - len(self._buffer[-1])
410 return self._frombuffer(size)
409 return self._frombuffer(size)
411
410
412 def _frombuffer(self, size):
411 def _frombuffer(self, size):
413 """return at most 'size' data from the buffer
412 """return at most 'size' data from the buffer
414
413
415 The data are removed from the buffer."""
414 The data are removed from the buffer."""
416 if size == 0 or not self._buffer:
415 if size == 0 or not self._buffer:
417 return b''
416 return b''
418 buf = self._buffer[0]
417 buf = self._buffer[0]
419 if len(self._buffer) > 1:
418 if len(self._buffer) > 1:
420 buf = b''.join(self._buffer)
419 buf = b''.join(self._buffer)
421
420
422 data = buf[:size]
421 data = buf[:size]
423 buf = buf[len(data) :]
422 buf = buf[len(data) :]
424 if buf:
423 if buf:
425 self._buffer = [buf]
424 self._buffer = [buf]
426 self._lenbuf = len(buf)
425 self._lenbuf = len(buf)
427 else:
426 else:
428 self._buffer = []
427 self._buffer = []
429 self._lenbuf = 0
428 self._lenbuf = 0
430 return data
429 return data
431
430
432 def _fillbuffer(self, size=_chunksize):
431 def _fillbuffer(self, size=_chunksize):
433 """read data to the buffer"""
432 """read data to the buffer"""
434 data = os.read(self._input.fileno(), size)
433 data = os.read(self._input.fileno(), size)
435 if not data:
434 if not data:
436 self._eof = True
435 self._eof = True
437 else:
436 else:
438 self._lenbuf += len(data)
437 self._lenbuf += len(data)
439 self._buffer.append(data)
438 self._buffer.append(data)
440
439
441 return data
440 return data
442
441
443
442
444 def mmapread(fp, size=None):
443 def mmapread(fp, size=None):
445 if size == 0:
444 if size == 0:
446 # size of 0 to mmap.mmap() means "all data"
445 # size of 0 to mmap.mmap() means "all data"
447 # rather than "zero bytes", so special case that.
446 # rather than "zero bytes", so special case that.
448 return b''
447 return b''
449 elif size is None:
448 elif size is None:
450 size = 0
449 size = 0
451 fd = getattr(fp, 'fileno', lambda: fp)()
450 fd = getattr(fp, 'fileno', lambda: fp)()
452 try:
451 try:
453 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
452 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
454 except ValueError:
453 except ValueError:
455 # Empty files cannot be mmapped, but mmapread should still work. Check
454 # Empty files cannot be mmapped, but mmapread should still work. Check
456 # if the file is empty, and if so, return an empty buffer.
455 # if the file is empty, and if so, return an empty buffer.
457 if os.fstat(fd).st_size == 0:
456 if os.fstat(fd).st_size == 0:
458 return b''
457 return b''
459 raise
458 raise
460
459
461
460
462 class fileobjectproxy(object):
461 class fileobjectproxy(object):
463 """A proxy around file objects that tells a watcher when events occur.
462 """A proxy around file objects that tells a watcher when events occur.
464
463
465 This type is intended to only be used for testing purposes. Think hard
464 This type is intended to only be used for testing purposes. Think hard
466 before using it in important code.
465 before using it in important code.
467 """
466 """
468
467
469 __slots__ = (
468 __slots__ = (
470 '_orig',
469 '_orig',
471 '_observer',
470 '_observer',
472 )
471 )
473
472
474 def __init__(self, fh, observer):
473 def __init__(self, fh, observer):
475 object.__setattr__(self, '_orig', fh)
474 object.__setattr__(self, '_orig', fh)
476 object.__setattr__(self, '_observer', observer)
475 object.__setattr__(self, '_observer', observer)
477
476
478 def __getattribute__(self, name):
477 def __getattribute__(self, name):
479 ours = {
478 ours = {
480 '_observer',
479 '_observer',
481 # IOBase
480 # IOBase
482 'close',
481 'close',
483 # closed if a property
482 # closed if a property
484 'fileno',
483 'fileno',
485 'flush',
484 'flush',
486 'isatty',
485 'isatty',
487 'readable',
486 'readable',
488 'readline',
487 'readline',
489 'readlines',
488 'readlines',
490 'seek',
489 'seek',
491 'seekable',
490 'seekable',
492 'tell',
491 'tell',
493 'truncate',
492 'truncate',
494 'writable',
493 'writable',
495 'writelines',
494 'writelines',
496 # RawIOBase
495 # RawIOBase
497 'read',
496 'read',
498 'readall',
497 'readall',
499 'readinto',
498 'readinto',
500 'write',
499 'write',
501 # BufferedIOBase
500 # BufferedIOBase
502 # raw is a property
501 # raw is a property
503 'detach',
502 'detach',
504 # read defined above
503 # read defined above
505 'read1',
504 'read1',
506 # readinto defined above
505 # readinto defined above
507 # write defined above
506 # write defined above
508 }
507 }
509
508
510 # We only observe some methods.
509 # We only observe some methods.
511 if name in ours:
510 if name in ours:
512 return object.__getattribute__(self, name)
511 return object.__getattribute__(self, name)
513
512
514 return getattr(object.__getattribute__(self, '_orig'), name)
513 return getattr(object.__getattribute__(self, '_orig'), name)
515
514
516 def __nonzero__(self):
515 def __nonzero__(self):
517 return bool(object.__getattribute__(self, '_orig'))
516 return bool(object.__getattribute__(self, '_orig'))
518
517
519 __bool__ = __nonzero__
518 __bool__ = __nonzero__
520
519
521 def __delattr__(self, name):
520 def __delattr__(self, name):
522 return delattr(object.__getattribute__(self, '_orig'), name)
521 return delattr(object.__getattribute__(self, '_orig'), name)
523
522
524 def __setattr__(self, name, value):
523 def __setattr__(self, name, value):
525 return setattr(object.__getattribute__(self, '_orig'), name, value)
524 return setattr(object.__getattribute__(self, '_orig'), name, value)
526
525
527 def __iter__(self):
526 def __iter__(self):
528 return object.__getattribute__(self, '_orig').__iter__()
527 return object.__getattribute__(self, '_orig').__iter__()
529
528
530 def _observedcall(self, name, *args, **kwargs):
529 def _observedcall(self, name, *args, **kwargs):
531 # Call the original object.
530 # Call the original object.
532 orig = object.__getattribute__(self, '_orig')
531 orig = object.__getattribute__(self, '_orig')
533 res = getattr(orig, name)(*args, **kwargs)
532 res = getattr(orig, name)(*args, **kwargs)
534
533
535 # Call a method on the observer of the same name with arguments
534 # Call a method on the observer of the same name with arguments
536 # so it can react, log, etc.
535 # so it can react, log, etc.
537 observer = object.__getattribute__(self, '_observer')
536 observer = object.__getattribute__(self, '_observer')
538 fn = getattr(observer, name, None)
537 fn = getattr(observer, name, None)
539 if fn:
538 if fn:
540 fn(res, *args, **kwargs)
539 fn(res, *args, **kwargs)
541
540
542 return res
541 return res
543
542
544 def close(self, *args, **kwargs):
543 def close(self, *args, **kwargs):
545 return object.__getattribute__(self, '_observedcall')(
544 return object.__getattribute__(self, '_observedcall')(
546 'close', *args, **kwargs
545 'close', *args, **kwargs
547 )
546 )
548
547
549 def fileno(self, *args, **kwargs):
548 def fileno(self, *args, **kwargs):
550 return object.__getattribute__(self, '_observedcall')(
549 return object.__getattribute__(self, '_observedcall')(
551 'fileno', *args, **kwargs
550 'fileno', *args, **kwargs
552 )
551 )
553
552
554 def flush(self, *args, **kwargs):
553 def flush(self, *args, **kwargs):
555 return object.__getattribute__(self, '_observedcall')(
554 return object.__getattribute__(self, '_observedcall')(
556 'flush', *args, **kwargs
555 'flush', *args, **kwargs
557 )
556 )
558
557
559 def isatty(self, *args, **kwargs):
558 def isatty(self, *args, **kwargs):
560 return object.__getattribute__(self, '_observedcall')(
559 return object.__getattribute__(self, '_observedcall')(
561 'isatty', *args, **kwargs
560 'isatty', *args, **kwargs
562 )
561 )
563
562
564 def readable(self, *args, **kwargs):
563 def readable(self, *args, **kwargs):
565 return object.__getattribute__(self, '_observedcall')(
564 return object.__getattribute__(self, '_observedcall')(
566 'readable', *args, **kwargs
565 'readable', *args, **kwargs
567 )
566 )
568
567
569 def readline(self, *args, **kwargs):
568 def readline(self, *args, **kwargs):
570 return object.__getattribute__(self, '_observedcall')(
569 return object.__getattribute__(self, '_observedcall')(
571 'readline', *args, **kwargs
570 'readline', *args, **kwargs
572 )
571 )
573
572
574 def readlines(self, *args, **kwargs):
573 def readlines(self, *args, **kwargs):
575 return object.__getattribute__(self, '_observedcall')(
574 return object.__getattribute__(self, '_observedcall')(
576 'readlines', *args, **kwargs
575 'readlines', *args, **kwargs
577 )
576 )
578
577
579 def seek(self, *args, **kwargs):
578 def seek(self, *args, **kwargs):
580 return object.__getattribute__(self, '_observedcall')(
579 return object.__getattribute__(self, '_observedcall')(
581 'seek', *args, **kwargs
580 'seek', *args, **kwargs
582 )
581 )
583
582
584 def seekable(self, *args, **kwargs):
583 def seekable(self, *args, **kwargs):
585 return object.__getattribute__(self, '_observedcall')(
584 return object.__getattribute__(self, '_observedcall')(
586 'seekable', *args, **kwargs
585 'seekable', *args, **kwargs
587 )
586 )
588
587
589 def tell(self, *args, **kwargs):
588 def tell(self, *args, **kwargs):
590 return object.__getattribute__(self, '_observedcall')(
589 return object.__getattribute__(self, '_observedcall')(
591 'tell', *args, **kwargs
590 'tell', *args, **kwargs
592 )
591 )
593
592
594 def truncate(self, *args, **kwargs):
593 def truncate(self, *args, **kwargs):
595 return object.__getattribute__(self, '_observedcall')(
594 return object.__getattribute__(self, '_observedcall')(
596 'truncate', *args, **kwargs
595 'truncate', *args, **kwargs
597 )
596 )
598
597
599 def writable(self, *args, **kwargs):
598 def writable(self, *args, **kwargs):
600 return object.__getattribute__(self, '_observedcall')(
599 return object.__getattribute__(self, '_observedcall')(
601 'writable', *args, **kwargs
600 'writable', *args, **kwargs
602 )
601 )
603
602
604 def writelines(self, *args, **kwargs):
603 def writelines(self, *args, **kwargs):
605 return object.__getattribute__(self, '_observedcall')(
604 return object.__getattribute__(self, '_observedcall')(
606 'writelines', *args, **kwargs
605 'writelines', *args, **kwargs
607 )
606 )
608
607
609 def read(self, *args, **kwargs):
608 def read(self, *args, **kwargs):
610 return object.__getattribute__(self, '_observedcall')(
609 return object.__getattribute__(self, '_observedcall')(
611 'read', *args, **kwargs
610 'read', *args, **kwargs
612 )
611 )
613
612
614 def readall(self, *args, **kwargs):
613 def readall(self, *args, **kwargs):
615 return object.__getattribute__(self, '_observedcall')(
614 return object.__getattribute__(self, '_observedcall')(
616 'readall', *args, **kwargs
615 'readall', *args, **kwargs
617 )
616 )
618
617
619 def readinto(self, *args, **kwargs):
618 def readinto(self, *args, **kwargs):
620 return object.__getattribute__(self, '_observedcall')(
619 return object.__getattribute__(self, '_observedcall')(
621 'readinto', *args, **kwargs
620 'readinto', *args, **kwargs
622 )
621 )
623
622
624 def write(self, *args, **kwargs):
623 def write(self, *args, **kwargs):
625 return object.__getattribute__(self, '_observedcall')(
624 return object.__getattribute__(self, '_observedcall')(
626 'write', *args, **kwargs
625 'write', *args, **kwargs
627 )
626 )
628
627
629 def detach(self, *args, **kwargs):
628 def detach(self, *args, **kwargs):
630 return object.__getattribute__(self, '_observedcall')(
629 return object.__getattribute__(self, '_observedcall')(
631 'detach', *args, **kwargs
630 'detach', *args, **kwargs
632 )
631 )
633
632
634 def read1(self, *args, **kwargs):
633 def read1(self, *args, **kwargs):
635 return object.__getattribute__(self, '_observedcall')(
634 return object.__getattribute__(self, '_observedcall')(
636 'read1', *args, **kwargs
635 'read1', *args, **kwargs
637 )
636 )
638
637
639
638
640 class observedbufferedinputpipe(bufferedinputpipe):
639 class observedbufferedinputpipe(bufferedinputpipe):
641 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
640 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
642
641
643 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
642 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
644 bypass ``fileobjectproxy``. Because of this, we need to make
643 bypass ``fileobjectproxy``. Because of this, we need to make
645 ``bufferedinputpipe`` aware of these operations.
644 ``bufferedinputpipe`` aware of these operations.
646
645
647 This variation of ``bufferedinputpipe`` can notify observers about
646 This variation of ``bufferedinputpipe`` can notify observers about
648 ``os.read()`` events. It also re-publishes other events, such as
647 ``os.read()`` events. It also re-publishes other events, such as
649 ``read()`` and ``readline()``.
648 ``read()`` and ``readline()``.
650 """
649 """
651
650
652 def _fillbuffer(self):
651 def _fillbuffer(self):
653 res = super(observedbufferedinputpipe, self)._fillbuffer()
652 res = super(observedbufferedinputpipe, self)._fillbuffer()
654
653
655 fn = getattr(self._input._observer, 'osread', None)
654 fn = getattr(self._input._observer, 'osread', None)
656 if fn:
655 if fn:
657 fn(res, _chunksize)
656 fn(res, _chunksize)
658
657
659 return res
658 return res
660
659
661 # We use different observer methods because the operation isn't
660 # We use different observer methods because the operation isn't
662 # performed on the actual file object but on us.
661 # performed on the actual file object but on us.
663 def read(self, size):
662 def read(self, size):
664 res = super(observedbufferedinputpipe, self).read(size)
663 res = super(observedbufferedinputpipe, self).read(size)
665
664
666 fn = getattr(self._input._observer, 'bufferedread', None)
665 fn = getattr(self._input._observer, 'bufferedread', None)
667 if fn:
666 if fn:
668 fn(res, size)
667 fn(res, size)
669
668
670 return res
669 return res
671
670
672 def readline(self, *args, **kwargs):
671 def readline(self, *args, **kwargs):
673 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
672 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
674
673
675 fn = getattr(self._input._observer, 'bufferedreadline', None)
674 fn = getattr(self._input._observer, 'bufferedreadline', None)
676 if fn:
675 if fn:
677 fn(res)
676 fn(res)
678
677
679 return res
678 return res
680
679
681
680
682 PROXIED_SOCKET_METHODS = {
681 PROXIED_SOCKET_METHODS = {
683 'makefile',
682 'makefile',
684 'recv',
683 'recv',
685 'recvfrom',
684 'recvfrom',
686 'recvfrom_into',
685 'recvfrom_into',
687 'recv_into',
686 'recv_into',
688 'send',
687 'send',
689 'sendall',
688 'sendall',
690 'sendto',
689 'sendto',
691 'setblocking',
690 'setblocking',
692 'settimeout',
691 'settimeout',
693 'gettimeout',
692 'gettimeout',
694 'setsockopt',
693 'setsockopt',
695 }
694 }
696
695
697
696
698 class socketproxy(object):
697 class socketproxy(object):
699 """A proxy around a socket that tells a watcher when events occur.
698 """A proxy around a socket that tells a watcher when events occur.
700
699
701 This is like ``fileobjectproxy`` except for sockets.
700 This is like ``fileobjectproxy`` except for sockets.
702
701
703 This type is intended to only be used for testing purposes. Think hard
702 This type is intended to only be used for testing purposes. Think hard
704 before using it in important code.
703 before using it in important code.
705 """
704 """
706
705
707 __slots__ = (
706 __slots__ = (
708 '_orig',
707 '_orig',
709 '_observer',
708 '_observer',
710 )
709 )
711
710
712 def __init__(self, sock, observer):
711 def __init__(self, sock, observer):
713 object.__setattr__(self, '_orig', sock)
712 object.__setattr__(self, '_orig', sock)
714 object.__setattr__(self, '_observer', observer)
713 object.__setattr__(self, '_observer', observer)
715
714
716 def __getattribute__(self, name):
715 def __getattribute__(self, name):
717 if name in PROXIED_SOCKET_METHODS:
716 if name in PROXIED_SOCKET_METHODS:
718 return object.__getattribute__(self, name)
717 return object.__getattribute__(self, name)
719
718
720 return getattr(object.__getattribute__(self, '_orig'), name)
719 return getattr(object.__getattribute__(self, '_orig'), name)
721
720
722 def __delattr__(self, name):
721 def __delattr__(self, name):
723 return delattr(object.__getattribute__(self, '_orig'), name)
722 return delattr(object.__getattribute__(self, '_orig'), name)
724
723
725 def __setattr__(self, name, value):
724 def __setattr__(self, name, value):
726 return setattr(object.__getattribute__(self, '_orig'), name, value)
725 return setattr(object.__getattribute__(self, '_orig'), name, value)
727
726
728 def __nonzero__(self):
727 def __nonzero__(self):
729 return bool(object.__getattribute__(self, '_orig'))
728 return bool(object.__getattribute__(self, '_orig'))
730
729
731 __bool__ = __nonzero__
730 __bool__ = __nonzero__
732
731
733 def _observedcall(self, name, *args, **kwargs):
732 def _observedcall(self, name, *args, **kwargs):
734 # Call the original object.
733 # Call the original object.
735 orig = object.__getattribute__(self, '_orig')
734 orig = object.__getattribute__(self, '_orig')
736 res = getattr(orig, name)(*args, **kwargs)
735 res = getattr(orig, name)(*args, **kwargs)
737
736
738 # Call a method on the observer of the same name with arguments
737 # Call a method on the observer of the same name with arguments
739 # so it can react, log, etc.
738 # so it can react, log, etc.
740 observer = object.__getattribute__(self, '_observer')
739 observer = object.__getattribute__(self, '_observer')
741 fn = getattr(observer, name, None)
740 fn = getattr(observer, name, None)
742 if fn:
741 if fn:
743 fn(res, *args, **kwargs)
742 fn(res, *args, **kwargs)
744
743
745 return res
744 return res
746
745
747 def makefile(self, *args, **kwargs):
746 def makefile(self, *args, **kwargs):
748 res = object.__getattribute__(self, '_observedcall')(
747 res = object.__getattribute__(self, '_observedcall')(
749 'makefile', *args, **kwargs
748 'makefile', *args, **kwargs
750 )
749 )
751
750
752 # The file object may be used for I/O. So we turn it into a
751 # The file object may be used for I/O. So we turn it into a
753 # proxy using our observer.
752 # proxy using our observer.
754 observer = object.__getattribute__(self, '_observer')
753 observer = object.__getattribute__(self, '_observer')
755 return makeloggingfileobject(
754 return makeloggingfileobject(
756 observer.fh,
755 observer.fh,
757 res,
756 res,
758 observer.name,
757 observer.name,
759 reads=observer.reads,
758 reads=observer.reads,
760 writes=observer.writes,
759 writes=observer.writes,
761 logdata=observer.logdata,
760 logdata=observer.logdata,
762 logdataapis=observer.logdataapis,
761 logdataapis=observer.logdataapis,
763 )
762 )
764
763
765 def recv(self, *args, **kwargs):
764 def recv(self, *args, **kwargs):
766 return object.__getattribute__(self, '_observedcall')(
765 return object.__getattribute__(self, '_observedcall')(
767 'recv', *args, **kwargs
766 'recv', *args, **kwargs
768 )
767 )
769
768
770 def recvfrom(self, *args, **kwargs):
769 def recvfrom(self, *args, **kwargs):
771 return object.__getattribute__(self, '_observedcall')(
770 return object.__getattribute__(self, '_observedcall')(
772 'recvfrom', *args, **kwargs
771 'recvfrom', *args, **kwargs
773 )
772 )
774
773
775 def recvfrom_into(self, *args, **kwargs):
774 def recvfrom_into(self, *args, **kwargs):
776 return object.__getattribute__(self, '_observedcall')(
775 return object.__getattribute__(self, '_observedcall')(
777 'recvfrom_into', *args, **kwargs
776 'recvfrom_into', *args, **kwargs
778 )
777 )
779
778
780 def recv_into(self, *args, **kwargs):
779 def recv_into(self, *args, **kwargs):
781 return object.__getattribute__(self, '_observedcall')(
780 return object.__getattribute__(self, '_observedcall')(
782 'recv_info', *args, **kwargs
781 'recv_info', *args, **kwargs
783 )
782 )
784
783
785 def send(self, *args, **kwargs):
784 def send(self, *args, **kwargs):
786 return object.__getattribute__(self, '_observedcall')(
785 return object.__getattribute__(self, '_observedcall')(
787 'send', *args, **kwargs
786 'send', *args, **kwargs
788 )
787 )
789
788
790 def sendall(self, *args, **kwargs):
789 def sendall(self, *args, **kwargs):
791 return object.__getattribute__(self, '_observedcall')(
790 return object.__getattribute__(self, '_observedcall')(
792 'sendall', *args, **kwargs
791 'sendall', *args, **kwargs
793 )
792 )
794
793
795 def sendto(self, *args, **kwargs):
794 def sendto(self, *args, **kwargs):
796 return object.__getattribute__(self, '_observedcall')(
795 return object.__getattribute__(self, '_observedcall')(
797 'sendto', *args, **kwargs
796 'sendto', *args, **kwargs
798 )
797 )
799
798
800 def setblocking(self, *args, **kwargs):
799 def setblocking(self, *args, **kwargs):
801 return object.__getattribute__(self, '_observedcall')(
800 return object.__getattribute__(self, '_observedcall')(
802 'setblocking', *args, **kwargs
801 'setblocking', *args, **kwargs
803 )
802 )
804
803
805 def settimeout(self, *args, **kwargs):
804 def settimeout(self, *args, **kwargs):
806 return object.__getattribute__(self, '_observedcall')(
805 return object.__getattribute__(self, '_observedcall')(
807 'settimeout', *args, **kwargs
806 'settimeout', *args, **kwargs
808 )
807 )
809
808
810 def gettimeout(self, *args, **kwargs):
809 def gettimeout(self, *args, **kwargs):
811 return object.__getattribute__(self, '_observedcall')(
810 return object.__getattribute__(self, '_observedcall')(
812 'gettimeout', *args, **kwargs
811 'gettimeout', *args, **kwargs
813 )
812 )
814
813
815 def setsockopt(self, *args, **kwargs):
814 def setsockopt(self, *args, **kwargs):
816 return object.__getattribute__(self, '_observedcall')(
815 return object.__getattribute__(self, '_observedcall')(
817 'setsockopt', *args, **kwargs
816 'setsockopt', *args, **kwargs
818 )
817 )
819
818
820
819
821 class baseproxyobserver(object):
820 class baseproxyobserver(object):
822 def __init__(self, fh, name, logdata, logdataapis):
821 def __init__(self, fh, name, logdata, logdataapis):
823 self.fh = fh
822 self.fh = fh
824 self.name = name
823 self.name = name
825 self.logdata = logdata
824 self.logdata = logdata
826 self.logdataapis = logdataapis
825 self.logdataapis = logdataapis
827
826
828 def _writedata(self, data):
827 def _writedata(self, data):
829 if not self.logdata:
828 if not self.logdata:
830 if self.logdataapis:
829 if self.logdataapis:
831 self.fh.write(b'\n')
830 self.fh.write(b'\n')
832 self.fh.flush()
831 self.fh.flush()
833 return
832 return
834
833
835 # Simple case writes all data on a single line.
834 # Simple case writes all data on a single line.
836 if b'\n' not in data:
835 if b'\n' not in data:
837 if self.logdataapis:
836 if self.logdataapis:
838 self.fh.write(b': %s\n' % stringutil.escapestr(data))
837 self.fh.write(b': %s\n' % stringutil.escapestr(data))
839 else:
838 else:
840 self.fh.write(
839 self.fh.write(
841 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
840 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
842 )
841 )
843 self.fh.flush()
842 self.fh.flush()
844 return
843 return
845
844
846 # Data with newlines is written to multiple lines.
845 # Data with newlines is written to multiple lines.
847 if self.logdataapis:
846 if self.logdataapis:
848 self.fh.write(b':\n')
847 self.fh.write(b':\n')
849
848
850 lines = data.splitlines(True)
849 lines = data.splitlines(True)
851 for line in lines:
850 for line in lines:
852 self.fh.write(
851 self.fh.write(
853 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
852 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
854 )
853 )
855 self.fh.flush()
854 self.fh.flush()
856
855
857
856
858 class fileobjectobserver(baseproxyobserver):
857 class fileobjectobserver(baseproxyobserver):
859 """Logs file object activity."""
858 """Logs file object activity."""
860
859
861 def __init__(
860 def __init__(
862 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
861 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
863 ):
862 ):
864 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
863 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
865 self.reads = reads
864 self.reads = reads
866 self.writes = writes
865 self.writes = writes
867
866
868 def read(self, res, size=-1):
867 def read(self, res, size=-1):
869 if not self.reads:
868 if not self.reads:
870 return
869 return
871 # Python 3 can return None from reads at EOF instead of empty strings.
870 # Python 3 can return None from reads at EOF instead of empty strings.
872 if res is None:
871 if res is None:
873 res = b''
872 res = b''
874
873
875 if size == -1 and res == b'':
874 if size == -1 and res == b'':
876 # Suppress pointless read(-1) calls that return
875 # Suppress pointless read(-1) calls that return
877 # nothing. These happen _a lot_ on Python 3, and there
876 # nothing. These happen _a lot_ on Python 3, and there
878 # doesn't seem to be a better workaround to have matching
877 # doesn't seem to be a better workaround to have matching
879 # Python 2 and 3 behavior. :(
878 # Python 2 and 3 behavior. :(
880 return
879 return
881
880
882 if self.logdataapis:
881 if self.logdataapis:
883 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
882 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
884
883
885 self._writedata(res)
884 self._writedata(res)
886
885
887 def readline(self, res, limit=-1):
886 def readline(self, res, limit=-1):
888 if not self.reads:
887 if not self.reads:
889 return
888 return
890
889
891 if self.logdataapis:
890 if self.logdataapis:
892 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
891 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
893
892
894 self._writedata(res)
893 self._writedata(res)
895
894
896 def readinto(self, res, dest):
895 def readinto(self, res, dest):
897 if not self.reads:
896 if not self.reads:
898 return
897 return
899
898
900 if self.logdataapis:
899 if self.logdataapis:
901 self.fh.write(
900 self.fh.write(
902 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
901 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
903 )
902 )
904
903
905 data = dest[0:res] if res is not None else b''
904 data = dest[0:res] if res is not None else b''
906
905
907 # _writedata() uses "in" operator and is confused by memoryview because
906 # _writedata() uses "in" operator and is confused by memoryview because
908 # characters are ints on Python 3.
907 # characters are ints on Python 3.
909 if isinstance(data, memoryview):
908 if isinstance(data, memoryview):
910 data = data.tobytes()
909 data = data.tobytes()
911
910
912 self._writedata(data)
911 self._writedata(data)
913
912
914 def write(self, res, data):
913 def write(self, res, data):
915 if not self.writes:
914 if not self.writes:
916 return
915 return
917
916
918 # Python 2 returns None from some write() calls. Python 3 (reasonably)
917 # Python 2 returns None from some write() calls. Python 3 (reasonably)
919 # returns the integer bytes written.
918 # returns the integer bytes written.
920 if res is None and data:
919 if res is None and data:
921 res = len(data)
920 res = len(data)
922
921
923 if self.logdataapis:
922 if self.logdataapis:
924 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
923 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
925
924
926 self._writedata(data)
925 self._writedata(data)
927
926
928 def flush(self, res):
927 def flush(self, res):
929 if not self.writes:
928 if not self.writes:
930 return
929 return
931
930
932 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
931 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
933
932
934 # For observedbufferedinputpipe.
933 # For observedbufferedinputpipe.
935 def bufferedread(self, res, size):
934 def bufferedread(self, res, size):
936 if not self.reads:
935 if not self.reads:
937 return
936 return
938
937
939 if self.logdataapis:
938 if self.logdataapis:
940 self.fh.write(
939 self.fh.write(
941 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
940 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
942 )
941 )
943
942
944 self._writedata(res)
943 self._writedata(res)
945
944
946 def bufferedreadline(self, res):
945 def bufferedreadline(self, res):
947 if not self.reads:
946 if not self.reads:
948 return
947 return
949
948
950 if self.logdataapis:
949 if self.logdataapis:
951 self.fh.write(
950 self.fh.write(
952 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
951 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
953 )
952 )
954
953
955 self._writedata(res)
954 self._writedata(res)
956
955
957
956
958 def makeloggingfileobject(
957 def makeloggingfileobject(
959 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
958 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
960 ):
959 ):
961 """Turn a file object into a logging file object."""
960 """Turn a file object into a logging file object."""
962
961
963 observer = fileobjectobserver(
962 observer = fileobjectobserver(
964 logh,
963 logh,
965 name,
964 name,
966 reads=reads,
965 reads=reads,
967 writes=writes,
966 writes=writes,
968 logdata=logdata,
967 logdata=logdata,
969 logdataapis=logdataapis,
968 logdataapis=logdataapis,
970 )
969 )
971 return fileobjectproxy(fh, observer)
970 return fileobjectproxy(fh, observer)
972
971
973
972
974 class socketobserver(baseproxyobserver):
973 class socketobserver(baseproxyobserver):
975 """Logs socket activity."""
974 """Logs socket activity."""
976
975
977 def __init__(
976 def __init__(
978 self,
977 self,
979 fh,
978 fh,
980 name,
979 name,
981 reads=True,
980 reads=True,
982 writes=True,
981 writes=True,
983 states=True,
982 states=True,
984 logdata=False,
983 logdata=False,
985 logdataapis=True,
984 logdataapis=True,
986 ):
985 ):
987 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
986 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
988 self.reads = reads
987 self.reads = reads
989 self.writes = writes
988 self.writes = writes
990 self.states = states
989 self.states = states
991
990
992 def makefile(self, res, mode=None, bufsize=None):
991 def makefile(self, res, mode=None, bufsize=None):
993 if not self.states:
992 if not self.states:
994 return
993 return
995
994
996 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
995 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
997
996
998 def recv(self, res, size, flags=0):
997 def recv(self, res, size, flags=0):
999 if not self.reads:
998 if not self.reads:
1000 return
999 return
1001
1000
1002 if self.logdataapis:
1001 if self.logdataapis:
1003 self.fh.write(
1002 self.fh.write(
1004 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
1003 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
1005 )
1004 )
1006 self._writedata(res)
1005 self._writedata(res)
1007
1006
1008 def recvfrom(self, res, size, flags=0):
1007 def recvfrom(self, res, size, flags=0):
1009 if not self.reads:
1008 if not self.reads:
1010 return
1009 return
1011
1010
1012 if self.logdataapis:
1011 if self.logdataapis:
1013 self.fh.write(
1012 self.fh.write(
1014 b'%s> recvfrom(%d, %d) -> %d'
1013 b'%s> recvfrom(%d, %d) -> %d'
1015 % (self.name, size, flags, len(res[0]))
1014 % (self.name, size, flags, len(res[0]))
1016 )
1015 )
1017
1016
1018 self._writedata(res[0])
1017 self._writedata(res[0])
1019
1018
1020 def recvfrom_into(self, res, buf, size, flags=0):
1019 def recvfrom_into(self, res, buf, size, flags=0):
1021 if not self.reads:
1020 if not self.reads:
1022 return
1021 return
1023
1022
1024 if self.logdataapis:
1023 if self.logdataapis:
1025 self.fh.write(
1024 self.fh.write(
1026 b'%s> recvfrom_into(%d, %d) -> %d'
1025 b'%s> recvfrom_into(%d, %d) -> %d'
1027 % (self.name, size, flags, res[0])
1026 % (self.name, size, flags, res[0])
1028 )
1027 )
1029
1028
1030 self._writedata(buf[0 : res[0]])
1029 self._writedata(buf[0 : res[0]])
1031
1030
1032 def recv_into(self, res, buf, size=0, flags=0):
1031 def recv_into(self, res, buf, size=0, flags=0):
1033 if not self.reads:
1032 if not self.reads:
1034 return
1033 return
1035
1034
1036 if self.logdataapis:
1035 if self.logdataapis:
1037 self.fh.write(
1036 self.fh.write(
1038 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1037 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1039 )
1038 )
1040
1039
1041 self._writedata(buf[0:res])
1040 self._writedata(buf[0:res])
1042
1041
1043 def send(self, res, data, flags=0):
1042 def send(self, res, data, flags=0):
1044 if not self.writes:
1043 if not self.writes:
1045 return
1044 return
1046
1045
1047 self.fh.write(
1046 self.fh.write(
1048 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1047 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1049 )
1048 )
1050 self._writedata(data)
1049 self._writedata(data)
1051
1050
1052 def sendall(self, res, data, flags=0):
1051 def sendall(self, res, data, flags=0):
1053 if not self.writes:
1052 if not self.writes:
1054 return
1053 return
1055
1054
1056 if self.logdataapis:
1055 if self.logdataapis:
1057 # Returns None on success. So don't bother reporting return value.
1056 # Returns None on success. So don't bother reporting return value.
1058 self.fh.write(
1057 self.fh.write(
1059 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1058 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1060 )
1059 )
1061
1060
1062 self._writedata(data)
1061 self._writedata(data)
1063
1062
1064 def sendto(self, res, data, flagsoraddress, address=None):
1063 def sendto(self, res, data, flagsoraddress, address=None):
1065 if not self.writes:
1064 if not self.writes:
1066 return
1065 return
1067
1066
1068 if address:
1067 if address:
1069 flags = flagsoraddress
1068 flags = flagsoraddress
1070 else:
1069 else:
1071 flags = 0
1070 flags = 0
1072
1071
1073 if self.logdataapis:
1072 if self.logdataapis:
1074 self.fh.write(
1073 self.fh.write(
1075 b'%s> sendto(%d, %d, %r) -> %d'
1074 b'%s> sendto(%d, %d, %r) -> %d'
1076 % (self.name, len(data), flags, address, res)
1075 % (self.name, len(data), flags, address, res)
1077 )
1076 )
1078
1077
1079 self._writedata(data)
1078 self._writedata(data)
1080
1079
1081 def setblocking(self, res, flag):
1080 def setblocking(self, res, flag):
1082 if not self.states:
1081 if not self.states:
1083 return
1082 return
1084
1083
1085 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1084 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1086
1085
1087 def settimeout(self, res, value):
1086 def settimeout(self, res, value):
1088 if not self.states:
1087 if not self.states:
1089 return
1088 return
1090
1089
1091 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1090 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1092
1091
1093 def gettimeout(self, res):
1092 def gettimeout(self, res):
1094 if not self.states:
1093 if not self.states:
1095 return
1094 return
1096
1095
1097 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1096 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1098
1097
1099 def setsockopt(self, res, level, optname, value):
1098 def setsockopt(self, res, level, optname, value):
1100 if not self.states:
1099 if not self.states:
1101 return
1100 return
1102
1101
1103 self.fh.write(
1102 self.fh.write(
1104 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1103 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1105 % (self.name, level, optname, value, res)
1104 % (self.name, level, optname, value, res)
1106 )
1105 )
1107
1106
1108
1107
1109 def makeloggingsocket(
1108 def makeloggingsocket(
1110 logh,
1109 logh,
1111 fh,
1110 fh,
1112 name,
1111 name,
1113 reads=True,
1112 reads=True,
1114 writes=True,
1113 writes=True,
1115 states=True,
1114 states=True,
1116 logdata=False,
1115 logdata=False,
1117 logdataapis=True,
1116 logdataapis=True,
1118 ):
1117 ):
1119 """Turn a socket into a logging socket."""
1118 """Turn a socket into a logging socket."""
1120
1119
1121 observer = socketobserver(
1120 observer = socketobserver(
1122 logh,
1121 logh,
1123 name,
1122 name,
1124 reads=reads,
1123 reads=reads,
1125 writes=writes,
1124 writes=writes,
1126 states=states,
1125 states=states,
1127 logdata=logdata,
1126 logdata=logdata,
1128 logdataapis=logdataapis,
1127 logdataapis=logdataapis,
1129 )
1128 )
1130 return socketproxy(fh, observer)
1129 return socketproxy(fh, observer)
1131
1130
1132
1131
1133 def version():
1132 def version():
1134 """Return version information if available."""
1133 """Return version information if available."""
1135 try:
1134 try:
1136 from . import __version__
1135 from . import __version__
1137
1136
1138 return __version__.version
1137 return __version__.version
1139 except ImportError:
1138 except ImportError:
1140 return b'unknown'
1139 return b'unknown'
1141
1140
1142
1141
1143 def versiontuple(v=None, n=4):
1142 def versiontuple(v=None, n=4):
1144 """Parses a Mercurial version string into an N-tuple.
1143 """Parses a Mercurial version string into an N-tuple.
1145
1144
1146 The version string to be parsed is specified with the ``v`` argument.
1145 The version string to be parsed is specified with the ``v`` argument.
1147 If it isn't defined, the current Mercurial version string will be parsed.
1146 If it isn't defined, the current Mercurial version string will be parsed.
1148
1147
1149 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1148 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1150 returned values:
1149 returned values:
1151
1150
1152 >>> v = b'3.6.1+190-df9b73d2d444'
1151 >>> v = b'3.6.1+190-df9b73d2d444'
1153 >>> versiontuple(v, 2)
1152 >>> versiontuple(v, 2)
1154 (3, 6)
1153 (3, 6)
1155 >>> versiontuple(v, 3)
1154 >>> versiontuple(v, 3)
1156 (3, 6, 1)
1155 (3, 6, 1)
1157 >>> versiontuple(v, 4)
1156 >>> versiontuple(v, 4)
1158 (3, 6, 1, '190-df9b73d2d444')
1157 (3, 6, 1, '190-df9b73d2d444')
1159
1158
1160 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1159 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1161 (3, 6, 1, '190-df9b73d2d444+20151118')
1160 (3, 6, 1, '190-df9b73d2d444+20151118')
1162
1161
1163 >>> v = b'3.6'
1162 >>> v = b'3.6'
1164 >>> versiontuple(v, 2)
1163 >>> versiontuple(v, 2)
1165 (3, 6)
1164 (3, 6)
1166 >>> versiontuple(v, 3)
1165 >>> versiontuple(v, 3)
1167 (3, 6, None)
1166 (3, 6, None)
1168 >>> versiontuple(v, 4)
1167 >>> versiontuple(v, 4)
1169 (3, 6, None, None)
1168 (3, 6, None, None)
1170
1169
1171 >>> v = b'3.9-rc'
1170 >>> v = b'3.9-rc'
1172 >>> versiontuple(v, 2)
1171 >>> versiontuple(v, 2)
1173 (3, 9)
1172 (3, 9)
1174 >>> versiontuple(v, 3)
1173 >>> versiontuple(v, 3)
1175 (3, 9, None)
1174 (3, 9, None)
1176 >>> versiontuple(v, 4)
1175 >>> versiontuple(v, 4)
1177 (3, 9, None, 'rc')
1176 (3, 9, None, 'rc')
1178
1177
1179 >>> v = b'3.9-rc+2-02a8fea4289b'
1178 >>> v = b'3.9-rc+2-02a8fea4289b'
1180 >>> versiontuple(v, 2)
1179 >>> versiontuple(v, 2)
1181 (3, 9)
1180 (3, 9)
1182 >>> versiontuple(v, 3)
1181 >>> versiontuple(v, 3)
1183 (3, 9, None)
1182 (3, 9, None)
1184 >>> versiontuple(v, 4)
1183 >>> versiontuple(v, 4)
1185 (3, 9, None, 'rc+2-02a8fea4289b')
1184 (3, 9, None, 'rc+2-02a8fea4289b')
1186
1185
1187 >>> versiontuple(b'4.6rc0')
1186 >>> versiontuple(b'4.6rc0')
1188 (4, 6, None, 'rc0')
1187 (4, 6, None, 'rc0')
1189 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1188 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1190 (4, 6, None, 'rc0+12-425d55e54f98')
1189 (4, 6, None, 'rc0+12-425d55e54f98')
1191 >>> versiontuple(b'.1.2.3')
1190 >>> versiontuple(b'.1.2.3')
1192 (None, None, None, '.1.2.3')
1191 (None, None, None, '.1.2.3')
1193 >>> versiontuple(b'12.34..5')
1192 >>> versiontuple(b'12.34..5')
1194 (12, 34, None, '..5')
1193 (12, 34, None, '..5')
1195 >>> versiontuple(b'1.2.3.4.5.6')
1194 >>> versiontuple(b'1.2.3.4.5.6')
1196 (1, 2, 3, '.4.5.6')
1195 (1, 2, 3, '.4.5.6')
1197 """
1196 """
1198 if not v:
1197 if not v:
1199 v = version()
1198 v = version()
1200 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1199 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1201 if not m:
1200 if not m:
1202 vparts, extra = b'', v
1201 vparts, extra = b'', v
1203 elif m.group(2):
1202 elif m.group(2):
1204 vparts, extra = m.groups()
1203 vparts, extra = m.groups()
1205 else:
1204 else:
1206 vparts, extra = m.group(1), None
1205 vparts, extra = m.group(1), None
1207
1206
1208 assert vparts is not None # help pytype
1207 assert vparts is not None # help pytype
1209
1208
1210 vints = []
1209 vints = []
1211 for i in vparts.split(b'.'):
1210 for i in vparts.split(b'.'):
1212 try:
1211 try:
1213 vints.append(int(i))
1212 vints.append(int(i))
1214 except ValueError:
1213 except ValueError:
1215 break
1214 break
1216 # (3, 6) -> (3, 6, None)
1215 # (3, 6) -> (3, 6, None)
1217 while len(vints) < 3:
1216 while len(vints) < 3:
1218 vints.append(None)
1217 vints.append(None)
1219
1218
1220 if n == 2:
1219 if n == 2:
1221 return (vints[0], vints[1])
1220 return (vints[0], vints[1])
1222 if n == 3:
1221 if n == 3:
1223 return (vints[0], vints[1], vints[2])
1222 return (vints[0], vints[1], vints[2])
1224 if n == 4:
1223 if n == 4:
1225 return (vints[0], vints[1], vints[2], extra)
1224 return (vints[0], vints[1], vints[2], extra)
1226
1225
1227 raise error.ProgrammingError(b"invalid version part request: %d" % n)
1226 raise error.ProgrammingError(b"invalid version part request: %d" % n)
1228
1227
1229
1228
1230 def cachefunc(func):
1229 def cachefunc(func):
1231 '''cache the result of function calls'''
1230 '''cache the result of function calls'''
1232 # XXX doesn't handle keywords args
1231 # XXX doesn't handle keywords args
1233 if func.__code__.co_argcount == 0:
1232 if func.__code__.co_argcount == 0:
1234 listcache = []
1233 listcache = []
1235
1234
1236 def f():
1235 def f():
1237 if len(listcache) == 0:
1236 if len(listcache) == 0:
1238 listcache.append(func())
1237 listcache.append(func())
1239 return listcache[0]
1238 return listcache[0]
1240
1239
1241 return f
1240 return f
1242 cache = {}
1241 cache = {}
1243 if func.__code__.co_argcount == 1:
1242 if func.__code__.co_argcount == 1:
1244 # we gain a small amount of time because
1243 # we gain a small amount of time because
1245 # we don't need to pack/unpack the list
1244 # we don't need to pack/unpack the list
1246 def f(arg):
1245 def f(arg):
1247 if arg not in cache:
1246 if arg not in cache:
1248 cache[arg] = func(arg)
1247 cache[arg] = func(arg)
1249 return cache[arg]
1248 return cache[arg]
1250
1249
1251 else:
1250 else:
1252
1251
1253 def f(*args):
1252 def f(*args):
1254 if args not in cache:
1253 if args not in cache:
1255 cache[args] = func(*args)
1254 cache[args] = func(*args)
1256 return cache[args]
1255 return cache[args]
1257
1256
1258 return f
1257 return f
1259
1258
1260
1259
1261 class cow(object):
1260 class cow(object):
1262 """helper class to make copy-on-write easier
1261 """helper class to make copy-on-write easier
1263
1262
1264 Call preparewrite before doing any writes.
1263 Call preparewrite before doing any writes.
1265 """
1264 """
1266
1265
1267 def preparewrite(self):
1266 def preparewrite(self):
1268 """call this before writes, return self or a copied new object"""
1267 """call this before writes, return self or a copied new object"""
1269 if getattr(self, '_copied', 0):
1268 if getattr(self, '_copied', 0):
1270 self._copied -= 1
1269 self._copied -= 1
1271 # Function cow.__init__ expects 1 arg(s), got 2 [wrong-arg-count]
1270 # Function cow.__init__ expects 1 arg(s), got 2 [wrong-arg-count]
1272 return self.__class__(self) # pytype: disable=wrong-arg-count
1271 return self.__class__(self) # pytype: disable=wrong-arg-count
1273 return self
1272 return self
1274
1273
1275 def copy(self):
1274 def copy(self):
1276 """always do a cheap copy"""
1275 """always do a cheap copy"""
1277 self._copied = getattr(self, '_copied', 0) + 1
1276 self._copied = getattr(self, '_copied', 0) + 1
1278 return self
1277 return self
1279
1278
1280
1279
1281 class sortdict(collections.OrderedDict):
1280 class sortdict(collections.OrderedDict):
1282 """a simple sorted dictionary
1281 """a simple sorted dictionary
1283
1282
1284 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1283 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1285 >>> d2 = d1.copy()
1284 >>> d2 = d1.copy()
1286 >>> d2
1285 >>> d2
1287 sortdict([('a', 0), ('b', 1)])
1286 sortdict([('a', 0), ('b', 1)])
1288 >>> d2.update([(b'a', 2)])
1287 >>> d2.update([(b'a', 2)])
1289 >>> list(d2.keys()) # should still be in last-set order
1288 >>> list(d2.keys()) # should still be in last-set order
1290 ['b', 'a']
1289 ['b', 'a']
1291 >>> d1.insert(1, b'a.5', 0.5)
1290 >>> d1.insert(1, b'a.5', 0.5)
1292 >>> d1
1291 >>> d1
1293 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1292 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1294 """
1293 """
1295
1294
1296 def __setitem__(self, key, value):
1295 def __setitem__(self, key, value):
1297 if key in self:
1296 if key in self:
1298 del self[key]
1297 del self[key]
1299 super(sortdict, self).__setitem__(key, value)
1298 super(sortdict, self).__setitem__(key, value)
1300
1299
1301 if pycompat.ispypy:
1300 if pycompat.ispypy:
1302 # __setitem__() isn't called as of PyPy 5.8.0
1301 # __setitem__() isn't called as of PyPy 5.8.0
1303 def update(self, src, **f):
1302 def update(self, src, **f):
1304 if isinstance(src, dict):
1303 if isinstance(src, dict):
1305 src = pycompat.iteritems(src)
1304 src = pycompat.iteritems(src)
1306 for k, v in src:
1305 for k, v in src:
1307 self[k] = v
1306 self[k] = v
1308 for k in f:
1307 for k in f:
1309 self[k] = f[k]
1308 self[k] = f[k]
1310
1309
1311 def insert(self, position, key, value):
1310 def insert(self, position, key, value):
1312 for (i, (k, v)) in enumerate(list(self.items())):
1311 for (i, (k, v)) in enumerate(list(self.items())):
1313 if i == position:
1312 if i == position:
1314 self[key] = value
1313 self[key] = value
1315 if i >= position:
1314 if i >= position:
1316 del self[k]
1315 del self[k]
1317 self[k] = v
1316 self[k] = v
1318
1317
1319
1318
1320 class cowdict(cow, dict):
1319 class cowdict(cow, dict):
1321 """copy-on-write dict
1320 """copy-on-write dict
1322
1321
1323 Be sure to call d = d.preparewrite() before writing to d.
1322 Be sure to call d = d.preparewrite() before writing to d.
1324
1323
1325 >>> a = cowdict()
1324 >>> a = cowdict()
1326 >>> a is a.preparewrite()
1325 >>> a is a.preparewrite()
1327 True
1326 True
1328 >>> b = a.copy()
1327 >>> b = a.copy()
1329 >>> b is a
1328 >>> b is a
1330 True
1329 True
1331 >>> c = b.copy()
1330 >>> c = b.copy()
1332 >>> c is a
1331 >>> c is a
1333 True
1332 True
1334 >>> a = a.preparewrite()
1333 >>> a = a.preparewrite()
1335 >>> b is a
1334 >>> b is a
1336 False
1335 False
1337 >>> a is a.preparewrite()
1336 >>> a is a.preparewrite()
1338 True
1337 True
1339 >>> c = c.preparewrite()
1338 >>> c = c.preparewrite()
1340 >>> b is c
1339 >>> b is c
1341 False
1340 False
1342 >>> b is b.preparewrite()
1341 >>> b is b.preparewrite()
1343 True
1342 True
1344 """
1343 """
1345
1344
1346
1345
1347 class cowsortdict(cow, sortdict):
1346 class cowsortdict(cow, sortdict):
1348 """copy-on-write sortdict
1347 """copy-on-write sortdict
1349
1348
1350 Be sure to call d = d.preparewrite() before writing to d.
1349 Be sure to call d = d.preparewrite() before writing to d.
1351 """
1350 """
1352
1351
1353
1352
1354 class transactional(object): # pytype: disable=ignored-metaclass
1353 class transactional(object): # pytype: disable=ignored-metaclass
1355 """Base class for making a transactional type into a context manager."""
1354 """Base class for making a transactional type into a context manager."""
1356
1355
1357 __metaclass__ = abc.ABCMeta
1356 __metaclass__ = abc.ABCMeta
1358
1357
1359 @abc.abstractmethod
1358 @abc.abstractmethod
1360 def close(self):
1359 def close(self):
1361 """Successfully closes the transaction."""
1360 """Successfully closes the transaction."""
1362
1361
1363 @abc.abstractmethod
1362 @abc.abstractmethod
1364 def release(self):
1363 def release(self):
1365 """Marks the end of the transaction.
1364 """Marks the end of the transaction.
1366
1365
1367 If the transaction has not been closed, it will be aborted.
1366 If the transaction has not been closed, it will be aborted.
1368 """
1367 """
1369
1368
1370 def __enter__(self):
1369 def __enter__(self):
1371 return self
1370 return self
1372
1371
1373 def __exit__(self, exc_type, exc_val, exc_tb):
1372 def __exit__(self, exc_type, exc_val, exc_tb):
1374 try:
1373 try:
1375 if exc_type is None:
1374 if exc_type is None:
1376 self.close()
1375 self.close()
1377 finally:
1376 finally:
1378 self.release()
1377 self.release()
1379
1378
1380
1379
1381 @contextlib.contextmanager
1380 @contextlib.contextmanager
1382 def acceptintervention(tr=None):
1381 def acceptintervention(tr=None):
1383 """A context manager that closes the transaction on InterventionRequired
1382 """A context manager that closes the transaction on InterventionRequired
1384
1383
1385 If no transaction was provided, this simply runs the body and returns
1384 If no transaction was provided, this simply runs the body and returns
1386 """
1385 """
1387 if not tr:
1386 if not tr:
1388 yield
1387 yield
1389 return
1388 return
1390 try:
1389 try:
1391 yield
1390 yield
1392 tr.close()
1391 tr.close()
1393 except error.InterventionRequired:
1392 except error.InterventionRequired:
1394 tr.close()
1393 tr.close()
1395 raise
1394 raise
1396 finally:
1395 finally:
1397 tr.release()
1396 tr.release()
1398
1397
1399
1398
1400 @contextlib.contextmanager
1399 @contextlib.contextmanager
1401 def nullcontextmanager(enter_result=None):
1400 def nullcontextmanager(enter_result=None):
1402 yield enter_result
1401 yield enter_result
1403
1402
1404
1403
1405 class _lrucachenode(object):
1404 class _lrucachenode(object):
1406 """A node in a doubly linked list.
1405 """A node in a doubly linked list.
1407
1406
1408 Holds a reference to nodes on either side as well as a key-value
1407 Holds a reference to nodes on either side as well as a key-value
1409 pair for the dictionary entry.
1408 pair for the dictionary entry.
1410 """
1409 """
1411
1410
1412 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1411 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1413
1412
1414 def __init__(self):
1413 def __init__(self):
1415 self.next = self
1414 self.next = self
1416 self.prev = self
1415 self.prev = self
1417
1416
1418 self.key = _notset
1417 self.key = _notset
1419 self.value = None
1418 self.value = None
1420 self.cost = 0
1419 self.cost = 0
1421
1420
1422 def markempty(self):
1421 def markempty(self):
1423 """Mark the node as emptied."""
1422 """Mark the node as emptied."""
1424 self.key = _notset
1423 self.key = _notset
1425 self.value = None
1424 self.value = None
1426 self.cost = 0
1425 self.cost = 0
1427
1426
1428
1427
1429 class lrucachedict(object):
1428 class lrucachedict(object):
1430 """Dict that caches most recent accesses and sets.
1429 """Dict that caches most recent accesses and sets.
1431
1430
1432 The dict consists of an actual backing dict - indexed by original
1431 The dict consists of an actual backing dict - indexed by original
1433 key - and a doubly linked circular list defining the order of entries in
1432 key - and a doubly linked circular list defining the order of entries in
1434 the cache.
1433 the cache.
1435
1434
1436 The head node is the newest entry in the cache. If the cache is full,
1435 The head node is the newest entry in the cache. If the cache is full,
1437 we recycle head.prev and make it the new head. Cache accesses result in
1436 we recycle head.prev and make it the new head. Cache accesses result in
1438 the node being moved to before the existing head and being marked as the
1437 the node being moved to before the existing head and being marked as the
1439 new head node.
1438 new head node.
1440
1439
1441 Items in the cache can be inserted with an optional "cost" value. This is
1440 Items in the cache can be inserted with an optional "cost" value. This is
1442 simply an integer that is specified by the caller. The cache can be queried
1441 simply an integer that is specified by the caller. The cache can be queried
1443 for the total cost of all items presently in the cache.
1442 for the total cost of all items presently in the cache.
1444
1443
1445 The cache can also define a maximum cost. If a cache insertion would
1444 The cache can also define a maximum cost. If a cache insertion would
1446 cause the total cost of the cache to go beyond the maximum cost limit,
1445 cause the total cost of the cache to go beyond the maximum cost limit,
1447 nodes will be evicted to make room for the new code. This can be used
1446 nodes will be evicted to make room for the new code. This can be used
1448 to e.g. set a max memory limit and associate an estimated bytes size
1447 to e.g. set a max memory limit and associate an estimated bytes size
1449 cost to each item in the cache. By default, no maximum cost is enforced.
1448 cost to each item in the cache. By default, no maximum cost is enforced.
1450 """
1449 """
1451
1450
1452 def __init__(self, max, maxcost=0):
1451 def __init__(self, max, maxcost=0):
1453 self._cache = {}
1452 self._cache = {}
1454
1453
1455 self._head = _lrucachenode()
1454 self._head = _lrucachenode()
1456 self._size = 1
1455 self._size = 1
1457 self.capacity = max
1456 self.capacity = max
1458 self.totalcost = 0
1457 self.totalcost = 0
1459 self.maxcost = maxcost
1458 self.maxcost = maxcost
1460
1459
1461 def __len__(self):
1460 def __len__(self):
1462 return len(self._cache)
1461 return len(self._cache)
1463
1462
1464 def __contains__(self, k):
1463 def __contains__(self, k):
1465 return k in self._cache
1464 return k in self._cache
1466
1465
1467 def __iter__(self):
1466 def __iter__(self):
1468 # We don't have to iterate in cache order, but why not.
1467 # We don't have to iterate in cache order, but why not.
1469 n = self._head
1468 n = self._head
1470 for i in range(len(self._cache)):
1469 for i in range(len(self._cache)):
1471 yield n.key
1470 yield n.key
1472 n = n.next
1471 n = n.next
1473
1472
1474 def __getitem__(self, k):
1473 def __getitem__(self, k):
1475 node = self._cache[k]
1474 node = self._cache[k]
1476 self._movetohead(node)
1475 self._movetohead(node)
1477 return node.value
1476 return node.value
1478
1477
1479 def insert(self, k, v, cost=0):
1478 def insert(self, k, v, cost=0):
1480 """Insert a new item in the cache with optional cost value."""
1479 """Insert a new item in the cache with optional cost value."""
1481 node = self._cache.get(k)
1480 node = self._cache.get(k)
1482 # Replace existing value and mark as newest.
1481 # Replace existing value and mark as newest.
1483 if node is not None:
1482 if node is not None:
1484 self.totalcost -= node.cost
1483 self.totalcost -= node.cost
1485 node.value = v
1484 node.value = v
1486 node.cost = cost
1485 node.cost = cost
1487 self.totalcost += cost
1486 self.totalcost += cost
1488 self._movetohead(node)
1487 self._movetohead(node)
1489
1488
1490 if self.maxcost:
1489 if self.maxcost:
1491 self._enforcecostlimit()
1490 self._enforcecostlimit()
1492
1491
1493 return
1492 return
1494
1493
1495 if self._size < self.capacity:
1494 if self._size < self.capacity:
1496 node = self._addcapacity()
1495 node = self._addcapacity()
1497 else:
1496 else:
1498 # Grab the last/oldest item.
1497 # Grab the last/oldest item.
1499 node = self._head.prev
1498 node = self._head.prev
1500
1499
1501 # At capacity. Kill the old entry.
1500 # At capacity. Kill the old entry.
1502 if node.key is not _notset:
1501 if node.key is not _notset:
1503 self.totalcost -= node.cost
1502 self.totalcost -= node.cost
1504 del self._cache[node.key]
1503 del self._cache[node.key]
1505
1504
1506 node.key = k
1505 node.key = k
1507 node.value = v
1506 node.value = v
1508 node.cost = cost
1507 node.cost = cost
1509 self.totalcost += cost
1508 self.totalcost += cost
1510 self._cache[k] = node
1509 self._cache[k] = node
1511 # And mark it as newest entry. No need to adjust order since it
1510 # And mark it as newest entry. No need to adjust order since it
1512 # is already self._head.prev.
1511 # is already self._head.prev.
1513 self._head = node
1512 self._head = node
1514
1513
1515 if self.maxcost:
1514 if self.maxcost:
1516 self._enforcecostlimit()
1515 self._enforcecostlimit()
1517
1516
1518 def __setitem__(self, k, v):
1517 def __setitem__(self, k, v):
1519 self.insert(k, v)
1518 self.insert(k, v)
1520
1519
1521 def __delitem__(self, k):
1520 def __delitem__(self, k):
1522 self.pop(k)
1521 self.pop(k)
1523
1522
1524 def pop(self, k, default=_notset):
1523 def pop(self, k, default=_notset):
1525 try:
1524 try:
1526 node = self._cache.pop(k)
1525 node = self._cache.pop(k)
1527 except KeyError:
1526 except KeyError:
1528 if default is _notset:
1527 if default is _notset:
1529 raise
1528 raise
1530 return default
1529 return default
1531
1530
1532 assert node is not None # help pytype
1531 assert node is not None # help pytype
1533 value = node.value
1532 value = node.value
1534 self.totalcost -= node.cost
1533 self.totalcost -= node.cost
1535 node.markempty()
1534 node.markempty()
1536
1535
1537 # Temporarily mark as newest item before re-adjusting head to make
1536 # Temporarily mark as newest item before re-adjusting head to make
1538 # this node the oldest item.
1537 # this node the oldest item.
1539 self._movetohead(node)
1538 self._movetohead(node)
1540 self._head = node.next
1539 self._head = node.next
1541
1540
1542 return value
1541 return value
1543
1542
1544 # Additional dict methods.
1543 # Additional dict methods.
1545
1544
1546 def get(self, k, default=None):
1545 def get(self, k, default=None):
1547 try:
1546 try:
1548 return self.__getitem__(k)
1547 return self.__getitem__(k)
1549 except KeyError:
1548 except KeyError:
1550 return default
1549 return default
1551
1550
1552 def peek(self, k, default=_notset):
1551 def peek(self, k, default=_notset):
1553 """Get the specified item without moving it to the head
1552 """Get the specified item without moving it to the head
1554
1553
1555 Unlike get(), this doesn't mutate the internal state. But be aware
1554 Unlike get(), this doesn't mutate the internal state. But be aware
1556 that it doesn't mean peek() is thread safe.
1555 that it doesn't mean peek() is thread safe.
1557 """
1556 """
1558 try:
1557 try:
1559 node = self._cache[k]
1558 node = self._cache[k]
1560 assert node is not None # help pytype
1559 assert node is not None # help pytype
1561 return node.value
1560 return node.value
1562 except KeyError:
1561 except KeyError:
1563 if default is _notset:
1562 if default is _notset:
1564 raise
1563 raise
1565 return default
1564 return default
1566
1565
1567 def clear(self):
1566 def clear(self):
1568 n = self._head
1567 n = self._head
1569 while n.key is not _notset:
1568 while n.key is not _notset:
1570 self.totalcost -= n.cost
1569 self.totalcost -= n.cost
1571 n.markempty()
1570 n.markempty()
1572 n = n.next
1571 n = n.next
1573
1572
1574 self._cache.clear()
1573 self._cache.clear()
1575
1574
1576 def copy(self, capacity=None, maxcost=0):
1575 def copy(self, capacity=None, maxcost=0):
1577 """Create a new cache as a copy of the current one.
1576 """Create a new cache as a copy of the current one.
1578
1577
1579 By default, the new cache has the same capacity as the existing one.
1578 By default, the new cache has the same capacity as the existing one.
1580 But, the cache capacity can be changed as part of performing the
1579 But, the cache capacity can be changed as part of performing the
1581 copy.
1580 copy.
1582
1581
1583 Items in the copy have an insertion/access order matching this
1582 Items in the copy have an insertion/access order matching this
1584 instance.
1583 instance.
1585 """
1584 """
1586
1585
1587 capacity = capacity or self.capacity
1586 capacity = capacity or self.capacity
1588 maxcost = maxcost or self.maxcost
1587 maxcost = maxcost or self.maxcost
1589 result = lrucachedict(capacity, maxcost=maxcost)
1588 result = lrucachedict(capacity, maxcost=maxcost)
1590
1589
1591 # We copy entries by iterating in oldest-to-newest order so the copy
1590 # We copy entries by iterating in oldest-to-newest order so the copy
1592 # has the correct ordering.
1591 # has the correct ordering.
1593
1592
1594 # Find the first non-empty entry.
1593 # Find the first non-empty entry.
1595 n = self._head.prev
1594 n = self._head.prev
1596 while n.key is _notset and n is not self._head:
1595 while n.key is _notset and n is not self._head:
1597 n = n.prev
1596 n = n.prev
1598
1597
1599 # We could potentially skip the first N items when decreasing capacity.
1598 # We could potentially skip the first N items when decreasing capacity.
1600 # But let's keep it simple unless it is a performance problem.
1599 # But let's keep it simple unless it is a performance problem.
1601 for i in range(len(self._cache)):
1600 for i in range(len(self._cache)):
1602 result.insert(n.key, n.value, cost=n.cost)
1601 result.insert(n.key, n.value, cost=n.cost)
1603 n = n.prev
1602 n = n.prev
1604
1603
1605 return result
1604 return result
1606
1605
1607 def popoldest(self):
1606 def popoldest(self):
1608 """Remove the oldest item from the cache.
1607 """Remove the oldest item from the cache.
1609
1608
1610 Returns the (key, value) describing the removed cache entry.
1609 Returns the (key, value) describing the removed cache entry.
1611 """
1610 """
1612 if not self._cache:
1611 if not self._cache:
1613 return
1612 return
1614
1613
1615 # Walk the linked list backwards starting at tail node until we hit
1614 # Walk the linked list backwards starting at tail node until we hit
1616 # a non-empty node.
1615 # a non-empty node.
1617 n = self._head.prev
1616 n = self._head.prev
1618
1617
1619 assert n is not None # help pytype
1618 assert n is not None # help pytype
1620
1619
1621 while n.key is _notset:
1620 while n.key is _notset:
1622 n = n.prev
1621 n = n.prev
1623
1622
1624 assert n is not None # help pytype
1623 assert n is not None # help pytype
1625
1624
1626 key, value = n.key, n.value
1625 key, value = n.key, n.value
1627
1626
1628 # And remove it from the cache and mark it as empty.
1627 # And remove it from the cache and mark it as empty.
1629 del self._cache[n.key]
1628 del self._cache[n.key]
1630 self.totalcost -= n.cost
1629 self.totalcost -= n.cost
1631 n.markempty()
1630 n.markempty()
1632
1631
1633 return key, value
1632 return key, value
1634
1633
1635 def _movetohead(self, node):
1634 def _movetohead(self, node):
1636 """Mark a node as the newest, making it the new head.
1635 """Mark a node as the newest, making it the new head.
1637
1636
1638 When a node is accessed, it becomes the freshest entry in the LRU
1637 When a node is accessed, it becomes the freshest entry in the LRU
1639 list, which is denoted by self._head.
1638 list, which is denoted by self._head.
1640
1639
1641 Visually, let's make ``N`` the new head node (* denotes head):
1640 Visually, let's make ``N`` the new head node (* denotes head):
1642
1641
1643 previous/oldest <-> head <-> next/next newest
1642 previous/oldest <-> head <-> next/next newest
1644
1643
1645 ----<->--- A* ---<->-----
1644 ----<->--- A* ---<->-----
1646 | |
1645 | |
1647 E <-> D <-> N <-> C <-> B
1646 E <-> D <-> N <-> C <-> B
1648
1647
1649 To:
1648 To:
1650
1649
1651 ----<->--- N* ---<->-----
1650 ----<->--- N* ---<->-----
1652 | |
1651 | |
1653 E <-> D <-> C <-> B <-> A
1652 E <-> D <-> C <-> B <-> A
1654
1653
1655 This requires the following moves:
1654 This requires the following moves:
1656
1655
1657 C.next = D (node.prev.next = node.next)
1656 C.next = D (node.prev.next = node.next)
1658 D.prev = C (node.next.prev = node.prev)
1657 D.prev = C (node.next.prev = node.prev)
1659 E.next = N (head.prev.next = node)
1658 E.next = N (head.prev.next = node)
1660 N.prev = E (node.prev = head.prev)
1659 N.prev = E (node.prev = head.prev)
1661 N.next = A (node.next = head)
1660 N.next = A (node.next = head)
1662 A.prev = N (head.prev = node)
1661 A.prev = N (head.prev = node)
1663 """
1662 """
1664 head = self._head
1663 head = self._head
1665 # C.next = D
1664 # C.next = D
1666 node.prev.next = node.next
1665 node.prev.next = node.next
1667 # D.prev = C
1666 # D.prev = C
1668 node.next.prev = node.prev
1667 node.next.prev = node.prev
1669 # N.prev = E
1668 # N.prev = E
1670 node.prev = head.prev
1669 node.prev = head.prev
1671 # N.next = A
1670 # N.next = A
1672 # It is tempting to do just "head" here, however if node is
1671 # It is tempting to do just "head" here, however if node is
1673 # adjacent to head, this will do bad things.
1672 # adjacent to head, this will do bad things.
1674 node.next = head.prev.next
1673 node.next = head.prev.next
1675 # E.next = N
1674 # E.next = N
1676 node.next.prev = node
1675 node.next.prev = node
1677 # A.prev = N
1676 # A.prev = N
1678 node.prev.next = node
1677 node.prev.next = node
1679
1678
1680 self._head = node
1679 self._head = node
1681
1680
1682 def _addcapacity(self):
1681 def _addcapacity(self):
1683 """Add a node to the circular linked list.
1682 """Add a node to the circular linked list.
1684
1683
1685 The new node is inserted before the head node.
1684 The new node is inserted before the head node.
1686 """
1685 """
1687 head = self._head
1686 head = self._head
1688 node = _lrucachenode()
1687 node = _lrucachenode()
1689 head.prev.next = node
1688 head.prev.next = node
1690 node.prev = head.prev
1689 node.prev = head.prev
1691 node.next = head
1690 node.next = head
1692 head.prev = node
1691 head.prev = node
1693 self._size += 1
1692 self._size += 1
1694 return node
1693 return node
1695
1694
1696 def _enforcecostlimit(self):
1695 def _enforcecostlimit(self):
1697 # This should run after an insertion. It should only be called if total
1696 # This should run after an insertion. It should only be called if total
1698 # cost limits are being enforced.
1697 # cost limits are being enforced.
1699 # The most recently inserted node is never evicted.
1698 # The most recently inserted node is never evicted.
1700 if len(self) <= 1 or self.totalcost <= self.maxcost:
1699 if len(self) <= 1 or self.totalcost <= self.maxcost:
1701 return
1700 return
1702
1701
1703 # This is logically equivalent to calling popoldest() until we
1702 # This is logically equivalent to calling popoldest() until we
1704 # free up enough cost. We don't do that since popoldest() needs
1703 # free up enough cost. We don't do that since popoldest() needs
1705 # to walk the linked list and doing this in a loop would be
1704 # to walk the linked list and doing this in a loop would be
1706 # quadratic. So we find the first non-empty node and then
1705 # quadratic. So we find the first non-empty node and then
1707 # walk nodes until we free up enough capacity.
1706 # walk nodes until we free up enough capacity.
1708 #
1707 #
1709 # If we only removed the minimum number of nodes to free enough
1708 # If we only removed the minimum number of nodes to free enough
1710 # cost at insert time, chances are high that the next insert would
1709 # cost at insert time, chances are high that the next insert would
1711 # also require pruning. This would effectively constitute quadratic
1710 # also require pruning. This would effectively constitute quadratic
1712 # behavior for insert-heavy workloads. To mitigate this, we set a
1711 # behavior for insert-heavy workloads. To mitigate this, we set a
1713 # target cost that is a percentage of the max cost. This will tend
1712 # target cost that is a percentage of the max cost. This will tend
1714 # to free more nodes when the high water mark is reached, which
1713 # to free more nodes when the high water mark is reached, which
1715 # lowers the chances of needing to prune on the subsequent insert.
1714 # lowers the chances of needing to prune on the subsequent insert.
1716 targetcost = int(self.maxcost * 0.75)
1715 targetcost = int(self.maxcost * 0.75)
1717
1716
1718 n = self._head.prev
1717 n = self._head.prev
1719 while n.key is _notset:
1718 while n.key is _notset:
1720 n = n.prev
1719 n = n.prev
1721
1720
1722 while len(self) > 1 and self.totalcost > targetcost:
1721 while len(self) > 1 and self.totalcost > targetcost:
1723 del self._cache[n.key]
1722 del self._cache[n.key]
1724 self.totalcost -= n.cost
1723 self.totalcost -= n.cost
1725 n.markempty()
1724 n.markempty()
1726 n = n.prev
1725 n = n.prev
1727
1726
1728
1727
1729 def lrucachefunc(func):
1728 def lrucachefunc(func):
1730 '''cache most recent results of function calls'''
1729 '''cache most recent results of function calls'''
1731 cache = {}
1730 cache = {}
1732 order = collections.deque()
1731 order = collections.deque()
1733 if func.__code__.co_argcount == 1:
1732 if func.__code__.co_argcount == 1:
1734
1733
1735 def f(arg):
1734 def f(arg):
1736 if arg not in cache:
1735 if arg not in cache:
1737 if len(cache) > 20:
1736 if len(cache) > 20:
1738 del cache[order.popleft()]
1737 del cache[order.popleft()]
1739 cache[arg] = func(arg)
1738 cache[arg] = func(arg)
1740 else:
1739 else:
1741 order.remove(arg)
1740 order.remove(arg)
1742 order.append(arg)
1741 order.append(arg)
1743 return cache[arg]
1742 return cache[arg]
1744
1743
1745 else:
1744 else:
1746
1745
1747 def f(*args):
1746 def f(*args):
1748 if args not in cache:
1747 if args not in cache:
1749 if len(cache) > 20:
1748 if len(cache) > 20:
1750 del cache[order.popleft()]
1749 del cache[order.popleft()]
1751 cache[args] = func(*args)
1750 cache[args] = func(*args)
1752 else:
1751 else:
1753 order.remove(args)
1752 order.remove(args)
1754 order.append(args)
1753 order.append(args)
1755 return cache[args]
1754 return cache[args]
1756
1755
1757 return f
1756 return f
1758
1757
1759
1758
1760 class propertycache(object):
1759 class propertycache(object):
1761 def __init__(self, func):
1760 def __init__(self, func):
1762 self.func = func
1761 self.func = func
1763 self.name = func.__name__
1762 self.name = func.__name__
1764
1763
1765 def __get__(self, obj, type=None):
1764 def __get__(self, obj, type=None):
1766 result = self.func(obj)
1765 result = self.func(obj)
1767 self.cachevalue(obj, result)
1766 self.cachevalue(obj, result)
1768 return result
1767 return result
1769
1768
1770 def cachevalue(self, obj, value):
1769 def cachevalue(self, obj, value):
1771 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1770 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1772 obj.__dict__[self.name] = value
1771 obj.__dict__[self.name] = value
1773
1772
1774
1773
1775 def clearcachedproperty(obj, prop):
1774 def clearcachedproperty(obj, prop):
1776 '''clear a cached property value, if one has been set'''
1775 '''clear a cached property value, if one has been set'''
1777 prop = pycompat.sysstr(prop)
1776 prop = pycompat.sysstr(prop)
1778 if prop in obj.__dict__:
1777 if prop in obj.__dict__:
1779 del obj.__dict__[prop]
1778 del obj.__dict__[prop]
1780
1779
1781
1780
1782 def increasingchunks(source, min=1024, max=65536):
1781 def increasingchunks(source, min=1024, max=65536):
1783 """return no less than min bytes per chunk while data remains,
1782 """return no less than min bytes per chunk while data remains,
1784 doubling min after each chunk until it reaches max"""
1783 doubling min after each chunk until it reaches max"""
1785
1784
1786 def log2(x):
1785 def log2(x):
1787 if not x:
1786 if not x:
1788 return 0
1787 return 0
1789 i = 0
1788 i = 0
1790 while x:
1789 while x:
1791 x >>= 1
1790 x >>= 1
1792 i += 1
1791 i += 1
1793 return i - 1
1792 return i - 1
1794
1793
1795 buf = []
1794 buf = []
1796 blen = 0
1795 blen = 0
1797 for chunk in source:
1796 for chunk in source:
1798 buf.append(chunk)
1797 buf.append(chunk)
1799 blen += len(chunk)
1798 blen += len(chunk)
1800 if blen >= min:
1799 if blen >= min:
1801 if min < max:
1800 if min < max:
1802 min = min << 1
1801 min = min << 1
1803 nmin = 1 << log2(blen)
1802 nmin = 1 << log2(blen)
1804 if nmin > min:
1803 if nmin > min:
1805 min = nmin
1804 min = nmin
1806 if min > max:
1805 if min > max:
1807 min = max
1806 min = max
1808 yield b''.join(buf)
1807 yield b''.join(buf)
1809 blen = 0
1808 blen = 0
1810 buf = []
1809 buf = []
1811 if buf:
1810 if buf:
1812 yield b''.join(buf)
1811 yield b''.join(buf)
1813
1812
1814
1813
1815 def always(fn):
1814 def always(fn):
1816 return True
1815 return True
1817
1816
1818
1817
1819 def never(fn):
1818 def never(fn):
1820 return False
1819 return False
1821
1820
1822
1821
1823 def nogc(func):
1822 def nogc(func):
1824 """disable garbage collector
1823 """disable garbage collector
1825
1824
1826 Python's garbage collector triggers a GC each time a certain number of
1825 Python's garbage collector triggers a GC each time a certain number of
1827 container objects (the number being defined by gc.get_threshold()) are
1826 container objects (the number being defined by gc.get_threshold()) are
1828 allocated even when marked not to be tracked by the collector. Tracking has
1827 allocated even when marked not to be tracked by the collector. Tracking has
1829 no effect on when GCs are triggered, only on what objects the GC looks
1828 no effect on when GCs are triggered, only on what objects the GC looks
1830 into. As a workaround, disable GC while building complex (huge)
1829 into. As a workaround, disable GC while building complex (huge)
1831 containers.
1830 containers.
1832
1831
1833 This garbage collector issue have been fixed in 2.7. But it still affect
1832 This garbage collector issue have been fixed in 2.7. But it still affect
1834 CPython's performance.
1833 CPython's performance.
1835 """
1834 """
1836
1835
1837 def wrapper(*args, **kwargs):
1836 def wrapper(*args, **kwargs):
1838 gcenabled = gc.isenabled()
1837 gcenabled = gc.isenabled()
1839 gc.disable()
1838 gc.disable()
1840 try:
1839 try:
1841 return func(*args, **kwargs)
1840 return func(*args, **kwargs)
1842 finally:
1841 finally:
1843 if gcenabled:
1842 if gcenabled:
1844 gc.enable()
1843 gc.enable()
1845
1844
1846 return wrapper
1845 return wrapper
1847
1846
1848
1847
1849 if pycompat.ispypy:
1848 if pycompat.ispypy:
1850 # PyPy runs slower with gc disabled
1849 # PyPy runs slower with gc disabled
1851 nogc = lambda x: x
1850 nogc = lambda x: x
1852
1851
1853
1852
1854 def pathto(root, n1, n2):
1853 def pathto(root, n1, n2):
1855 # type: (bytes, bytes, bytes) -> bytes
1854 # type: (bytes, bytes, bytes) -> bytes
1856 """return the relative path from one place to another.
1855 """return the relative path from one place to another.
1857 root should use os.sep to separate directories
1856 root should use os.sep to separate directories
1858 n1 should use os.sep to separate directories
1857 n1 should use os.sep to separate directories
1859 n2 should use "/" to separate directories
1858 n2 should use "/" to separate directories
1860 returns an os.sep-separated path.
1859 returns an os.sep-separated path.
1861
1860
1862 If n1 is a relative path, it's assumed it's
1861 If n1 is a relative path, it's assumed it's
1863 relative to root.
1862 relative to root.
1864 n2 should always be relative to root.
1863 n2 should always be relative to root.
1865 """
1864 """
1866 if not n1:
1865 if not n1:
1867 return localpath(n2)
1866 return localpath(n2)
1868 if os.path.isabs(n1):
1867 if os.path.isabs(n1):
1869 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1868 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1870 return os.path.join(root, localpath(n2))
1869 return os.path.join(root, localpath(n2))
1871 n2 = b'/'.join((pconvert(root), n2))
1870 n2 = b'/'.join((pconvert(root), n2))
1872 a, b = splitpath(n1), n2.split(b'/')
1871 a, b = splitpath(n1), n2.split(b'/')
1873 a.reverse()
1872 a.reverse()
1874 b.reverse()
1873 b.reverse()
1875 while a and b and a[-1] == b[-1]:
1874 while a and b and a[-1] == b[-1]:
1876 a.pop()
1875 a.pop()
1877 b.pop()
1876 b.pop()
1878 b.reverse()
1877 b.reverse()
1879 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1878 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1880
1879
1881
1880
1882 def checksignature(func, depth=1):
1881 def checksignature(func, depth=1):
1883 '''wrap a function with code to check for calling errors'''
1882 '''wrap a function with code to check for calling errors'''
1884
1883
1885 def check(*args, **kwargs):
1884 def check(*args, **kwargs):
1886 try:
1885 try:
1887 return func(*args, **kwargs)
1886 return func(*args, **kwargs)
1888 except TypeError:
1887 except TypeError:
1889 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1888 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1890 raise error.SignatureError
1889 raise error.SignatureError
1891 raise
1890 raise
1892
1891
1893 return check
1892 return check
1894
1893
1895
1894
1896 # a whilelist of known filesystems where hardlink works reliably
1895 # a whilelist of known filesystems where hardlink works reliably
1897 _hardlinkfswhitelist = {
1896 _hardlinkfswhitelist = {
1898 b'apfs',
1897 b'apfs',
1899 b'btrfs',
1898 b'btrfs',
1900 b'ext2',
1899 b'ext2',
1901 b'ext3',
1900 b'ext3',
1902 b'ext4',
1901 b'ext4',
1903 b'hfs',
1902 b'hfs',
1904 b'jfs',
1903 b'jfs',
1905 b'NTFS',
1904 b'NTFS',
1906 b'reiserfs',
1905 b'reiserfs',
1907 b'tmpfs',
1906 b'tmpfs',
1908 b'ufs',
1907 b'ufs',
1909 b'xfs',
1908 b'xfs',
1910 b'zfs',
1909 b'zfs',
1911 }
1910 }
1912
1911
1913
1912
1914 def copyfile(
1913 def copyfile(
1915 src,
1914 src,
1916 dest,
1915 dest,
1917 hardlink=False,
1916 hardlink=False,
1918 copystat=False,
1917 copystat=False,
1919 checkambig=False,
1918 checkambig=False,
1920 nb_bytes=None,
1919 nb_bytes=None,
1921 no_hardlink_cb=None,
1920 no_hardlink_cb=None,
1922 check_fs_hardlink=True,
1921 check_fs_hardlink=True,
1923 ):
1922 ):
1924 """copy a file, preserving mode and optionally other stat info like
1923 """copy a file, preserving mode and optionally other stat info like
1925 atime/mtime
1924 atime/mtime
1926
1925
1927 checkambig argument is used with filestat, and is useful only if
1926 checkambig argument is used with filestat, and is useful only if
1928 destination file is guarded by any lock (e.g. repo.lock or
1927 destination file is guarded by any lock (e.g. repo.lock or
1929 repo.wlock).
1928 repo.wlock).
1930
1929
1931 copystat and checkambig should be exclusive.
1930 copystat and checkambig should be exclusive.
1932
1931
1933 nb_bytes: if set only copy the first `nb_bytes` of the source file.
1932 nb_bytes: if set only copy the first `nb_bytes` of the source file.
1934 """
1933 """
1935 assert not (copystat and checkambig)
1934 assert not (copystat and checkambig)
1936 oldstat = None
1935 oldstat = None
1937 if os.path.lexists(dest):
1936 if os.path.lexists(dest):
1938 if checkambig:
1937 if checkambig:
1939 oldstat = checkambig and filestat.frompath(dest)
1938 oldstat = checkambig and filestat.frompath(dest)
1940 unlink(dest)
1939 unlink(dest)
1941 if hardlink and check_fs_hardlink:
1940 if hardlink and check_fs_hardlink:
1942 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1941 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1943 # unless we are confident that dest is on a whitelisted filesystem.
1942 # unless we are confident that dest is on a whitelisted filesystem.
1944 try:
1943 try:
1945 fstype = getfstype(os.path.dirname(dest))
1944 fstype = getfstype(os.path.dirname(dest))
1946 except OSError:
1945 except OSError:
1947 fstype = None
1946 fstype = None
1948 if fstype not in _hardlinkfswhitelist:
1947 if fstype not in _hardlinkfswhitelist:
1949 if no_hardlink_cb is not None:
1948 if no_hardlink_cb is not None:
1950 no_hardlink_cb()
1949 no_hardlink_cb()
1951 hardlink = False
1950 hardlink = False
1952 if hardlink:
1951 if hardlink:
1953 try:
1952 try:
1954 oslink(src, dest)
1953 oslink(src, dest)
1955 if nb_bytes is not None:
1954 if nb_bytes is not None:
1956 m = "the `nb_bytes` argument is incompatible with `hardlink`"
1955 m = "the `nb_bytes` argument is incompatible with `hardlink`"
1957 raise error.ProgrammingError(m)
1956 raise error.ProgrammingError(m)
1958 return
1957 return
1959 except (IOError, OSError) as exc:
1958 except (IOError, OSError) as exc:
1960 if exc.errno != errno.EEXIST and no_hardlink_cb is not None:
1959 if exc.errno != errno.EEXIST and no_hardlink_cb is not None:
1961 no_hardlink_cb()
1960 no_hardlink_cb()
1962 # fall back to normal copy
1961 # fall back to normal copy
1963 if os.path.islink(src):
1962 if os.path.islink(src):
1964 os.symlink(os.readlink(src), dest)
1963 os.symlink(os.readlink(src), dest)
1965 # copytime is ignored for symlinks, but in general copytime isn't needed
1964 # copytime is ignored for symlinks, but in general copytime isn't needed
1966 # for them anyway
1965 # for them anyway
1967 if nb_bytes is not None:
1966 if nb_bytes is not None:
1968 m = "cannot use `nb_bytes` on a symlink"
1967 m = "cannot use `nb_bytes` on a symlink"
1969 raise error.ProgrammingError(m)
1968 raise error.ProgrammingError(m)
1970 else:
1969 else:
1971 try:
1970 try:
1972 shutil.copyfile(src, dest)
1971 shutil.copyfile(src, dest)
1973 if copystat:
1972 if copystat:
1974 # copystat also copies mode
1973 # copystat also copies mode
1975 shutil.copystat(src, dest)
1974 shutil.copystat(src, dest)
1976 else:
1975 else:
1977 shutil.copymode(src, dest)
1976 shutil.copymode(src, dest)
1978 if oldstat and oldstat.stat:
1977 if oldstat and oldstat.stat:
1979 newstat = filestat.frompath(dest)
1978 newstat = filestat.frompath(dest)
1980 if newstat.isambig(oldstat):
1979 if newstat.isambig(oldstat):
1981 # stat of copied file is ambiguous to original one
1980 # stat of copied file is ambiguous to original one
1982 advanced = (
1981 advanced = (
1983 oldstat.stat[stat.ST_MTIME] + 1
1982 oldstat.stat[stat.ST_MTIME] + 1
1984 ) & 0x7FFFFFFF
1983 ) & 0x7FFFFFFF
1985 os.utime(dest, (advanced, advanced))
1984 os.utime(dest, (advanced, advanced))
1986 # We could do something smarter using `copy_file_range` call or similar
1985 # We could do something smarter using `copy_file_range` call or similar
1987 if nb_bytes is not None:
1986 if nb_bytes is not None:
1988 with open(dest, mode='r+') as f:
1987 with open(dest, mode='r+') as f:
1989 f.truncate(nb_bytes)
1988 f.truncate(nb_bytes)
1990 except shutil.Error as inst:
1989 except shutil.Error as inst:
1991 raise error.Abort(stringutil.forcebytestr(inst))
1990 raise error.Abort(stringutil.forcebytestr(inst))
1992
1991
1993
1992
1994 def copyfiles(src, dst, hardlink=None, progress=None):
1993 def copyfiles(src, dst, hardlink=None, progress=None):
1995 """Copy a directory tree using hardlinks if possible."""
1994 """Copy a directory tree using hardlinks if possible."""
1996 num = 0
1995 num = 0
1997
1996
1998 def settopic():
1997 def settopic():
1999 if progress:
1998 if progress:
2000 progress.topic = _(b'linking') if hardlink else _(b'copying')
1999 progress.topic = _(b'linking') if hardlink else _(b'copying')
2001
2000
2002 if os.path.isdir(src):
2001 if os.path.isdir(src):
2003 if hardlink is None:
2002 if hardlink is None:
2004 hardlink = (
2003 hardlink = (
2005 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
2004 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
2006 )
2005 )
2007 settopic()
2006 settopic()
2008 os.mkdir(dst)
2007 os.mkdir(dst)
2009 for name, kind in listdir(src):
2008 for name, kind in listdir(src):
2010 srcname = os.path.join(src, name)
2009 srcname = os.path.join(src, name)
2011 dstname = os.path.join(dst, name)
2010 dstname = os.path.join(dst, name)
2012 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
2011 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
2013 num += n
2012 num += n
2014 else:
2013 else:
2015 if hardlink is None:
2014 if hardlink is None:
2016 hardlink = (
2015 hardlink = (
2017 os.stat(os.path.dirname(src)).st_dev
2016 os.stat(os.path.dirname(src)).st_dev
2018 == os.stat(os.path.dirname(dst)).st_dev
2017 == os.stat(os.path.dirname(dst)).st_dev
2019 )
2018 )
2020 settopic()
2019 settopic()
2021
2020
2022 if hardlink:
2021 if hardlink:
2023 try:
2022 try:
2024 oslink(src, dst)
2023 oslink(src, dst)
2025 except (IOError, OSError) as exc:
2024 except (IOError, OSError) as exc:
2026 if exc.errno != errno.EEXIST:
2025 if exc.errno != errno.EEXIST:
2027 hardlink = False
2026 hardlink = False
2028 # XXX maybe try to relink if the file exist ?
2027 # XXX maybe try to relink if the file exist ?
2029 shutil.copy(src, dst)
2028 shutil.copy(src, dst)
2030 else:
2029 else:
2031 shutil.copy(src, dst)
2030 shutil.copy(src, dst)
2032 num += 1
2031 num += 1
2033 if progress:
2032 if progress:
2034 progress.increment()
2033 progress.increment()
2035
2034
2036 return hardlink, num
2035 return hardlink, num
2037
2036
2038
2037
2039 _winreservednames = {
2038 _winreservednames = {
2040 b'con',
2039 b'con',
2041 b'prn',
2040 b'prn',
2042 b'aux',
2041 b'aux',
2043 b'nul',
2042 b'nul',
2044 b'com1',
2043 b'com1',
2045 b'com2',
2044 b'com2',
2046 b'com3',
2045 b'com3',
2047 b'com4',
2046 b'com4',
2048 b'com5',
2047 b'com5',
2049 b'com6',
2048 b'com6',
2050 b'com7',
2049 b'com7',
2051 b'com8',
2050 b'com8',
2052 b'com9',
2051 b'com9',
2053 b'lpt1',
2052 b'lpt1',
2054 b'lpt2',
2053 b'lpt2',
2055 b'lpt3',
2054 b'lpt3',
2056 b'lpt4',
2055 b'lpt4',
2057 b'lpt5',
2056 b'lpt5',
2058 b'lpt6',
2057 b'lpt6',
2059 b'lpt7',
2058 b'lpt7',
2060 b'lpt8',
2059 b'lpt8',
2061 b'lpt9',
2060 b'lpt9',
2062 }
2061 }
2063 _winreservedchars = b':*?"<>|'
2062 _winreservedchars = b':*?"<>|'
2064
2063
2065
2064
2066 def checkwinfilename(path):
2065 def checkwinfilename(path):
2067 # type: (bytes) -> Optional[bytes]
2066 # type: (bytes) -> Optional[bytes]
2068 r"""Check that the base-relative path is a valid filename on Windows.
2067 r"""Check that the base-relative path is a valid filename on Windows.
2069 Returns None if the path is ok, or a UI string describing the problem.
2068 Returns None if the path is ok, or a UI string describing the problem.
2070
2069
2071 >>> checkwinfilename(b"just/a/normal/path")
2070 >>> checkwinfilename(b"just/a/normal/path")
2072 >>> checkwinfilename(b"foo/bar/con.xml")
2071 >>> checkwinfilename(b"foo/bar/con.xml")
2073 "filename contains 'con', which is reserved on Windows"
2072 "filename contains 'con', which is reserved on Windows"
2074 >>> checkwinfilename(b"foo/con.xml/bar")
2073 >>> checkwinfilename(b"foo/con.xml/bar")
2075 "filename contains 'con', which is reserved on Windows"
2074 "filename contains 'con', which is reserved on Windows"
2076 >>> checkwinfilename(b"foo/bar/xml.con")
2075 >>> checkwinfilename(b"foo/bar/xml.con")
2077 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2076 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2078 "filename contains 'AUX', which is reserved on Windows"
2077 "filename contains 'AUX', which is reserved on Windows"
2079 >>> checkwinfilename(b"foo/bar/bla:.txt")
2078 >>> checkwinfilename(b"foo/bar/bla:.txt")
2080 "filename contains ':', which is reserved on Windows"
2079 "filename contains ':', which is reserved on Windows"
2081 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2080 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2082 "filename contains '\\x07', which is invalid on Windows"
2081 "filename contains '\\x07', which is invalid on Windows"
2083 >>> checkwinfilename(b"foo/bar/bla ")
2082 >>> checkwinfilename(b"foo/bar/bla ")
2084 "filename ends with ' ', which is not allowed on Windows"
2083 "filename ends with ' ', which is not allowed on Windows"
2085 >>> checkwinfilename(b"../bar")
2084 >>> checkwinfilename(b"../bar")
2086 >>> checkwinfilename(b"foo\\")
2085 >>> checkwinfilename(b"foo\\")
2087 "filename ends with '\\', which is invalid on Windows"
2086 "filename ends with '\\', which is invalid on Windows"
2088 >>> checkwinfilename(b"foo\\/bar")
2087 >>> checkwinfilename(b"foo\\/bar")
2089 "directory name ends with '\\', which is invalid on Windows"
2088 "directory name ends with '\\', which is invalid on Windows"
2090 """
2089 """
2091 if path.endswith(b'\\'):
2090 if path.endswith(b'\\'):
2092 return _(b"filename ends with '\\', which is invalid on Windows")
2091 return _(b"filename ends with '\\', which is invalid on Windows")
2093 if b'\\/' in path:
2092 if b'\\/' in path:
2094 return _(b"directory name ends with '\\', which is invalid on Windows")
2093 return _(b"directory name ends with '\\', which is invalid on Windows")
2095 for n in path.replace(b'\\', b'/').split(b'/'):
2094 for n in path.replace(b'\\', b'/').split(b'/'):
2096 if not n:
2095 if not n:
2097 continue
2096 continue
2098 for c in _filenamebytestr(n):
2097 for c in _filenamebytestr(n):
2099 if c in _winreservedchars:
2098 if c in _winreservedchars:
2100 return (
2099 return (
2101 _(
2100 _(
2102 b"filename contains '%s', which is reserved "
2101 b"filename contains '%s', which is reserved "
2103 b"on Windows"
2102 b"on Windows"
2104 )
2103 )
2105 % c
2104 % c
2106 )
2105 )
2107 if ord(c) <= 31:
2106 if ord(c) <= 31:
2108 return _(
2107 return _(
2109 b"filename contains '%s', which is invalid on Windows"
2108 b"filename contains '%s', which is invalid on Windows"
2110 ) % stringutil.escapestr(c)
2109 ) % stringutil.escapestr(c)
2111 base = n.split(b'.')[0]
2110 base = n.split(b'.')[0]
2112 if base and base.lower() in _winreservednames:
2111 if base and base.lower() in _winreservednames:
2113 return (
2112 return (
2114 _(b"filename contains '%s', which is reserved on Windows")
2113 _(b"filename contains '%s', which is reserved on Windows")
2115 % base
2114 % base
2116 )
2115 )
2117 t = n[-1:]
2116 t = n[-1:]
2118 if t in b'. ' and n not in b'..':
2117 if t in b'. ' and n not in b'..':
2119 return (
2118 return (
2120 _(
2119 _(
2121 b"filename ends with '%s', which is not allowed "
2120 b"filename ends with '%s', which is not allowed "
2122 b"on Windows"
2121 b"on Windows"
2123 )
2122 )
2124 % t
2123 % t
2125 )
2124 )
2126
2125
2127
2126
2128 timer = getattr(time, "perf_counter", None)
2127 timer = getattr(time, "perf_counter", None)
2129
2128
2130 if pycompat.iswindows:
2129 if pycompat.iswindows:
2131 checkosfilename = checkwinfilename
2130 checkosfilename = checkwinfilename
2132 if not timer:
2131 if not timer:
2133 timer = time.clock
2132 timer = time.clock
2134 else:
2133 else:
2135 # mercurial.windows doesn't have platform.checkosfilename
2134 # mercurial.windows doesn't have platform.checkosfilename
2136 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2135 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2137 if not timer:
2136 if not timer:
2138 timer = time.time
2137 timer = time.time
2139
2138
2140
2139
2141 def makelock(info, pathname):
2140 def makelock(info, pathname):
2142 """Create a lock file atomically if possible
2141 """Create a lock file atomically if possible
2143
2142
2144 This may leave a stale lock file if symlink isn't supported and signal
2143 This may leave a stale lock file if symlink isn't supported and signal
2145 interrupt is enabled.
2144 interrupt is enabled.
2146 """
2145 """
2147 try:
2146 try:
2148 return os.symlink(info, pathname)
2147 return os.symlink(info, pathname)
2149 except OSError as why:
2148 except OSError as why:
2150 if why.errno == errno.EEXIST:
2149 if why.errno == errno.EEXIST:
2151 raise
2150 raise
2152 except AttributeError: # no symlink in os
2151 except AttributeError: # no symlink in os
2153 pass
2152 pass
2154
2153
2155 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2154 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2156 ld = os.open(pathname, flags)
2155 ld = os.open(pathname, flags)
2157 os.write(ld, info)
2156 os.write(ld, info)
2158 os.close(ld)
2157 os.close(ld)
2159
2158
2160
2159
2161 def readlock(pathname):
2160 def readlock(pathname):
2162 # type: (bytes) -> bytes
2161 # type: (bytes) -> bytes
2163 try:
2162 try:
2164 return readlink(pathname)
2163 return readlink(pathname)
2165 except OSError as why:
2164 except OSError as why:
2166 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2165 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2167 raise
2166 raise
2168 except AttributeError: # no symlink in os
2167 except AttributeError: # no symlink in os
2169 pass
2168 pass
2170 with posixfile(pathname, b'rb') as fp:
2169 with posixfile(pathname, b'rb') as fp:
2171 return fp.read()
2170 return fp.read()
2172
2171
2173
2172
2174 def fstat(fp):
2173 def fstat(fp):
2175 '''stat file object that may not have fileno method.'''
2174 '''stat file object that may not have fileno method.'''
2176 try:
2175 try:
2177 return os.fstat(fp.fileno())
2176 return os.fstat(fp.fileno())
2178 except AttributeError:
2177 except AttributeError:
2179 return os.stat(fp.name)
2178 return os.stat(fp.name)
2180
2179
2181
2180
2182 # File system features
2181 # File system features
2183
2182
2184
2183
2185 def fscasesensitive(path):
2184 def fscasesensitive(path):
2186 # type: (bytes) -> bool
2185 # type: (bytes) -> bool
2187 """
2186 """
2188 Return true if the given path is on a case-sensitive filesystem
2187 Return true if the given path is on a case-sensitive filesystem
2189
2188
2190 Requires a path (like /foo/.hg) ending with a foldable final
2189 Requires a path (like /foo/.hg) ending with a foldable final
2191 directory component.
2190 directory component.
2192 """
2191 """
2193 s1 = os.lstat(path)
2192 s1 = os.lstat(path)
2194 d, b = os.path.split(path)
2193 d, b = os.path.split(path)
2195 b2 = b.upper()
2194 b2 = b.upper()
2196 if b == b2:
2195 if b == b2:
2197 b2 = b.lower()
2196 b2 = b.lower()
2198 if b == b2:
2197 if b == b2:
2199 return True # no evidence against case sensitivity
2198 return True # no evidence against case sensitivity
2200 p2 = os.path.join(d, b2)
2199 p2 = os.path.join(d, b2)
2201 try:
2200 try:
2202 s2 = os.lstat(p2)
2201 s2 = os.lstat(p2)
2203 if s2 == s1:
2202 if s2 == s1:
2204 return False
2203 return False
2205 return True
2204 return True
2206 except OSError:
2205 except OSError:
2207 return True
2206 return True
2208
2207
2209
2208
2210 _re2_input = lambda x: x
2209 _re2_input = lambda x: x
2211 try:
2210 try:
2212 import re2 # pytype: disable=import-error
2211 import re2 # pytype: disable=import-error
2213
2212
2214 _re2 = None
2213 _re2 = None
2215 except ImportError:
2214 except ImportError:
2216 _re2 = False
2215 _re2 = False
2217
2216
2218
2217
2219 class _re(object):
2218 class _re(object):
2220 def _checkre2(self):
2219 def _checkre2(self):
2221 global _re2
2220 global _re2
2222 global _re2_input
2221 global _re2_input
2223
2222
2224 check_pattern = br'\[([^\[]+)\]'
2223 check_pattern = br'\[([^\[]+)\]'
2225 check_input = b'[ui]'
2224 check_input = b'[ui]'
2226 try:
2225 try:
2227 # check if match works, see issue3964
2226 # check if match works, see issue3964
2228 _re2 = bool(re2.match(check_pattern, check_input))
2227 _re2 = bool(re2.match(check_pattern, check_input))
2229 except ImportError:
2228 except ImportError:
2230 _re2 = False
2229 _re2 = False
2231 except TypeError:
2230 except TypeError:
2232 # the `pyre-2` project provides a re2 module that accept bytes
2231 # the `pyre-2` project provides a re2 module that accept bytes
2233 # the `fb-re2` project provides a re2 module that acccept sysstr
2232 # the `fb-re2` project provides a re2 module that acccept sysstr
2234 check_pattern = pycompat.sysstr(check_pattern)
2233 check_pattern = pycompat.sysstr(check_pattern)
2235 check_input = pycompat.sysstr(check_input)
2234 check_input = pycompat.sysstr(check_input)
2236 _re2 = bool(re2.match(check_pattern, check_input))
2235 _re2 = bool(re2.match(check_pattern, check_input))
2237 _re2_input = pycompat.sysstr
2236 _re2_input = pycompat.sysstr
2238
2237
2239 def compile(self, pat, flags=0):
2238 def compile(self, pat, flags=0):
2240 """Compile a regular expression, using re2 if possible
2239 """Compile a regular expression, using re2 if possible
2241
2240
2242 For best performance, use only re2-compatible regexp features. The
2241 For best performance, use only re2-compatible regexp features. The
2243 only flags from the re module that are re2-compatible are
2242 only flags from the re module that are re2-compatible are
2244 IGNORECASE and MULTILINE."""
2243 IGNORECASE and MULTILINE."""
2245 if _re2 is None:
2244 if _re2 is None:
2246 self._checkre2()
2245 self._checkre2()
2247 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2246 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2248 if flags & remod.IGNORECASE:
2247 if flags & remod.IGNORECASE:
2249 pat = b'(?i)' + pat
2248 pat = b'(?i)' + pat
2250 if flags & remod.MULTILINE:
2249 if flags & remod.MULTILINE:
2251 pat = b'(?m)' + pat
2250 pat = b'(?m)' + pat
2252 try:
2251 try:
2253 return re2.compile(_re2_input(pat))
2252 return re2.compile(_re2_input(pat))
2254 except re2.error:
2253 except re2.error:
2255 pass
2254 pass
2256 return remod.compile(pat, flags)
2255 return remod.compile(pat, flags)
2257
2256
2258 @propertycache
2257 @propertycache
2259 def escape(self):
2258 def escape(self):
2260 """Return the version of escape corresponding to self.compile.
2259 """Return the version of escape corresponding to self.compile.
2261
2260
2262 This is imperfect because whether re2 or re is used for a particular
2261 This is imperfect because whether re2 or re is used for a particular
2263 function depends on the flags, etc, but it's the best we can do.
2262 function depends on the flags, etc, but it's the best we can do.
2264 """
2263 """
2265 global _re2
2264 global _re2
2266 if _re2 is None:
2265 if _re2 is None:
2267 self._checkre2()
2266 self._checkre2()
2268 if _re2:
2267 if _re2:
2269 return re2.escape
2268 return re2.escape
2270 else:
2269 else:
2271 return remod.escape
2270 return remod.escape
2272
2271
2273
2272
2274 re = _re()
2273 re = _re()
2275
2274
2276 _fspathcache = {}
2275 _fspathcache = {}
2277
2276
2278
2277
2279 def fspath(name, root):
2278 def fspath(name, root):
2280 # type: (bytes, bytes) -> bytes
2279 # type: (bytes, bytes) -> bytes
2281 """Get name in the case stored in the filesystem
2280 """Get name in the case stored in the filesystem
2282
2281
2283 The name should be relative to root, and be normcase-ed for efficiency.
2282 The name should be relative to root, and be normcase-ed for efficiency.
2284
2283
2285 Note that this function is unnecessary, and should not be
2284 Note that this function is unnecessary, and should not be
2286 called, for case-sensitive filesystems (simply because it's expensive).
2285 called, for case-sensitive filesystems (simply because it's expensive).
2287
2286
2288 The root should be normcase-ed, too.
2287 The root should be normcase-ed, too.
2289 """
2288 """
2290
2289
2291 def _makefspathcacheentry(dir):
2290 def _makefspathcacheentry(dir):
2292 return {normcase(n): n for n in os.listdir(dir)}
2291 return {normcase(n): n for n in os.listdir(dir)}
2293
2292
2294 seps = pycompat.ossep
2293 seps = pycompat.ossep
2295 if pycompat.osaltsep:
2294 if pycompat.osaltsep:
2296 seps = seps + pycompat.osaltsep
2295 seps = seps + pycompat.osaltsep
2297 # Protect backslashes. This gets silly very quickly.
2296 # Protect backslashes. This gets silly very quickly.
2298 seps.replace(b'\\', b'\\\\')
2297 seps.replace(b'\\', b'\\\\')
2299 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2298 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2300 dir = os.path.normpath(root)
2299 dir = os.path.normpath(root)
2301 result = []
2300 result = []
2302 for part, sep in pattern.findall(name):
2301 for part, sep in pattern.findall(name):
2303 if sep:
2302 if sep:
2304 result.append(sep)
2303 result.append(sep)
2305 continue
2304 continue
2306
2305
2307 if dir not in _fspathcache:
2306 if dir not in _fspathcache:
2308 _fspathcache[dir] = _makefspathcacheentry(dir)
2307 _fspathcache[dir] = _makefspathcacheentry(dir)
2309 contents = _fspathcache[dir]
2308 contents = _fspathcache[dir]
2310
2309
2311 found = contents.get(part)
2310 found = contents.get(part)
2312 if not found:
2311 if not found:
2313 # retry "once per directory" per "dirstate.walk" which
2312 # retry "once per directory" per "dirstate.walk" which
2314 # may take place for each patches of "hg qpush", for example
2313 # may take place for each patches of "hg qpush", for example
2315 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2314 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2316 found = contents.get(part)
2315 found = contents.get(part)
2317
2316
2318 result.append(found or part)
2317 result.append(found or part)
2319 dir = os.path.join(dir, part)
2318 dir = os.path.join(dir, part)
2320
2319
2321 return b''.join(result)
2320 return b''.join(result)
2322
2321
2323
2322
2324 def checknlink(testfile):
2323 def checknlink(testfile):
2325 # type: (bytes) -> bool
2324 # type: (bytes) -> bool
2326 '''check whether hardlink count reporting works properly'''
2325 '''check whether hardlink count reporting works properly'''
2327
2326
2328 # testfile may be open, so we need a separate file for checking to
2327 # testfile may be open, so we need a separate file for checking to
2329 # work around issue2543 (or testfile may get lost on Samba shares)
2328 # work around issue2543 (or testfile may get lost on Samba shares)
2330 f1, f2, fp = None, None, None
2329 f1, f2, fp = None, None, None
2331 try:
2330 try:
2332 fd, f1 = pycompat.mkstemp(
2331 fd, f1 = pycompat.mkstemp(
2333 prefix=b'.%s-' % os.path.basename(testfile),
2332 prefix=b'.%s-' % os.path.basename(testfile),
2334 suffix=b'1~',
2333 suffix=b'1~',
2335 dir=os.path.dirname(testfile),
2334 dir=os.path.dirname(testfile),
2336 )
2335 )
2337 os.close(fd)
2336 os.close(fd)
2338 f2 = b'%s2~' % f1[:-2]
2337 f2 = b'%s2~' % f1[:-2]
2339
2338
2340 oslink(f1, f2)
2339 oslink(f1, f2)
2341 # nlinks() may behave differently for files on Windows shares if
2340 # nlinks() may behave differently for files on Windows shares if
2342 # the file is open.
2341 # the file is open.
2343 fp = posixfile(f2)
2342 fp = posixfile(f2)
2344 return nlinks(f2) > 1
2343 return nlinks(f2) > 1
2345 except OSError:
2344 except OSError:
2346 return False
2345 return False
2347 finally:
2346 finally:
2348 if fp is not None:
2347 if fp is not None:
2349 fp.close()
2348 fp.close()
2350 for f in (f1, f2):
2349 for f in (f1, f2):
2351 try:
2350 try:
2352 if f is not None:
2351 if f is not None:
2353 os.unlink(f)
2352 os.unlink(f)
2354 except OSError:
2353 except OSError:
2355 pass
2354 pass
2356
2355
2357
2356
2358 def endswithsep(path):
2357 def endswithsep(path):
2359 # type: (bytes) -> bool
2358 # type: (bytes) -> bool
2360 '''Check path ends with os.sep or os.altsep.'''
2359 '''Check path ends with os.sep or os.altsep.'''
2361 return bool( # help pytype
2360 return bool( # help pytype
2362 path.endswith(pycompat.ossep)
2361 path.endswith(pycompat.ossep)
2363 or pycompat.osaltsep
2362 or pycompat.osaltsep
2364 and path.endswith(pycompat.osaltsep)
2363 and path.endswith(pycompat.osaltsep)
2365 )
2364 )
2366
2365
2367
2366
2368 def splitpath(path):
2367 def splitpath(path):
2369 # type: (bytes) -> List[bytes]
2368 # type: (bytes) -> List[bytes]
2370 """Split path by os.sep.
2369 """Split path by os.sep.
2371 Note that this function does not use os.altsep because this is
2370 Note that this function does not use os.altsep because this is
2372 an alternative of simple "xxx.split(os.sep)".
2371 an alternative of simple "xxx.split(os.sep)".
2373 It is recommended to use os.path.normpath() before using this
2372 It is recommended to use os.path.normpath() before using this
2374 function if need."""
2373 function if need."""
2375 return path.split(pycompat.ossep)
2374 return path.split(pycompat.ossep)
2376
2375
2377
2376
2378 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2377 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2379 """Create a temporary file with the same contents from name
2378 """Create a temporary file with the same contents from name
2380
2379
2381 The permission bits are copied from the original file.
2380 The permission bits are copied from the original file.
2382
2381
2383 If the temporary file is going to be truncated immediately, you
2382 If the temporary file is going to be truncated immediately, you
2384 can use emptyok=True as an optimization.
2383 can use emptyok=True as an optimization.
2385
2384
2386 Returns the name of the temporary file.
2385 Returns the name of the temporary file.
2387 """
2386 """
2388 d, fn = os.path.split(name)
2387 d, fn = os.path.split(name)
2389 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2388 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2390 os.close(fd)
2389 os.close(fd)
2391 # Temporary files are created with mode 0600, which is usually not
2390 # Temporary files are created with mode 0600, which is usually not
2392 # what we want. If the original file already exists, just copy
2391 # what we want. If the original file already exists, just copy
2393 # its mode. Otherwise, manually obey umask.
2392 # its mode. Otherwise, manually obey umask.
2394 copymode(name, temp, createmode, enforcewritable)
2393 copymode(name, temp, createmode, enforcewritable)
2395
2394
2396 if emptyok:
2395 if emptyok:
2397 return temp
2396 return temp
2398 try:
2397 try:
2399 try:
2398 try:
2400 ifp = posixfile(name, b"rb")
2399 ifp = posixfile(name, b"rb")
2401 except IOError as inst:
2400 except IOError as inst:
2402 if inst.errno == errno.ENOENT:
2401 if inst.errno == errno.ENOENT:
2403 return temp
2402 return temp
2404 if not getattr(inst, 'filename', None):
2403 if not getattr(inst, 'filename', None):
2405 inst.filename = name
2404 inst.filename = name
2406 raise
2405 raise
2407 ofp = posixfile(temp, b"wb")
2406 ofp = posixfile(temp, b"wb")
2408 for chunk in filechunkiter(ifp):
2407 for chunk in filechunkiter(ifp):
2409 ofp.write(chunk)
2408 ofp.write(chunk)
2410 ifp.close()
2409 ifp.close()
2411 ofp.close()
2410 ofp.close()
2412 except: # re-raises
2411 except: # re-raises
2413 try:
2412 try:
2414 os.unlink(temp)
2413 os.unlink(temp)
2415 except OSError:
2414 except OSError:
2416 pass
2415 pass
2417 raise
2416 raise
2418 return temp
2417 return temp
2419
2418
2420
2419
2421 class filestat(object):
2420 class filestat(object):
2422 """help to exactly detect change of a file
2421 """help to exactly detect change of a file
2423
2422
2424 'stat' attribute is result of 'os.stat()' if specified 'path'
2423 'stat' attribute is result of 'os.stat()' if specified 'path'
2425 exists. Otherwise, it is None. This can avoid preparative
2424 exists. Otherwise, it is None. This can avoid preparative
2426 'exists()' examination on client side of this class.
2425 'exists()' examination on client side of this class.
2427 """
2426 """
2428
2427
2429 def __init__(self, stat):
2428 def __init__(self, stat):
2430 self.stat = stat
2429 self.stat = stat
2431
2430
2432 @classmethod
2431 @classmethod
2433 def frompath(cls, path):
2432 def frompath(cls, path):
2434 try:
2433 try:
2435 stat = os.stat(path)
2434 stat = os.stat(path)
2436 except OSError as err:
2435 except OSError as err:
2437 if err.errno != errno.ENOENT:
2436 if err.errno != errno.ENOENT:
2438 raise
2437 raise
2439 stat = None
2438 stat = None
2440 return cls(stat)
2439 return cls(stat)
2441
2440
2442 @classmethod
2441 @classmethod
2443 def fromfp(cls, fp):
2442 def fromfp(cls, fp):
2444 stat = os.fstat(fp.fileno())
2443 stat = os.fstat(fp.fileno())
2445 return cls(stat)
2444 return cls(stat)
2446
2445
2447 __hash__ = object.__hash__
2446 __hash__ = object.__hash__
2448
2447
2449 def __eq__(self, old):
2448 def __eq__(self, old):
2450 try:
2449 try:
2451 # if ambiguity between stat of new and old file is
2450 # if ambiguity between stat of new and old file is
2452 # avoided, comparison of size, ctime and mtime is enough
2451 # avoided, comparison of size, ctime and mtime is enough
2453 # to exactly detect change of a file regardless of platform
2452 # to exactly detect change of a file regardless of platform
2454 return (
2453 return (
2455 self.stat.st_size == old.stat.st_size
2454 self.stat.st_size == old.stat.st_size
2456 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2455 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2457 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2456 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2458 )
2457 )
2459 except AttributeError:
2458 except AttributeError:
2460 pass
2459 pass
2461 try:
2460 try:
2462 return self.stat is None and old.stat is None
2461 return self.stat is None and old.stat is None
2463 except AttributeError:
2462 except AttributeError:
2464 return False
2463 return False
2465
2464
2466 def isambig(self, old):
2465 def isambig(self, old):
2467 """Examine whether new (= self) stat is ambiguous against old one
2466 """Examine whether new (= self) stat is ambiguous against old one
2468
2467
2469 "S[N]" below means stat of a file at N-th change:
2468 "S[N]" below means stat of a file at N-th change:
2470
2469
2471 - S[n-1].ctime < S[n].ctime: can detect change of a file
2470 - S[n-1].ctime < S[n].ctime: can detect change of a file
2472 - S[n-1].ctime == S[n].ctime
2471 - S[n-1].ctime == S[n].ctime
2473 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2472 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2474 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2473 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2475 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2474 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2476 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2475 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2477
2476
2478 Case (*2) above means that a file was changed twice or more at
2477 Case (*2) above means that a file was changed twice or more at
2479 same time in sec (= S[n-1].ctime), and comparison of timestamp
2478 same time in sec (= S[n-1].ctime), and comparison of timestamp
2480 is ambiguous.
2479 is ambiguous.
2481
2480
2482 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2481 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2483 timestamp is ambiguous".
2482 timestamp is ambiguous".
2484
2483
2485 But advancing mtime only in case (*2) doesn't work as
2484 But advancing mtime only in case (*2) doesn't work as
2486 expected, because naturally advanced S[n].mtime in case (*1)
2485 expected, because naturally advanced S[n].mtime in case (*1)
2487 might be equal to manually advanced S[n-1 or earlier].mtime.
2486 might be equal to manually advanced S[n-1 or earlier].mtime.
2488
2487
2489 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2488 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2490 treated as ambiguous regardless of mtime, to avoid overlooking
2489 treated as ambiguous regardless of mtime, to avoid overlooking
2491 by confliction between such mtime.
2490 by confliction between such mtime.
2492
2491
2493 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2492 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2494 S[n].mtime", even if size of a file isn't changed.
2493 S[n].mtime", even if size of a file isn't changed.
2495 """
2494 """
2496 try:
2495 try:
2497 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2496 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2498 except AttributeError:
2497 except AttributeError:
2499 return False
2498 return False
2500
2499
2501 def avoidambig(self, path, old):
2500 def avoidambig(self, path, old):
2502 """Change file stat of specified path to avoid ambiguity
2501 """Change file stat of specified path to avoid ambiguity
2503
2502
2504 'old' should be previous filestat of 'path'.
2503 'old' should be previous filestat of 'path'.
2505
2504
2506 This skips avoiding ambiguity, if a process doesn't have
2505 This skips avoiding ambiguity, if a process doesn't have
2507 appropriate privileges for 'path'. This returns False in this
2506 appropriate privileges for 'path'. This returns False in this
2508 case.
2507 case.
2509
2508
2510 Otherwise, this returns True, as "ambiguity is avoided".
2509 Otherwise, this returns True, as "ambiguity is avoided".
2511 """
2510 """
2512 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2511 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2513 try:
2512 try:
2514 os.utime(path, (advanced, advanced))
2513 os.utime(path, (advanced, advanced))
2515 except OSError as inst:
2514 except OSError as inst:
2516 if inst.errno == errno.EPERM:
2515 if inst.errno == errno.EPERM:
2517 # utime() on the file created by another user causes EPERM,
2516 # utime() on the file created by another user causes EPERM,
2518 # if a process doesn't have appropriate privileges
2517 # if a process doesn't have appropriate privileges
2519 return False
2518 return False
2520 raise
2519 raise
2521 return True
2520 return True
2522
2521
2523 def __ne__(self, other):
2522 def __ne__(self, other):
2524 return not self == other
2523 return not self == other
2525
2524
2526
2525
2527 class atomictempfile(object):
2526 class atomictempfile(object):
2528 """writable file object that atomically updates a file
2527 """writable file object that atomically updates a file
2529
2528
2530 All writes will go to a temporary copy of the original file. Call
2529 All writes will go to a temporary copy of the original file. Call
2531 close() when you are done writing, and atomictempfile will rename
2530 close() when you are done writing, and atomictempfile will rename
2532 the temporary copy to the original name, making the changes
2531 the temporary copy to the original name, making the changes
2533 visible. If the object is destroyed without being closed, all your
2532 visible. If the object is destroyed without being closed, all your
2534 writes are discarded.
2533 writes are discarded.
2535
2534
2536 checkambig argument of constructor is used with filestat, and is
2535 checkambig argument of constructor is used with filestat, and is
2537 useful only if target file is guarded by any lock (e.g. repo.lock
2536 useful only if target file is guarded by any lock (e.g. repo.lock
2538 or repo.wlock).
2537 or repo.wlock).
2539 """
2538 """
2540
2539
2541 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2540 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2542 self.__name = name # permanent name
2541 self.__name = name # permanent name
2543 self._tempname = mktempcopy(
2542 self._tempname = mktempcopy(
2544 name,
2543 name,
2545 emptyok=(b'w' in mode),
2544 emptyok=(b'w' in mode),
2546 createmode=createmode,
2545 createmode=createmode,
2547 enforcewritable=(b'w' in mode),
2546 enforcewritable=(b'w' in mode),
2548 )
2547 )
2549
2548
2550 self._fp = posixfile(self._tempname, mode)
2549 self._fp = posixfile(self._tempname, mode)
2551 self._checkambig = checkambig
2550 self._checkambig = checkambig
2552
2551
2553 # delegated methods
2552 # delegated methods
2554 self.read = self._fp.read
2553 self.read = self._fp.read
2555 self.write = self._fp.write
2554 self.write = self._fp.write
2556 self.seek = self._fp.seek
2555 self.seek = self._fp.seek
2557 self.tell = self._fp.tell
2556 self.tell = self._fp.tell
2558 self.fileno = self._fp.fileno
2557 self.fileno = self._fp.fileno
2559
2558
2560 def close(self):
2559 def close(self):
2561 if not self._fp.closed:
2560 if not self._fp.closed:
2562 self._fp.close()
2561 self._fp.close()
2563 filename = localpath(self.__name)
2562 filename = localpath(self.__name)
2564 oldstat = self._checkambig and filestat.frompath(filename)
2563 oldstat = self._checkambig and filestat.frompath(filename)
2565 if oldstat and oldstat.stat:
2564 if oldstat and oldstat.stat:
2566 rename(self._tempname, filename)
2565 rename(self._tempname, filename)
2567 newstat = filestat.frompath(filename)
2566 newstat = filestat.frompath(filename)
2568 if newstat.isambig(oldstat):
2567 if newstat.isambig(oldstat):
2569 # stat of changed file is ambiguous to original one
2568 # stat of changed file is ambiguous to original one
2570 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2569 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2571 os.utime(filename, (advanced, advanced))
2570 os.utime(filename, (advanced, advanced))
2572 else:
2571 else:
2573 rename(self._tempname, filename)
2572 rename(self._tempname, filename)
2574
2573
2575 def discard(self):
2574 def discard(self):
2576 if not self._fp.closed:
2575 if not self._fp.closed:
2577 try:
2576 try:
2578 os.unlink(self._tempname)
2577 os.unlink(self._tempname)
2579 except OSError:
2578 except OSError:
2580 pass
2579 pass
2581 self._fp.close()
2580 self._fp.close()
2582
2581
2583 def __del__(self):
2582 def __del__(self):
2584 if safehasattr(self, '_fp'): # constructor actually did something
2583 if safehasattr(self, '_fp'): # constructor actually did something
2585 self.discard()
2584 self.discard()
2586
2585
2587 def __enter__(self):
2586 def __enter__(self):
2588 return self
2587 return self
2589
2588
2590 def __exit__(self, exctype, excvalue, traceback):
2589 def __exit__(self, exctype, excvalue, traceback):
2591 if exctype is not None:
2590 if exctype is not None:
2592 self.discard()
2591 self.discard()
2593 else:
2592 else:
2594 self.close()
2593 self.close()
2595
2594
2596
2595
2597 def unlinkpath(f, ignoremissing=False, rmdir=True):
2596 def unlinkpath(f, ignoremissing=False, rmdir=True):
2598 # type: (bytes, bool, bool) -> None
2597 # type: (bytes, bool, bool) -> None
2599 """unlink and remove the directory if it is empty"""
2598 """unlink and remove the directory if it is empty"""
2600 if ignoremissing:
2599 if ignoremissing:
2601 tryunlink(f)
2600 tryunlink(f)
2602 else:
2601 else:
2603 unlink(f)
2602 unlink(f)
2604 if rmdir:
2603 if rmdir:
2605 # try removing directories that might now be empty
2604 # try removing directories that might now be empty
2606 try:
2605 try:
2607 removedirs(os.path.dirname(f))
2606 removedirs(os.path.dirname(f))
2608 except OSError:
2607 except OSError:
2609 pass
2608 pass
2610
2609
2611
2610
2612 def tryunlink(f):
2611 def tryunlink(f):
2613 # type: (bytes) -> None
2612 # type: (bytes) -> None
2614 """Attempt to remove a file, ignoring ENOENT errors."""
2613 """Attempt to remove a file, ignoring ENOENT errors."""
2615 try:
2614 try:
2616 unlink(f)
2615 unlink(f)
2617 except OSError as e:
2616 except OSError as e:
2618 if e.errno != errno.ENOENT:
2617 if e.errno != errno.ENOENT:
2619 raise
2618 raise
2620
2619
2621
2620
2622 def makedirs(name, mode=None, notindexed=False):
2621 def makedirs(name, mode=None, notindexed=False):
2623 # type: (bytes, Optional[int], bool) -> None
2622 # type: (bytes, Optional[int], bool) -> None
2624 """recursive directory creation with parent mode inheritance
2623 """recursive directory creation with parent mode inheritance
2625
2624
2626 Newly created directories are marked as "not to be indexed by
2625 Newly created directories are marked as "not to be indexed by
2627 the content indexing service", if ``notindexed`` is specified
2626 the content indexing service", if ``notindexed`` is specified
2628 for "write" mode access.
2627 for "write" mode access.
2629 """
2628 """
2630 try:
2629 try:
2631 makedir(name, notindexed)
2630 makedir(name, notindexed)
2632 except OSError as err:
2631 except OSError as err:
2633 if err.errno == errno.EEXIST:
2632 if err.errno == errno.EEXIST:
2634 return
2633 return
2635 if err.errno != errno.ENOENT or not name:
2634 if err.errno != errno.ENOENT or not name:
2636 raise
2635 raise
2637 parent = os.path.dirname(abspath(name))
2636 parent = os.path.dirname(abspath(name))
2638 if parent == name:
2637 if parent == name:
2639 raise
2638 raise
2640 makedirs(parent, mode, notindexed)
2639 makedirs(parent, mode, notindexed)
2641 try:
2640 try:
2642 makedir(name, notindexed)
2641 makedir(name, notindexed)
2643 except OSError as err:
2642 except OSError as err:
2644 # Catch EEXIST to handle races
2643 # Catch EEXIST to handle races
2645 if err.errno == errno.EEXIST:
2644 if err.errno == errno.EEXIST:
2646 return
2645 return
2647 raise
2646 raise
2648 if mode is not None:
2647 if mode is not None:
2649 os.chmod(name, mode)
2648 os.chmod(name, mode)
2650
2649
2651
2650
2652 def readfile(path):
2651 def readfile(path):
2653 # type: (bytes) -> bytes
2652 # type: (bytes) -> bytes
2654 with open(path, b'rb') as fp:
2653 with open(path, b'rb') as fp:
2655 return fp.read()
2654 return fp.read()
2656
2655
2657
2656
2658 def writefile(path, text):
2657 def writefile(path, text):
2659 # type: (bytes, bytes) -> None
2658 # type: (bytes, bytes) -> None
2660 with open(path, b'wb') as fp:
2659 with open(path, b'wb') as fp:
2661 fp.write(text)
2660 fp.write(text)
2662
2661
2663
2662
2664 def appendfile(path, text):
2663 def appendfile(path, text):
2665 # type: (bytes, bytes) -> None
2664 # type: (bytes, bytes) -> None
2666 with open(path, b'ab') as fp:
2665 with open(path, b'ab') as fp:
2667 fp.write(text)
2666 fp.write(text)
2668
2667
2669
2668
2670 class chunkbuffer(object):
2669 class chunkbuffer(object):
2671 """Allow arbitrary sized chunks of data to be efficiently read from an
2670 """Allow arbitrary sized chunks of data to be efficiently read from an
2672 iterator over chunks of arbitrary size."""
2671 iterator over chunks of arbitrary size."""
2673
2672
2674 def __init__(self, in_iter):
2673 def __init__(self, in_iter):
2675 """in_iter is the iterator that's iterating over the input chunks."""
2674 """in_iter is the iterator that's iterating over the input chunks."""
2676
2675
2677 def splitbig(chunks):
2676 def splitbig(chunks):
2678 for chunk in chunks:
2677 for chunk in chunks:
2679 if len(chunk) > 2 ** 20:
2678 if len(chunk) > 2 ** 20:
2680 pos = 0
2679 pos = 0
2681 while pos < len(chunk):
2680 while pos < len(chunk):
2682 end = pos + 2 ** 18
2681 end = pos + 2 ** 18
2683 yield chunk[pos:end]
2682 yield chunk[pos:end]
2684 pos = end
2683 pos = end
2685 else:
2684 else:
2686 yield chunk
2685 yield chunk
2687
2686
2688 self.iter = splitbig(in_iter)
2687 self.iter = splitbig(in_iter)
2689 self._queue = collections.deque()
2688 self._queue = collections.deque()
2690 self._chunkoffset = 0
2689 self._chunkoffset = 0
2691
2690
2692 def read(self, l=None):
2691 def read(self, l=None):
2693 """Read L bytes of data from the iterator of chunks of data.
2692 """Read L bytes of data from the iterator of chunks of data.
2694 Returns less than L bytes if the iterator runs dry.
2693 Returns less than L bytes if the iterator runs dry.
2695
2694
2696 If size parameter is omitted, read everything"""
2695 If size parameter is omitted, read everything"""
2697 if l is None:
2696 if l is None:
2698 return b''.join(self.iter)
2697 return b''.join(self.iter)
2699
2698
2700 left = l
2699 left = l
2701 buf = []
2700 buf = []
2702 queue = self._queue
2701 queue = self._queue
2703 while left > 0:
2702 while left > 0:
2704 # refill the queue
2703 # refill the queue
2705 if not queue:
2704 if not queue:
2706 target = 2 ** 18
2705 target = 2 ** 18
2707 for chunk in self.iter:
2706 for chunk in self.iter:
2708 queue.append(chunk)
2707 queue.append(chunk)
2709 target -= len(chunk)
2708 target -= len(chunk)
2710 if target <= 0:
2709 if target <= 0:
2711 break
2710 break
2712 if not queue:
2711 if not queue:
2713 break
2712 break
2714
2713
2715 # The easy way to do this would be to queue.popleft(), modify the
2714 # The easy way to do this would be to queue.popleft(), modify the
2716 # chunk (if necessary), then queue.appendleft(). However, for cases
2715 # chunk (if necessary), then queue.appendleft(). However, for cases
2717 # where we read partial chunk content, this incurs 2 dequeue
2716 # where we read partial chunk content, this incurs 2 dequeue
2718 # mutations and creates a new str for the remaining chunk in the
2717 # mutations and creates a new str for the remaining chunk in the
2719 # queue. Our code below avoids this overhead.
2718 # queue. Our code below avoids this overhead.
2720
2719
2721 chunk = queue[0]
2720 chunk = queue[0]
2722 chunkl = len(chunk)
2721 chunkl = len(chunk)
2723 offset = self._chunkoffset
2722 offset = self._chunkoffset
2724
2723
2725 # Use full chunk.
2724 # Use full chunk.
2726 if offset == 0 and left >= chunkl:
2725 if offset == 0 and left >= chunkl:
2727 left -= chunkl
2726 left -= chunkl
2728 queue.popleft()
2727 queue.popleft()
2729 buf.append(chunk)
2728 buf.append(chunk)
2730 # self._chunkoffset remains at 0.
2729 # self._chunkoffset remains at 0.
2731 continue
2730 continue
2732
2731
2733 chunkremaining = chunkl - offset
2732 chunkremaining = chunkl - offset
2734
2733
2735 # Use all of unconsumed part of chunk.
2734 # Use all of unconsumed part of chunk.
2736 if left >= chunkremaining:
2735 if left >= chunkremaining:
2737 left -= chunkremaining
2736 left -= chunkremaining
2738 queue.popleft()
2737 queue.popleft()
2739 # offset == 0 is enabled by block above, so this won't merely
2738 # offset == 0 is enabled by block above, so this won't merely
2740 # copy via ``chunk[0:]``.
2739 # copy via ``chunk[0:]``.
2741 buf.append(chunk[offset:])
2740 buf.append(chunk[offset:])
2742 self._chunkoffset = 0
2741 self._chunkoffset = 0
2743
2742
2744 # Partial chunk needed.
2743 # Partial chunk needed.
2745 else:
2744 else:
2746 buf.append(chunk[offset : offset + left])
2745 buf.append(chunk[offset : offset + left])
2747 self._chunkoffset += left
2746 self._chunkoffset += left
2748 left -= chunkremaining
2747 left -= chunkremaining
2749
2748
2750 return b''.join(buf)
2749 return b''.join(buf)
2751
2750
2752
2751
2753 def filechunkiter(f, size=131072, limit=None):
2752 def filechunkiter(f, size=131072, limit=None):
2754 """Create a generator that produces the data in the file size
2753 """Create a generator that produces the data in the file size
2755 (default 131072) bytes at a time, up to optional limit (default is
2754 (default 131072) bytes at a time, up to optional limit (default is
2756 to read all data). Chunks may be less than size bytes if the
2755 to read all data). Chunks may be less than size bytes if the
2757 chunk is the last chunk in the file, or the file is a socket or
2756 chunk is the last chunk in the file, or the file is a socket or
2758 some other type of file that sometimes reads less data than is
2757 some other type of file that sometimes reads less data than is
2759 requested."""
2758 requested."""
2760 assert size >= 0
2759 assert size >= 0
2761 assert limit is None or limit >= 0
2760 assert limit is None or limit >= 0
2762 while True:
2761 while True:
2763 if limit is None:
2762 if limit is None:
2764 nbytes = size
2763 nbytes = size
2765 else:
2764 else:
2766 nbytes = min(limit, size)
2765 nbytes = min(limit, size)
2767 s = nbytes and f.read(nbytes)
2766 s = nbytes and f.read(nbytes)
2768 if not s:
2767 if not s:
2769 break
2768 break
2770 if limit:
2769 if limit:
2771 limit -= len(s)
2770 limit -= len(s)
2772 yield s
2771 yield s
2773
2772
2774
2773
2775 class cappedreader(object):
2774 class cappedreader(object):
2776 """A file object proxy that allows reading up to N bytes.
2775 """A file object proxy that allows reading up to N bytes.
2777
2776
2778 Given a source file object, instances of this type allow reading up to
2777 Given a source file object, instances of this type allow reading up to
2779 N bytes from that source file object. Attempts to read past the allowed
2778 N bytes from that source file object. Attempts to read past the allowed
2780 limit are treated as EOF.
2779 limit are treated as EOF.
2781
2780
2782 It is assumed that I/O is not performed on the original file object
2781 It is assumed that I/O is not performed on the original file object
2783 in addition to I/O that is performed by this instance. If there is,
2782 in addition to I/O that is performed by this instance. If there is,
2784 state tracking will get out of sync and unexpected results will ensue.
2783 state tracking will get out of sync and unexpected results will ensue.
2785 """
2784 """
2786
2785
2787 def __init__(self, fh, limit):
2786 def __init__(self, fh, limit):
2788 """Allow reading up to <limit> bytes from <fh>."""
2787 """Allow reading up to <limit> bytes from <fh>."""
2789 self._fh = fh
2788 self._fh = fh
2790 self._left = limit
2789 self._left = limit
2791
2790
2792 def read(self, n=-1):
2791 def read(self, n=-1):
2793 if not self._left:
2792 if not self._left:
2794 return b''
2793 return b''
2795
2794
2796 if n < 0:
2795 if n < 0:
2797 n = self._left
2796 n = self._left
2798
2797
2799 data = self._fh.read(min(n, self._left))
2798 data = self._fh.read(min(n, self._left))
2800 self._left -= len(data)
2799 self._left -= len(data)
2801 assert self._left >= 0
2800 assert self._left >= 0
2802
2801
2803 return data
2802 return data
2804
2803
2805 def readinto(self, b):
2804 def readinto(self, b):
2806 res = self.read(len(b))
2805 res = self.read(len(b))
2807 if res is None:
2806 if res is None:
2808 return None
2807 return None
2809
2808
2810 b[0 : len(res)] = res
2809 b[0 : len(res)] = res
2811 return len(res)
2810 return len(res)
2812
2811
2813
2812
2814 def unitcountfn(*unittable):
2813 def unitcountfn(*unittable):
2815 '''return a function that renders a readable count of some quantity'''
2814 '''return a function that renders a readable count of some quantity'''
2816
2815
2817 def go(count):
2816 def go(count):
2818 for multiplier, divisor, format in unittable:
2817 for multiplier, divisor, format in unittable:
2819 if abs(count) >= divisor * multiplier:
2818 if abs(count) >= divisor * multiplier:
2820 return format % (count / float(divisor))
2819 return format % (count / float(divisor))
2821 return unittable[-1][2] % count
2820 return unittable[-1][2] % count
2822
2821
2823 return go
2822 return go
2824
2823
2825
2824
2826 def processlinerange(fromline, toline):
2825 def processlinerange(fromline, toline):
2827 # type: (int, int) -> Tuple[int, int]
2826 # type: (int, int) -> Tuple[int, int]
2828 """Check that linerange <fromline>:<toline> makes sense and return a
2827 """Check that linerange <fromline>:<toline> makes sense and return a
2829 0-based range.
2828 0-based range.
2830
2829
2831 >>> processlinerange(10, 20)
2830 >>> processlinerange(10, 20)
2832 (9, 20)
2831 (9, 20)
2833 >>> processlinerange(2, 1)
2832 >>> processlinerange(2, 1)
2834 Traceback (most recent call last):
2833 Traceback (most recent call last):
2835 ...
2834 ...
2836 ParseError: line range must be positive
2835 ParseError: line range must be positive
2837 >>> processlinerange(0, 5)
2836 >>> processlinerange(0, 5)
2838 Traceback (most recent call last):
2837 Traceback (most recent call last):
2839 ...
2838 ...
2840 ParseError: fromline must be strictly positive
2839 ParseError: fromline must be strictly positive
2841 """
2840 """
2842 if toline - fromline < 0:
2841 if toline - fromline < 0:
2843 raise error.ParseError(_(b"line range must be positive"))
2842 raise error.ParseError(_(b"line range must be positive"))
2844 if fromline < 1:
2843 if fromline < 1:
2845 raise error.ParseError(_(b"fromline must be strictly positive"))
2844 raise error.ParseError(_(b"fromline must be strictly positive"))
2846 return fromline - 1, toline
2845 return fromline - 1, toline
2847
2846
2848
2847
2849 bytecount = unitcountfn(
2848 bytecount = unitcountfn(
2850 (100, 1 << 30, _(b'%.0f GB')),
2849 (100, 1 << 30, _(b'%.0f GB')),
2851 (10, 1 << 30, _(b'%.1f GB')),
2850 (10, 1 << 30, _(b'%.1f GB')),
2852 (1, 1 << 30, _(b'%.2f GB')),
2851 (1, 1 << 30, _(b'%.2f GB')),
2853 (100, 1 << 20, _(b'%.0f MB')),
2852 (100, 1 << 20, _(b'%.0f MB')),
2854 (10, 1 << 20, _(b'%.1f MB')),
2853 (10, 1 << 20, _(b'%.1f MB')),
2855 (1, 1 << 20, _(b'%.2f MB')),
2854 (1, 1 << 20, _(b'%.2f MB')),
2856 (100, 1 << 10, _(b'%.0f KB')),
2855 (100, 1 << 10, _(b'%.0f KB')),
2857 (10, 1 << 10, _(b'%.1f KB')),
2856 (10, 1 << 10, _(b'%.1f KB')),
2858 (1, 1 << 10, _(b'%.2f KB')),
2857 (1, 1 << 10, _(b'%.2f KB')),
2859 (1, 1, _(b'%.0f bytes')),
2858 (1, 1, _(b'%.0f bytes')),
2860 )
2859 )
2861
2860
2862
2861
2863 class transformingwriter(object):
2862 class transformingwriter(object):
2864 """Writable file wrapper to transform data by function"""
2863 """Writable file wrapper to transform data by function"""
2865
2864
2866 def __init__(self, fp, encode):
2865 def __init__(self, fp, encode):
2867 self._fp = fp
2866 self._fp = fp
2868 self._encode = encode
2867 self._encode = encode
2869
2868
2870 def close(self):
2869 def close(self):
2871 self._fp.close()
2870 self._fp.close()
2872
2871
2873 def flush(self):
2872 def flush(self):
2874 self._fp.flush()
2873 self._fp.flush()
2875
2874
2876 def write(self, data):
2875 def write(self, data):
2877 return self._fp.write(self._encode(data))
2876 return self._fp.write(self._encode(data))
2878
2877
2879
2878
2880 # Matches a single EOL which can either be a CRLF where repeated CR
2879 # Matches a single EOL which can either be a CRLF where repeated CR
2881 # are removed or a LF. We do not care about old Macintosh files, so a
2880 # are removed or a LF. We do not care about old Macintosh files, so a
2882 # stray CR is an error.
2881 # stray CR is an error.
2883 _eolre = remod.compile(br'\r*\n')
2882 _eolre = remod.compile(br'\r*\n')
2884
2883
2885
2884
2886 def tolf(s):
2885 def tolf(s):
2887 # type: (bytes) -> bytes
2886 # type: (bytes) -> bytes
2888 return _eolre.sub(b'\n', s)
2887 return _eolre.sub(b'\n', s)
2889
2888
2890
2889
2891 def tocrlf(s):
2890 def tocrlf(s):
2892 # type: (bytes) -> bytes
2891 # type: (bytes) -> bytes
2893 return _eolre.sub(b'\r\n', s)
2892 return _eolre.sub(b'\r\n', s)
2894
2893
2895
2894
2896 def _crlfwriter(fp):
2895 def _crlfwriter(fp):
2897 return transformingwriter(fp, tocrlf)
2896 return transformingwriter(fp, tocrlf)
2898
2897
2899
2898
2900 if pycompat.oslinesep == b'\r\n':
2899 if pycompat.oslinesep == b'\r\n':
2901 tonativeeol = tocrlf
2900 tonativeeol = tocrlf
2902 fromnativeeol = tolf
2901 fromnativeeol = tolf
2903 nativeeolwriter = _crlfwriter
2902 nativeeolwriter = _crlfwriter
2904 else:
2903 else:
2905 tonativeeol = pycompat.identity
2904 tonativeeol = pycompat.identity
2906 fromnativeeol = pycompat.identity
2905 fromnativeeol = pycompat.identity
2907 nativeeolwriter = pycompat.identity
2906 nativeeolwriter = pycompat.identity
2908
2907
2909 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2908 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2910 3,
2909 3,
2911 0,
2910 0,
2912 ):
2911 ):
2913 # There is an issue in CPython that some IO methods do not handle EINTR
2912 # There is an issue in CPython that some IO methods do not handle EINTR
2914 # correctly. The following table shows what CPython version (and functions)
2913 # correctly. The following table shows what CPython version (and functions)
2915 # are affected (buggy: has the EINTR bug, okay: otherwise):
2914 # are affected (buggy: has the EINTR bug, okay: otherwise):
2916 #
2915 #
2917 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2916 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2918 # --------------------------------------------------
2917 # --------------------------------------------------
2919 # fp.__iter__ | buggy | buggy | okay
2918 # fp.__iter__ | buggy | buggy | okay
2920 # fp.read* | buggy | okay [1] | okay
2919 # fp.read* | buggy | okay [1] | okay
2921 #
2920 #
2922 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2921 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2923 #
2922 #
2924 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2923 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2925 # like "read*" work fine, as we do not support Python < 2.7.4.
2924 # like "read*" work fine, as we do not support Python < 2.7.4.
2926 #
2925 #
2927 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2926 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2928 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2927 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2929 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2928 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2930 # fp.__iter__ but not other fp.read* methods.
2929 # fp.__iter__ but not other fp.read* methods.
2931 #
2930 #
2932 # On modern systems like Linux, the "read" syscall cannot be interrupted
2931 # On modern systems like Linux, the "read" syscall cannot be interrupted
2933 # when reading "fast" files like on-disk files. So the EINTR issue only
2932 # when reading "fast" files like on-disk files. So the EINTR issue only
2934 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2933 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2935 # files approximately as "fast" files and use the fast (unsafe) code path,
2934 # files approximately as "fast" files and use the fast (unsafe) code path,
2936 # to minimize the performance impact.
2935 # to minimize the performance impact.
2937
2936
2938 def iterfile(fp):
2937 def iterfile(fp):
2939 fastpath = True
2938 fastpath = True
2940 if type(fp) is file:
2939 if type(fp) is file:
2941 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2940 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2942 if fastpath:
2941 if fastpath:
2943 return fp
2942 return fp
2944 else:
2943 else:
2945 # fp.readline deals with EINTR correctly, use it as a workaround.
2944 # fp.readline deals with EINTR correctly, use it as a workaround.
2946 return iter(fp.readline, b'')
2945 return iter(fp.readline, b'')
2947
2946
2948
2947
2949 else:
2948 else:
2950 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2949 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2951 def iterfile(fp):
2950 def iterfile(fp):
2952 return fp
2951 return fp
2953
2952
2954
2953
2955 def iterlines(iterator):
2954 def iterlines(iterator):
2956 # type: (Iterator[bytes]) -> Iterator[bytes]
2955 # type: (Iterator[bytes]) -> Iterator[bytes]
2957 for chunk in iterator:
2956 for chunk in iterator:
2958 for line in chunk.splitlines():
2957 for line in chunk.splitlines():
2959 yield line
2958 yield line
2960
2959
2961
2960
2962 def expandpath(path):
2961 def expandpath(path):
2963 # type: (bytes) -> bytes
2962 # type: (bytes) -> bytes
2964 return os.path.expanduser(os.path.expandvars(path))
2963 return os.path.expanduser(os.path.expandvars(path))
2965
2964
2966
2965
2967 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2966 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2968 """Return the result of interpolating items in the mapping into string s.
2967 """Return the result of interpolating items in the mapping into string s.
2969
2968
2970 prefix is a single character string, or a two character string with
2969 prefix is a single character string, or a two character string with
2971 a backslash as the first character if the prefix needs to be escaped in
2970 a backslash as the first character if the prefix needs to be escaped in
2972 a regular expression.
2971 a regular expression.
2973
2972
2974 fn is an optional function that will be applied to the replacement text
2973 fn is an optional function that will be applied to the replacement text
2975 just before replacement.
2974 just before replacement.
2976
2975
2977 escape_prefix is an optional flag that allows using doubled prefix for
2976 escape_prefix is an optional flag that allows using doubled prefix for
2978 its escaping.
2977 its escaping.
2979 """
2978 """
2980 fn = fn or (lambda s: s)
2979 fn = fn or (lambda s: s)
2981 patterns = b'|'.join(mapping.keys())
2980 patterns = b'|'.join(mapping.keys())
2982 if escape_prefix:
2981 if escape_prefix:
2983 patterns += b'|' + prefix
2982 patterns += b'|' + prefix
2984 if len(prefix) > 1:
2983 if len(prefix) > 1:
2985 prefix_char = prefix[1:]
2984 prefix_char = prefix[1:]
2986 else:
2985 else:
2987 prefix_char = prefix
2986 prefix_char = prefix
2988 mapping[prefix_char] = prefix_char
2987 mapping[prefix_char] = prefix_char
2989 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2988 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2990 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2989 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2991
2990
2992
2991
2993 timecount = unitcountfn(
2992 timecount = unitcountfn(
2994 (1, 1e3, _(b'%.0f s')),
2993 (1, 1e3, _(b'%.0f s')),
2995 (100, 1, _(b'%.1f s')),
2994 (100, 1, _(b'%.1f s')),
2996 (10, 1, _(b'%.2f s')),
2995 (10, 1, _(b'%.2f s')),
2997 (1, 1, _(b'%.3f s')),
2996 (1, 1, _(b'%.3f s')),
2998 (100, 0.001, _(b'%.1f ms')),
2997 (100, 0.001, _(b'%.1f ms')),
2999 (10, 0.001, _(b'%.2f ms')),
2998 (10, 0.001, _(b'%.2f ms')),
3000 (1, 0.001, _(b'%.3f ms')),
2999 (1, 0.001, _(b'%.3f ms')),
3001 (100, 0.000001, _(b'%.1f us')),
3000 (100, 0.000001, _(b'%.1f us')),
3002 (10, 0.000001, _(b'%.2f us')),
3001 (10, 0.000001, _(b'%.2f us')),
3003 (1, 0.000001, _(b'%.3f us')),
3002 (1, 0.000001, _(b'%.3f us')),
3004 (100, 0.000000001, _(b'%.1f ns')),
3003 (100, 0.000000001, _(b'%.1f ns')),
3005 (10, 0.000000001, _(b'%.2f ns')),
3004 (10, 0.000000001, _(b'%.2f ns')),
3006 (1, 0.000000001, _(b'%.3f ns')),
3005 (1, 0.000000001, _(b'%.3f ns')),
3007 )
3006 )
3008
3007
3009
3008
3010 @attr.s
3009 @attr.s
3011 class timedcmstats(object):
3010 class timedcmstats(object):
3012 """Stats information produced by the timedcm context manager on entering."""
3011 """Stats information produced by the timedcm context manager on entering."""
3013
3012
3014 # the starting value of the timer as a float (meaning and resulution is
3013 # the starting value of the timer as a float (meaning and resulution is
3015 # platform dependent, see util.timer)
3014 # platform dependent, see util.timer)
3016 start = attr.ib(default=attr.Factory(lambda: timer()))
3015 start = attr.ib(default=attr.Factory(lambda: timer()))
3017 # the number of seconds as a floating point value; starts at 0, updated when
3016 # the number of seconds as a floating point value; starts at 0, updated when
3018 # the context is exited.
3017 # the context is exited.
3019 elapsed = attr.ib(default=0)
3018 elapsed = attr.ib(default=0)
3020 # the number of nested timedcm context managers.
3019 # the number of nested timedcm context managers.
3021 level = attr.ib(default=1)
3020 level = attr.ib(default=1)
3022
3021
3023 def __bytes__(self):
3022 def __bytes__(self):
3024 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3023 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3025
3024
3026 __str__ = encoding.strmethod(__bytes__)
3025 __str__ = encoding.strmethod(__bytes__)
3027
3026
3028
3027
3029 @contextlib.contextmanager
3028 @contextlib.contextmanager
3030 def timedcm(whencefmt, *whenceargs):
3029 def timedcm(whencefmt, *whenceargs):
3031 """A context manager that produces timing information for a given context.
3030 """A context manager that produces timing information for a given context.
3032
3031
3033 On entering a timedcmstats instance is produced.
3032 On entering a timedcmstats instance is produced.
3034
3033
3035 This context manager is reentrant.
3034 This context manager is reentrant.
3036
3035
3037 """
3036 """
3038 # track nested context managers
3037 # track nested context managers
3039 timedcm._nested += 1
3038 timedcm._nested += 1
3040 timing_stats = timedcmstats(level=timedcm._nested)
3039 timing_stats = timedcmstats(level=timedcm._nested)
3041 try:
3040 try:
3042 with tracing.log(whencefmt, *whenceargs):
3041 with tracing.log(whencefmt, *whenceargs):
3043 yield timing_stats
3042 yield timing_stats
3044 finally:
3043 finally:
3045 timing_stats.elapsed = timer() - timing_stats.start
3044 timing_stats.elapsed = timer() - timing_stats.start
3046 timedcm._nested -= 1
3045 timedcm._nested -= 1
3047
3046
3048
3047
3049 timedcm._nested = 0
3048 timedcm._nested = 0
3050
3049
3051
3050
3052 def timed(func):
3051 def timed(func):
3053 """Report the execution time of a function call to stderr.
3052 """Report the execution time of a function call to stderr.
3054
3053
3055 During development, use as a decorator when you need to measure
3054 During development, use as a decorator when you need to measure
3056 the cost of a function, e.g. as follows:
3055 the cost of a function, e.g. as follows:
3057
3056
3058 @util.timed
3057 @util.timed
3059 def foo(a, b, c):
3058 def foo(a, b, c):
3060 pass
3059 pass
3061 """
3060 """
3062
3061
3063 def wrapper(*args, **kwargs):
3062 def wrapper(*args, **kwargs):
3064 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3063 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3065 result = func(*args, **kwargs)
3064 result = func(*args, **kwargs)
3066 stderr = procutil.stderr
3065 stderr = procutil.stderr
3067 stderr.write(
3066 stderr.write(
3068 b'%s%s: %s\n'
3067 b'%s%s: %s\n'
3069 % (
3068 % (
3070 b' ' * time_stats.level * 2,
3069 b' ' * time_stats.level * 2,
3071 pycompat.bytestr(func.__name__),
3070 pycompat.bytestr(func.__name__),
3072 time_stats,
3071 time_stats,
3073 )
3072 )
3074 )
3073 )
3075 return result
3074 return result
3076
3075
3077 return wrapper
3076 return wrapper
3078
3077
3079
3078
3080 _sizeunits = (
3079 _sizeunits = (
3081 (b'm', 2 ** 20),
3080 (b'm', 2 ** 20),
3082 (b'k', 2 ** 10),
3081 (b'k', 2 ** 10),
3083 (b'g', 2 ** 30),
3082 (b'g', 2 ** 30),
3084 (b'kb', 2 ** 10),
3083 (b'kb', 2 ** 10),
3085 (b'mb', 2 ** 20),
3084 (b'mb', 2 ** 20),
3086 (b'gb', 2 ** 30),
3085 (b'gb', 2 ** 30),
3087 (b'b', 1),
3086 (b'b', 1),
3088 )
3087 )
3089
3088
3090
3089
3091 def sizetoint(s):
3090 def sizetoint(s):
3092 # type: (bytes) -> int
3091 # type: (bytes) -> int
3093 """Convert a space specifier to a byte count.
3092 """Convert a space specifier to a byte count.
3094
3093
3095 >>> sizetoint(b'30')
3094 >>> sizetoint(b'30')
3096 30
3095 30
3097 >>> sizetoint(b'2.2kb')
3096 >>> sizetoint(b'2.2kb')
3098 2252
3097 2252
3099 >>> sizetoint(b'6M')
3098 >>> sizetoint(b'6M')
3100 6291456
3099 6291456
3101 """
3100 """
3102 t = s.strip().lower()
3101 t = s.strip().lower()
3103 try:
3102 try:
3104 for k, u in _sizeunits:
3103 for k, u in _sizeunits:
3105 if t.endswith(k):
3104 if t.endswith(k):
3106 return int(float(t[: -len(k)]) * u)
3105 return int(float(t[: -len(k)]) * u)
3107 return int(t)
3106 return int(t)
3108 except ValueError:
3107 except ValueError:
3109 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3108 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3110
3109
3111
3110
3112 class hooks(object):
3111 class hooks(object):
3113 """A collection of hook functions that can be used to extend a
3112 """A collection of hook functions that can be used to extend a
3114 function's behavior. Hooks are called in lexicographic order,
3113 function's behavior. Hooks are called in lexicographic order,
3115 based on the names of their sources."""
3114 based on the names of their sources."""
3116
3115
3117 def __init__(self):
3116 def __init__(self):
3118 self._hooks = []
3117 self._hooks = []
3119
3118
3120 def add(self, source, hook):
3119 def add(self, source, hook):
3121 self._hooks.append((source, hook))
3120 self._hooks.append((source, hook))
3122
3121
3123 def __call__(self, *args):
3122 def __call__(self, *args):
3124 self._hooks.sort(key=lambda x: x[0])
3123 self._hooks.sort(key=lambda x: x[0])
3125 results = []
3124 results = []
3126 for source, hook in self._hooks:
3125 for source, hook in self._hooks:
3127 results.append(hook(*args))
3126 results.append(hook(*args))
3128 return results
3127 return results
3129
3128
3130
3129
3131 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3130 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3132 """Yields lines for a nicely formatted stacktrace.
3131 """Yields lines for a nicely formatted stacktrace.
3133 Skips the 'skip' last entries, then return the last 'depth' entries.
3132 Skips the 'skip' last entries, then return the last 'depth' entries.
3134 Each file+linenumber is formatted according to fileline.
3133 Each file+linenumber is formatted according to fileline.
3135 Each line is formatted according to line.
3134 Each line is formatted according to line.
3136 If line is None, it yields:
3135 If line is None, it yields:
3137 length of longest filepath+line number,
3136 length of longest filepath+line number,
3138 filepath+linenumber,
3137 filepath+linenumber,
3139 function
3138 function
3140
3139
3141 Not be used in production code but very convenient while developing.
3140 Not be used in production code but very convenient while developing.
3142 """
3141 """
3143 entries = [
3142 entries = [
3144 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3143 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3145 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3144 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3146 ][-depth:]
3145 ][-depth:]
3147 if entries:
3146 if entries:
3148 fnmax = max(len(entry[0]) for entry in entries)
3147 fnmax = max(len(entry[0]) for entry in entries)
3149 for fnln, func in entries:
3148 for fnln, func in entries:
3150 if line is None:
3149 if line is None:
3151 yield (fnmax, fnln, func)
3150 yield (fnmax, fnln, func)
3152 else:
3151 else:
3153 yield line % (fnmax, fnln, func)
3152 yield line % (fnmax, fnln, func)
3154
3153
3155
3154
3156 def debugstacktrace(
3155 def debugstacktrace(
3157 msg=b'stacktrace',
3156 msg=b'stacktrace',
3158 skip=0,
3157 skip=0,
3159 f=procutil.stderr,
3158 f=procutil.stderr,
3160 otherf=procutil.stdout,
3159 otherf=procutil.stdout,
3161 depth=0,
3160 depth=0,
3162 prefix=b'',
3161 prefix=b'',
3163 ):
3162 ):
3164 """Writes a message to f (stderr) with a nicely formatted stacktrace.
3163 """Writes a message to f (stderr) with a nicely formatted stacktrace.
3165 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3164 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3166 By default it will flush stdout first.
3165 By default it will flush stdout first.
3167 It can be used everywhere and intentionally does not require an ui object.
3166 It can be used everywhere and intentionally does not require an ui object.
3168 Not be used in production code but very convenient while developing.
3167 Not be used in production code but very convenient while developing.
3169 """
3168 """
3170 if otherf:
3169 if otherf:
3171 otherf.flush()
3170 otherf.flush()
3172 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3171 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3173 for line in getstackframes(skip + 1, depth=depth):
3172 for line in getstackframes(skip + 1, depth=depth):
3174 f.write(prefix + line)
3173 f.write(prefix + line)
3175 f.flush()
3174 f.flush()
3176
3175
3177
3176
3178 # convenient shortcut
3177 # convenient shortcut
3179 dst = debugstacktrace
3178 dst = debugstacktrace
3180
3179
3181
3180
3182 def safename(f, tag, ctx, others=None):
3181 def safename(f, tag, ctx, others=None):
3183 """
3182 """
3184 Generate a name that it is safe to rename f to in the given context.
3183 Generate a name that it is safe to rename f to in the given context.
3185
3184
3186 f: filename to rename
3185 f: filename to rename
3187 tag: a string tag that will be included in the new name
3186 tag: a string tag that will be included in the new name
3188 ctx: a context, in which the new name must not exist
3187 ctx: a context, in which the new name must not exist
3189 others: a set of other filenames that the new name must not be in
3188 others: a set of other filenames that the new name must not be in
3190
3189
3191 Returns a file name of the form oldname~tag[~number] which does not exist
3190 Returns a file name of the form oldname~tag[~number] which does not exist
3192 in the provided context and is not in the set of other names.
3191 in the provided context and is not in the set of other names.
3193 """
3192 """
3194 if others is None:
3193 if others is None:
3195 others = set()
3194 others = set()
3196
3195
3197 fn = b'%s~%s' % (f, tag)
3196 fn = b'%s~%s' % (f, tag)
3198 if fn not in ctx and fn not in others:
3197 if fn not in ctx and fn not in others:
3199 return fn
3198 return fn
3200 for n in itertools.count(1):
3199 for n in itertools.count(1):
3201 fn = b'%s~%s~%s' % (f, tag, n)
3200 fn = b'%s~%s~%s' % (f, tag, n)
3202 if fn not in ctx and fn not in others:
3201 if fn not in ctx and fn not in others:
3203 return fn
3202 return fn
3204
3203
3205
3204
3206 def readexactly(stream, n):
3205 def readexactly(stream, n):
3207 '''read n bytes from stream.read and abort if less was available'''
3206 '''read n bytes from stream.read and abort if less was available'''
3208 s = stream.read(n)
3207 s = stream.read(n)
3209 if len(s) < n:
3208 if len(s) < n:
3210 raise error.Abort(
3209 raise error.Abort(
3211 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3210 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3212 % (len(s), n)
3211 % (len(s), n)
3213 )
3212 )
3214 return s
3213 return s
3215
3214
3216
3215
3217 def uvarintencode(value):
3216 def uvarintencode(value):
3218 """Encode an unsigned integer value to a varint.
3217 """Encode an unsigned integer value to a varint.
3219
3218
3220 A varint is a variable length integer of 1 or more bytes. Each byte
3219 A varint is a variable length integer of 1 or more bytes. Each byte
3221 except the last has the most significant bit set. The lower 7 bits of
3220 except the last has the most significant bit set. The lower 7 bits of
3222 each byte store the 2's complement representation, least significant group
3221 each byte store the 2's complement representation, least significant group
3223 first.
3222 first.
3224
3223
3225 >>> uvarintencode(0)
3224 >>> uvarintencode(0)
3226 '\\x00'
3225 '\\x00'
3227 >>> uvarintencode(1)
3226 >>> uvarintencode(1)
3228 '\\x01'
3227 '\\x01'
3229 >>> uvarintencode(127)
3228 >>> uvarintencode(127)
3230 '\\x7f'
3229 '\\x7f'
3231 >>> uvarintencode(1337)
3230 >>> uvarintencode(1337)
3232 '\\xb9\\n'
3231 '\\xb9\\n'
3233 >>> uvarintencode(65536)
3232 >>> uvarintencode(65536)
3234 '\\x80\\x80\\x04'
3233 '\\x80\\x80\\x04'
3235 >>> uvarintencode(-1)
3234 >>> uvarintencode(-1)
3236 Traceback (most recent call last):
3235 Traceback (most recent call last):
3237 ...
3236 ...
3238 ProgrammingError: negative value for uvarint: -1
3237 ProgrammingError: negative value for uvarint: -1
3239 """
3238 """
3240 if value < 0:
3239 if value < 0:
3241 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3240 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3242 bits = value & 0x7F
3241 bits = value & 0x7F
3243 value >>= 7
3242 value >>= 7
3244 bytes = []
3243 bytes = []
3245 while value:
3244 while value:
3246 bytes.append(pycompat.bytechr(0x80 | bits))
3245 bytes.append(pycompat.bytechr(0x80 | bits))
3247 bits = value & 0x7F
3246 bits = value & 0x7F
3248 value >>= 7
3247 value >>= 7
3249 bytes.append(pycompat.bytechr(bits))
3248 bytes.append(pycompat.bytechr(bits))
3250
3249
3251 return b''.join(bytes)
3250 return b''.join(bytes)
3252
3251
3253
3252
3254 def uvarintdecodestream(fh):
3253 def uvarintdecodestream(fh):
3255 """Decode an unsigned variable length integer from a stream.
3254 """Decode an unsigned variable length integer from a stream.
3256
3255
3257 The passed argument is anything that has a ``.read(N)`` method.
3256 The passed argument is anything that has a ``.read(N)`` method.
3258
3257
3259 >>> try:
3258 >>> try:
3260 ... from StringIO import StringIO as BytesIO
3259 ... from StringIO import StringIO as BytesIO
3261 ... except ImportError:
3260 ... except ImportError:
3262 ... from io import BytesIO
3261 ... from io import BytesIO
3263 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3262 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3264 0
3263 0
3265 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3264 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3266 1
3265 1
3267 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3266 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3268 127
3267 127
3269 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3268 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3270 1337
3269 1337
3271 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3270 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3272 65536
3271 65536
3273 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3272 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3274 Traceback (most recent call last):
3273 Traceback (most recent call last):
3275 ...
3274 ...
3276 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3275 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3277 """
3276 """
3278 result = 0
3277 result = 0
3279 shift = 0
3278 shift = 0
3280 while True:
3279 while True:
3281 byte = ord(readexactly(fh, 1))
3280 byte = ord(readexactly(fh, 1))
3282 result |= (byte & 0x7F) << shift
3281 result |= (byte & 0x7F) << shift
3283 if not (byte & 0x80):
3282 if not (byte & 0x80):
3284 return result
3283 return result
3285 shift += 7
3284 shift += 7
3286
3285
3287
3286
3288 # Passing the '' locale means that the locale should be set according to the
3287 # Passing the '' locale means that the locale should be set according to the
3289 # user settings (environment variables).
3288 # user settings (environment variables).
3290 # Python sometimes avoids setting the global locale settings. When interfacing
3289 # Python sometimes avoids setting the global locale settings. When interfacing
3291 # with C code (e.g. the curses module or the Subversion bindings), the global
3290 # with C code (e.g. the curses module or the Subversion bindings), the global
3292 # locale settings must be initialized correctly. Python 2 does not initialize
3291 # locale settings must be initialized correctly. Python 2 does not initialize
3293 # the global locale settings on interpreter startup. Python 3 sometimes
3292 # the global locale settings on interpreter startup. Python 3 sometimes
3294 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3293 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3295 # explicitly initialize it to get consistent behavior if it's not already
3294 # explicitly initialize it to get consistent behavior if it's not already
3296 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3295 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3297 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3296 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3298 # if we can remove this code.
3297 # if we can remove this code.
3299 @contextlib.contextmanager
3298 @contextlib.contextmanager
3300 def with_lc_ctype():
3299 def with_lc_ctype():
3301 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3300 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3302 if oldloc == 'C':
3301 if oldloc == 'C':
3303 try:
3302 try:
3304 try:
3303 try:
3305 locale.setlocale(locale.LC_CTYPE, '')
3304 locale.setlocale(locale.LC_CTYPE, '')
3306 except locale.Error:
3305 except locale.Error:
3307 # The likely case is that the locale from the environment
3306 # The likely case is that the locale from the environment
3308 # variables is unknown.
3307 # variables is unknown.
3309 pass
3308 pass
3310 yield
3309 yield
3311 finally:
3310 finally:
3312 locale.setlocale(locale.LC_CTYPE, oldloc)
3311 locale.setlocale(locale.LC_CTYPE, oldloc)
3313 else:
3312 else:
3314 yield
3313 yield
3315
3314
3316
3315
3317 def _estimatememory():
3316 def _estimatememory():
3318 # type: () -> Optional[int]
3317 # type: () -> Optional[int]
3319 """Provide an estimate for the available system memory in Bytes.
3318 """Provide an estimate for the available system memory in Bytes.
3320
3319
3321 If no estimate can be provided on the platform, returns None.
3320 If no estimate can be provided on the platform, returns None.
3322 """
3321 """
3323 if pycompat.sysplatform.startswith(b'win'):
3322 if pycompat.sysplatform.startswith(b'win'):
3324 # On Windows, use the GlobalMemoryStatusEx kernel function directly.
3323 # On Windows, use the GlobalMemoryStatusEx kernel function directly.
3325 from ctypes import c_long as DWORD, c_ulonglong as DWORDLONG
3324 from ctypes import c_long as DWORD, c_ulonglong as DWORDLONG
3326 from ctypes.wintypes import ( # pytype: disable=import-error
3325 from ctypes.wintypes import ( # pytype: disable=import-error
3327 Structure,
3326 Structure,
3328 byref,
3327 byref,
3329 sizeof,
3328 sizeof,
3330 windll,
3329 windll,
3331 )
3330 )
3332
3331
3333 class MEMORYSTATUSEX(Structure):
3332 class MEMORYSTATUSEX(Structure):
3334 _fields_ = [
3333 _fields_ = [
3335 ('dwLength', DWORD),
3334 ('dwLength', DWORD),
3336 ('dwMemoryLoad', DWORD),
3335 ('dwMemoryLoad', DWORD),
3337 ('ullTotalPhys', DWORDLONG),
3336 ('ullTotalPhys', DWORDLONG),
3338 ('ullAvailPhys', DWORDLONG),
3337 ('ullAvailPhys', DWORDLONG),
3339 ('ullTotalPageFile', DWORDLONG),
3338 ('ullTotalPageFile', DWORDLONG),
3340 ('ullAvailPageFile', DWORDLONG),
3339 ('ullAvailPageFile', DWORDLONG),
3341 ('ullTotalVirtual', DWORDLONG),
3340 ('ullTotalVirtual', DWORDLONG),
3342 ('ullAvailVirtual', DWORDLONG),
3341 ('ullAvailVirtual', DWORDLONG),
3343 ('ullExtendedVirtual', DWORDLONG),
3342 ('ullExtendedVirtual', DWORDLONG),
3344 ]
3343 ]
3345
3344
3346 x = MEMORYSTATUSEX()
3345 x = MEMORYSTATUSEX()
3347 x.dwLength = sizeof(x)
3346 x.dwLength = sizeof(x)
3348 windll.kernel32.GlobalMemoryStatusEx(byref(x))
3347 windll.kernel32.GlobalMemoryStatusEx(byref(x))
3349 return x.ullAvailPhys
3348 return x.ullAvailPhys
3350
3349
3351 # On newer Unix-like systems and Mac OSX, the sysconf interface
3350 # On newer Unix-like systems and Mac OSX, the sysconf interface
3352 # can be used. _SC_PAGE_SIZE is part of POSIX; _SC_PHYS_PAGES
3351 # can be used. _SC_PAGE_SIZE is part of POSIX; _SC_PHYS_PAGES
3353 # seems to be implemented on most systems.
3352 # seems to be implemented on most systems.
3354 try:
3353 try:
3355 pagesize = os.sysconf(os.sysconf_names['SC_PAGE_SIZE'])
3354 pagesize = os.sysconf(os.sysconf_names['SC_PAGE_SIZE'])
3356 pages = os.sysconf(os.sysconf_names['SC_PHYS_PAGES'])
3355 pages = os.sysconf(os.sysconf_names['SC_PHYS_PAGES'])
3357 return pagesize * pages
3356 return pagesize * pages
3358 except OSError: # sysconf can fail
3357 except OSError: # sysconf can fail
3359 pass
3358 pass
3360 except KeyError: # unknown parameter
3359 except KeyError: # unknown parameter
3361 pass
3360 pass
@@ -1,468 +1,468 b''
1 # worker.py - master-slave parallelism support
1 # worker.py - master-slave parallelism support
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import os
11 import os
12 import pickle
12 import signal
13 import signal
13 import sys
14 import sys
14 import threading
15 import threading
15 import time
16 import time
16
17
17 try:
18 try:
18 import selectors
19 import selectors
19
20
20 selectors.BaseSelector
21 selectors.BaseSelector
21 except ImportError:
22 except ImportError:
22 from .thirdparty import selectors2 as selectors
23 from .thirdparty import selectors2 as selectors
23
24
24 from .i18n import _
25 from .i18n import _
25 from . import (
26 from . import (
26 encoding,
27 encoding,
27 error,
28 error,
28 pycompat,
29 pycompat,
29 scmutil,
30 scmutil,
30 util,
31 )
31 )
32
32
33
33
34 def countcpus():
34 def countcpus():
35 '''try to count the number of CPUs on the system'''
35 '''try to count the number of CPUs on the system'''
36
36
37 # posix
37 # posix
38 try:
38 try:
39 n = int(os.sysconf('SC_NPROCESSORS_ONLN'))
39 n = int(os.sysconf('SC_NPROCESSORS_ONLN'))
40 if n > 0:
40 if n > 0:
41 return n
41 return n
42 except (AttributeError, ValueError):
42 except (AttributeError, ValueError):
43 pass
43 pass
44
44
45 # windows
45 # windows
46 try:
46 try:
47 n = int(encoding.environ[b'NUMBER_OF_PROCESSORS'])
47 n = int(encoding.environ[b'NUMBER_OF_PROCESSORS'])
48 if n > 0:
48 if n > 0:
49 return n
49 return n
50 except (KeyError, ValueError):
50 except (KeyError, ValueError):
51 pass
51 pass
52
52
53 return 1
53 return 1
54
54
55
55
56 def _numworkers(ui):
56 def _numworkers(ui):
57 s = ui.config(b'worker', b'numcpus')
57 s = ui.config(b'worker', b'numcpus')
58 if s:
58 if s:
59 try:
59 try:
60 n = int(s)
60 n = int(s)
61 if n >= 1:
61 if n >= 1:
62 return n
62 return n
63 except ValueError:
63 except ValueError:
64 raise error.Abort(_(b'number of cpus must be an integer'))
64 raise error.Abort(_(b'number of cpus must be an integer'))
65 return min(max(countcpus(), 4), 32)
65 return min(max(countcpus(), 4), 32)
66
66
67
67
68 if pycompat.ispy3:
68 if pycompat.ispy3:
69
69
70 def ismainthread():
70 def ismainthread():
71 return threading.current_thread() == threading.main_thread()
71 return threading.current_thread() == threading.main_thread()
72
72
73 class _blockingreader(object):
73 class _blockingreader(object):
74 def __init__(self, wrapped):
74 def __init__(self, wrapped):
75 self._wrapped = wrapped
75 self._wrapped = wrapped
76
76
77 # Do NOT implement readinto() by making it delegate to
77 # Do NOT implement readinto() by making it delegate to
78 # _wrapped.readinto(), since that is unbuffered. The unpickler is fine
78 # _wrapped.readinto(), since that is unbuffered. The unpickler is fine
79 # with just read() and readline(), so we don't need to implement it.
79 # with just read() and readline(), so we don't need to implement it.
80
80
81 def readline(self):
81 def readline(self):
82 return self._wrapped.readline()
82 return self._wrapped.readline()
83
83
84 # issue multiple reads until size is fulfilled
84 # issue multiple reads until size is fulfilled
85 def read(self, size=-1):
85 def read(self, size=-1):
86 if size < 0:
86 if size < 0:
87 return self._wrapped.readall()
87 return self._wrapped.readall()
88
88
89 buf = bytearray(size)
89 buf = bytearray(size)
90 view = memoryview(buf)
90 view = memoryview(buf)
91 pos = 0
91 pos = 0
92
92
93 while pos < size:
93 while pos < size:
94 ret = self._wrapped.readinto(view[pos:])
94 ret = self._wrapped.readinto(view[pos:])
95 if not ret:
95 if not ret:
96 break
96 break
97 pos += ret
97 pos += ret
98
98
99 del view
99 del view
100 del buf[pos:]
100 del buf[pos:]
101 return bytes(buf)
101 return bytes(buf)
102
102
103
103
104 else:
104 else:
105
105
106 def ismainthread():
106 def ismainthread():
107 # pytype: disable=module-attr
107 # pytype: disable=module-attr
108 return isinstance(threading.current_thread(), threading._MainThread)
108 return isinstance(threading.current_thread(), threading._MainThread)
109 # pytype: enable=module-attr
109 # pytype: enable=module-attr
110
110
111 def _blockingreader(wrapped):
111 def _blockingreader(wrapped):
112 return wrapped
112 return wrapped
113
113
114
114
115 if pycompat.isposix or pycompat.iswindows:
115 if pycompat.isposix or pycompat.iswindows:
116 _STARTUP_COST = 0.01
116 _STARTUP_COST = 0.01
117 # The Windows worker is thread based. If tasks are CPU bound, threads
117 # The Windows worker is thread based. If tasks are CPU bound, threads
118 # in the presence of the GIL result in excessive context switching and
118 # in the presence of the GIL result in excessive context switching and
119 # this overhead can slow down execution.
119 # this overhead can slow down execution.
120 _DISALLOW_THREAD_UNSAFE = pycompat.iswindows
120 _DISALLOW_THREAD_UNSAFE = pycompat.iswindows
121 else:
121 else:
122 _STARTUP_COST = 1e30
122 _STARTUP_COST = 1e30
123 _DISALLOW_THREAD_UNSAFE = False
123 _DISALLOW_THREAD_UNSAFE = False
124
124
125
125
126 def worthwhile(ui, costperop, nops, threadsafe=True):
126 def worthwhile(ui, costperop, nops, threadsafe=True):
127 """try to determine whether the benefit of multiple processes can
127 """try to determine whether the benefit of multiple processes can
128 outweigh the cost of starting them"""
128 outweigh the cost of starting them"""
129
129
130 if not threadsafe and _DISALLOW_THREAD_UNSAFE:
130 if not threadsafe and _DISALLOW_THREAD_UNSAFE:
131 return False
131 return False
132
132
133 linear = costperop * nops
133 linear = costperop * nops
134 workers = _numworkers(ui)
134 workers = _numworkers(ui)
135 benefit = linear - (_STARTUP_COST * workers + linear / workers)
135 benefit = linear - (_STARTUP_COST * workers + linear / workers)
136 return benefit >= 0.15
136 return benefit >= 0.15
137
137
138
138
139 def worker(
139 def worker(
140 ui, costperarg, func, staticargs, args, hasretval=False, threadsafe=True
140 ui, costperarg, func, staticargs, args, hasretval=False, threadsafe=True
141 ):
141 ):
142 """run a function, possibly in parallel in multiple worker
142 """run a function, possibly in parallel in multiple worker
143 processes.
143 processes.
144
144
145 returns a progress iterator
145 returns a progress iterator
146
146
147 costperarg - cost of a single task
147 costperarg - cost of a single task
148
148
149 func - function to run. It is expected to return a progress iterator.
149 func - function to run. It is expected to return a progress iterator.
150
150
151 staticargs - arguments to pass to every invocation of the function
151 staticargs - arguments to pass to every invocation of the function
152
152
153 args - arguments to split into chunks, to pass to individual
153 args - arguments to split into chunks, to pass to individual
154 workers
154 workers
155
155
156 hasretval - when True, func and the current function return an progress
156 hasretval - when True, func and the current function return an progress
157 iterator then a dict (encoded as an iterator that yield many (False, ..)
157 iterator then a dict (encoded as an iterator that yield many (False, ..)
158 then a (True, dict)). The dicts are joined in some arbitrary order, so
158 then a (True, dict)). The dicts are joined in some arbitrary order, so
159 overlapping keys are a bad idea.
159 overlapping keys are a bad idea.
160
160
161 threadsafe - whether work items are thread safe and can be executed using
161 threadsafe - whether work items are thread safe and can be executed using
162 a thread-based worker. Should be disabled for CPU heavy tasks that don't
162 a thread-based worker. Should be disabled for CPU heavy tasks that don't
163 release the GIL.
163 release the GIL.
164 """
164 """
165 enabled = ui.configbool(b'worker', b'enabled')
165 enabled = ui.configbool(b'worker', b'enabled')
166 if enabled and _platformworker is _posixworker and not ismainthread():
166 if enabled and _platformworker is _posixworker and not ismainthread():
167 # The POSIX worker has to install a handler for SIGCHLD.
167 # The POSIX worker has to install a handler for SIGCHLD.
168 # Python up to 3.9 only allows this in the main thread.
168 # Python up to 3.9 only allows this in the main thread.
169 enabled = False
169 enabled = False
170
170
171 if enabled and worthwhile(ui, costperarg, len(args), threadsafe=threadsafe):
171 if enabled and worthwhile(ui, costperarg, len(args), threadsafe=threadsafe):
172 return _platformworker(ui, func, staticargs, args, hasretval)
172 return _platformworker(ui, func, staticargs, args, hasretval)
173 return func(*staticargs + (args,))
173 return func(*staticargs + (args,))
174
174
175
175
176 def _posixworker(ui, func, staticargs, args, hasretval):
176 def _posixworker(ui, func, staticargs, args, hasretval):
177 workers = _numworkers(ui)
177 workers = _numworkers(ui)
178 oldhandler = signal.getsignal(signal.SIGINT)
178 oldhandler = signal.getsignal(signal.SIGINT)
179 signal.signal(signal.SIGINT, signal.SIG_IGN)
179 signal.signal(signal.SIGINT, signal.SIG_IGN)
180 pids, problem = set(), [0]
180 pids, problem = set(), [0]
181
181
182 def killworkers():
182 def killworkers():
183 # unregister SIGCHLD handler as all children will be killed. This
183 # unregister SIGCHLD handler as all children will be killed. This
184 # function shouldn't be interrupted by another SIGCHLD; otherwise pids
184 # function shouldn't be interrupted by another SIGCHLD; otherwise pids
185 # could be updated while iterating, which would cause inconsistency.
185 # could be updated while iterating, which would cause inconsistency.
186 signal.signal(signal.SIGCHLD, oldchldhandler)
186 signal.signal(signal.SIGCHLD, oldchldhandler)
187 # if one worker bails, there's no good reason to wait for the rest
187 # if one worker bails, there's no good reason to wait for the rest
188 for p in pids:
188 for p in pids:
189 try:
189 try:
190 os.kill(p, signal.SIGTERM)
190 os.kill(p, signal.SIGTERM)
191 except OSError as err:
191 except OSError as err:
192 if err.errno != errno.ESRCH:
192 if err.errno != errno.ESRCH:
193 raise
193 raise
194
194
195 def waitforworkers(blocking=True):
195 def waitforworkers(blocking=True):
196 for pid in pids.copy():
196 for pid in pids.copy():
197 p = st = 0
197 p = st = 0
198 while True:
198 while True:
199 try:
199 try:
200 p, st = os.waitpid(pid, (0 if blocking else os.WNOHANG))
200 p, st = os.waitpid(pid, (0 if blocking else os.WNOHANG))
201 break
201 break
202 except OSError as e:
202 except OSError as e:
203 if e.errno == errno.EINTR:
203 if e.errno == errno.EINTR:
204 continue
204 continue
205 elif e.errno == errno.ECHILD:
205 elif e.errno == errno.ECHILD:
206 # child would already be reaped, but pids yet been
206 # child would already be reaped, but pids yet been
207 # updated (maybe interrupted just after waitpid)
207 # updated (maybe interrupted just after waitpid)
208 pids.discard(pid)
208 pids.discard(pid)
209 break
209 break
210 else:
210 else:
211 raise
211 raise
212 if not p:
212 if not p:
213 # skip subsequent steps, because child process should
213 # skip subsequent steps, because child process should
214 # be still running in this case
214 # be still running in this case
215 continue
215 continue
216 pids.discard(p)
216 pids.discard(p)
217 st = _exitstatus(st)
217 st = _exitstatus(st)
218 if st and not problem[0]:
218 if st and not problem[0]:
219 problem[0] = st
219 problem[0] = st
220
220
221 def sigchldhandler(signum, frame):
221 def sigchldhandler(signum, frame):
222 waitforworkers(blocking=False)
222 waitforworkers(blocking=False)
223 if problem[0]:
223 if problem[0]:
224 killworkers()
224 killworkers()
225
225
226 oldchldhandler = signal.signal(signal.SIGCHLD, sigchldhandler)
226 oldchldhandler = signal.signal(signal.SIGCHLD, sigchldhandler)
227 ui.flush()
227 ui.flush()
228 parentpid = os.getpid()
228 parentpid = os.getpid()
229 pipes = []
229 pipes = []
230 retval = {}
230 retval = {}
231 for pargs in partition(args, min(workers, len(args))):
231 for pargs in partition(args, min(workers, len(args))):
232 # Every worker gets its own pipe to send results on, so we don't have to
232 # Every worker gets its own pipe to send results on, so we don't have to
233 # implement atomic writes larger than PIPE_BUF. Each forked process has
233 # implement atomic writes larger than PIPE_BUF. Each forked process has
234 # its own pipe's descriptors in the local variables, and the parent
234 # its own pipe's descriptors in the local variables, and the parent
235 # process has the full list of pipe descriptors (and it doesn't really
235 # process has the full list of pipe descriptors (and it doesn't really
236 # care what order they're in).
236 # care what order they're in).
237 rfd, wfd = os.pipe()
237 rfd, wfd = os.pipe()
238 pipes.append((rfd, wfd))
238 pipes.append((rfd, wfd))
239 # make sure we use os._exit in all worker code paths. otherwise the
239 # make sure we use os._exit in all worker code paths. otherwise the
240 # worker may do some clean-ups which could cause surprises like
240 # worker may do some clean-ups which could cause surprises like
241 # deadlock. see sshpeer.cleanup for example.
241 # deadlock. see sshpeer.cleanup for example.
242 # override error handling *before* fork. this is necessary because
242 # override error handling *before* fork. this is necessary because
243 # exception (signal) may arrive after fork, before "pid =" assignment
243 # exception (signal) may arrive after fork, before "pid =" assignment
244 # completes, and other exception handler (dispatch.py) can lead to
244 # completes, and other exception handler (dispatch.py) can lead to
245 # unexpected code path without os._exit.
245 # unexpected code path without os._exit.
246 ret = -1
246 ret = -1
247 try:
247 try:
248 pid = os.fork()
248 pid = os.fork()
249 if pid == 0:
249 if pid == 0:
250 signal.signal(signal.SIGINT, oldhandler)
250 signal.signal(signal.SIGINT, oldhandler)
251 signal.signal(signal.SIGCHLD, oldchldhandler)
251 signal.signal(signal.SIGCHLD, oldchldhandler)
252
252
253 def workerfunc():
253 def workerfunc():
254 for r, w in pipes[:-1]:
254 for r, w in pipes[:-1]:
255 os.close(r)
255 os.close(r)
256 os.close(w)
256 os.close(w)
257 os.close(rfd)
257 os.close(rfd)
258 for result in func(*(staticargs + (pargs,))):
258 for result in func(*(staticargs + (pargs,))):
259 os.write(wfd, util.pickle.dumps(result))
259 os.write(wfd, pickle.dumps(result))
260 return 0
260 return 0
261
261
262 ret = scmutil.callcatch(ui, workerfunc)
262 ret = scmutil.callcatch(ui, workerfunc)
263 except: # parent re-raises, child never returns
263 except: # parent re-raises, child never returns
264 if os.getpid() == parentpid:
264 if os.getpid() == parentpid:
265 raise
265 raise
266 exctype = sys.exc_info()[0]
266 exctype = sys.exc_info()[0]
267 force = not issubclass(exctype, KeyboardInterrupt)
267 force = not issubclass(exctype, KeyboardInterrupt)
268 ui.traceback(force=force)
268 ui.traceback(force=force)
269 finally:
269 finally:
270 if os.getpid() != parentpid:
270 if os.getpid() != parentpid:
271 try:
271 try:
272 ui.flush()
272 ui.flush()
273 except: # never returns, no re-raises
273 except: # never returns, no re-raises
274 pass
274 pass
275 finally:
275 finally:
276 os._exit(ret & 255)
276 os._exit(ret & 255)
277 pids.add(pid)
277 pids.add(pid)
278 selector = selectors.DefaultSelector()
278 selector = selectors.DefaultSelector()
279 for rfd, wfd in pipes:
279 for rfd, wfd in pipes:
280 os.close(wfd)
280 os.close(wfd)
281 selector.register(os.fdopen(rfd, 'rb', 0), selectors.EVENT_READ)
281 selector.register(os.fdopen(rfd, 'rb', 0), selectors.EVENT_READ)
282
282
283 def cleanup():
283 def cleanup():
284 signal.signal(signal.SIGINT, oldhandler)
284 signal.signal(signal.SIGINT, oldhandler)
285 waitforworkers()
285 waitforworkers()
286 signal.signal(signal.SIGCHLD, oldchldhandler)
286 signal.signal(signal.SIGCHLD, oldchldhandler)
287 selector.close()
287 selector.close()
288 return problem[0]
288 return problem[0]
289
289
290 try:
290 try:
291 openpipes = len(pipes)
291 openpipes = len(pipes)
292 while openpipes > 0:
292 while openpipes > 0:
293 for key, events in selector.select():
293 for key, events in selector.select():
294 try:
294 try:
295 res = util.pickle.load(_blockingreader(key.fileobj))
295 res = pickle.load(_blockingreader(key.fileobj))
296 if hasretval and res[0]:
296 if hasretval and res[0]:
297 retval.update(res[1])
297 retval.update(res[1])
298 else:
298 else:
299 yield res
299 yield res
300 except EOFError:
300 except EOFError:
301 selector.unregister(key.fileobj)
301 selector.unregister(key.fileobj)
302 key.fileobj.close()
302 key.fileobj.close()
303 openpipes -= 1
303 openpipes -= 1
304 except IOError as e:
304 except IOError as e:
305 if e.errno == errno.EINTR:
305 if e.errno == errno.EINTR:
306 continue
306 continue
307 raise
307 raise
308 except: # re-raises
308 except: # re-raises
309 killworkers()
309 killworkers()
310 cleanup()
310 cleanup()
311 raise
311 raise
312 status = cleanup()
312 status = cleanup()
313 if status:
313 if status:
314 if status < 0:
314 if status < 0:
315 os.kill(os.getpid(), -status)
315 os.kill(os.getpid(), -status)
316 raise error.WorkerError(status)
316 raise error.WorkerError(status)
317 if hasretval:
317 if hasretval:
318 yield True, retval
318 yield True, retval
319
319
320
320
321 def _posixexitstatus(code):
321 def _posixexitstatus(code):
322 """convert a posix exit status into the same form returned by
322 """convert a posix exit status into the same form returned by
323 os.spawnv
323 os.spawnv
324
324
325 returns None if the process was stopped instead of exiting"""
325 returns None if the process was stopped instead of exiting"""
326 if os.WIFEXITED(code):
326 if os.WIFEXITED(code):
327 return os.WEXITSTATUS(code)
327 return os.WEXITSTATUS(code)
328 elif os.WIFSIGNALED(code):
328 elif os.WIFSIGNALED(code):
329 return -(os.WTERMSIG(code))
329 return -(os.WTERMSIG(code))
330
330
331
331
332 def _windowsworker(ui, func, staticargs, args, hasretval):
332 def _windowsworker(ui, func, staticargs, args, hasretval):
333 class Worker(threading.Thread):
333 class Worker(threading.Thread):
334 def __init__(
334 def __init__(
335 self, taskqueue, resultqueue, func, staticargs, *args, **kwargs
335 self, taskqueue, resultqueue, func, staticargs, *args, **kwargs
336 ):
336 ):
337 threading.Thread.__init__(self, *args, **kwargs)
337 threading.Thread.__init__(self, *args, **kwargs)
338 self._taskqueue = taskqueue
338 self._taskqueue = taskqueue
339 self._resultqueue = resultqueue
339 self._resultqueue = resultqueue
340 self._func = func
340 self._func = func
341 self._staticargs = staticargs
341 self._staticargs = staticargs
342 self._interrupted = False
342 self._interrupted = False
343 self.daemon = True
343 self.daemon = True
344 self.exception = None
344 self.exception = None
345
345
346 def interrupt(self):
346 def interrupt(self):
347 self._interrupted = True
347 self._interrupted = True
348
348
349 def run(self):
349 def run(self):
350 try:
350 try:
351 while not self._taskqueue.empty():
351 while not self._taskqueue.empty():
352 try:
352 try:
353 args = self._taskqueue.get_nowait()
353 args = self._taskqueue.get_nowait()
354 for res in self._func(*self._staticargs + (args,)):
354 for res in self._func(*self._staticargs + (args,)):
355 self._resultqueue.put(res)
355 self._resultqueue.put(res)
356 # threading doesn't provide a native way to
356 # threading doesn't provide a native way to
357 # interrupt execution. handle it manually at every
357 # interrupt execution. handle it manually at every
358 # iteration.
358 # iteration.
359 if self._interrupted:
359 if self._interrupted:
360 return
360 return
361 except pycompat.queue.Empty:
361 except pycompat.queue.Empty:
362 break
362 break
363 except Exception as e:
363 except Exception as e:
364 # store the exception such that the main thread can resurface
364 # store the exception such that the main thread can resurface
365 # it as if the func was running without workers.
365 # it as if the func was running without workers.
366 self.exception = e
366 self.exception = e
367 raise
367 raise
368
368
369 threads = []
369 threads = []
370
370
371 def trykillworkers():
371 def trykillworkers():
372 # Allow up to 1 second to clean worker threads nicely
372 # Allow up to 1 second to clean worker threads nicely
373 cleanupend = time.time() + 1
373 cleanupend = time.time() + 1
374 for t in threads:
374 for t in threads:
375 t.interrupt()
375 t.interrupt()
376 for t in threads:
376 for t in threads:
377 remainingtime = cleanupend - time.time()
377 remainingtime = cleanupend - time.time()
378 t.join(remainingtime)
378 t.join(remainingtime)
379 if t.is_alive():
379 if t.is_alive():
380 # pass over the workers joining failure. it is more
380 # pass over the workers joining failure. it is more
381 # important to surface the inital exception than the
381 # important to surface the inital exception than the
382 # fact that one of workers may be processing a large
382 # fact that one of workers may be processing a large
383 # task and does not get to handle the interruption.
383 # task and does not get to handle the interruption.
384 ui.warn(
384 ui.warn(
385 _(
385 _(
386 b"failed to kill worker threads while "
386 b"failed to kill worker threads while "
387 b"handling an exception\n"
387 b"handling an exception\n"
388 )
388 )
389 )
389 )
390 return
390 return
391
391
392 workers = _numworkers(ui)
392 workers = _numworkers(ui)
393 resultqueue = pycompat.queue.Queue()
393 resultqueue = pycompat.queue.Queue()
394 taskqueue = pycompat.queue.Queue()
394 taskqueue = pycompat.queue.Queue()
395 retval = {}
395 retval = {}
396 # partition work to more pieces than workers to minimize the chance
396 # partition work to more pieces than workers to minimize the chance
397 # of uneven distribution of large tasks between the workers
397 # of uneven distribution of large tasks between the workers
398 for pargs in partition(args, workers * 20):
398 for pargs in partition(args, workers * 20):
399 taskqueue.put(pargs)
399 taskqueue.put(pargs)
400 for _i in range(workers):
400 for _i in range(workers):
401 t = Worker(taskqueue, resultqueue, func, staticargs)
401 t = Worker(taskqueue, resultqueue, func, staticargs)
402 threads.append(t)
402 threads.append(t)
403 t.start()
403 t.start()
404 try:
404 try:
405 while len(threads) > 0:
405 while len(threads) > 0:
406 while not resultqueue.empty():
406 while not resultqueue.empty():
407 res = resultqueue.get()
407 res = resultqueue.get()
408 if hasretval and res[0]:
408 if hasretval and res[0]:
409 retval.update(res[1])
409 retval.update(res[1])
410 else:
410 else:
411 yield res
411 yield res
412 threads[0].join(0.05)
412 threads[0].join(0.05)
413 finishedthreads = [_t for _t in threads if not _t.is_alive()]
413 finishedthreads = [_t for _t in threads if not _t.is_alive()]
414 for t in finishedthreads:
414 for t in finishedthreads:
415 if t.exception is not None:
415 if t.exception is not None:
416 raise t.exception
416 raise t.exception
417 threads.remove(t)
417 threads.remove(t)
418 except (Exception, KeyboardInterrupt): # re-raises
418 except (Exception, KeyboardInterrupt): # re-raises
419 trykillworkers()
419 trykillworkers()
420 raise
420 raise
421 while not resultqueue.empty():
421 while not resultqueue.empty():
422 res = resultqueue.get()
422 res = resultqueue.get()
423 if hasretval and res[0]:
423 if hasretval and res[0]:
424 retval.update(res[1])
424 retval.update(res[1])
425 else:
425 else:
426 yield res
426 yield res
427 if hasretval:
427 if hasretval:
428 yield True, retval
428 yield True, retval
429
429
430
430
431 if pycompat.iswindows:
431 if pycompat.iswindows:
432 _platformworker = _windowsworker
432 _platformworker = _windowsworker
433 else:
433 else:
434 _platformworker = _posixworker
434 _platformworker = _posixworker
435 _exitstatus = _posixexitstatus
435 _exitstatus = _posixexitstatus
436
436
437
437
438 def partition(lst, nslices):
438 def partition(lst, nslices):
439 """partition a list into N slices of roughly equal size
439 """partition a list into N slices of roughly equal size
440
440
441 The current strategy takes every Nth element from the input. If
441 The current strategy takes every Nth element from the input. If
442 we ever write workers that need to preserve grouping in input
442 we ever write workers that need to preserve grouping in input
443 we should consider allowing callers to specify a partition strategy.
443 we should consider allowing callers to specify a partition strategy.
444
444
445 olivia is not a fan of this partitioning strategy when files are involved.
445 olivia is not a fan of this partitioning strategy when files are involved.
446 In his words:
446 In his words:
447
447
448 Single-threaded Mercurial makes a point of creating and visiting
448 Single-threaded Mercurial makes a point of creating and visiting
449 files in a fixed order (alphabetical). When creating files in order,
449 files in a fixed order (alphabetical). When creating files in order,
450 a typical filesystem is likely to allocate them on nearby regions on
450 a typical filesystem is likely to allocate them on nearby regions on
451 disk. Thus, when revisiting in the same order, locality is maximized
451 disk. Thus, when revisiting in the same order, locality is maximized
452 and various forms of OS and disk-level caching and read-ahead get a
452 and various forms of OS and disk-level caching and read-ahead get a
453 chance to work.
453 chance to work.
454
454
455 This effect can be quite significant on spinning disks. I discovered it
455 This effect can be quite significant on spinning disks. I discovered it
456 circa Mercurial v0.4 when revlogs were named by hashes of filenames.
456 circa Mercurial v0.4 when revlogs were named by hashes of filenames.
457 Tarring a repo and copying it to another disk effectively randomized
457 Tarring a repo and copying it to another disk effectively randomized
458 the revlog ordering on disk by sorting the revlogs by hash and suddenly
458 the revlog ordering on disk by sorting the revlogs by hash and suddenly
459 performance of my kernel checkout benchmark dropped by ~10x because the
459 performance of my kernel checkout benchmark dropped by ~10x because the
460 "working set" of sectors visited no longer fit in the drive's cache and
460 "working set" of sectors visited no longer fit in the drive's cache and
461 the workload switched from streaming to random I/O.
461 the workload switched from streaming to random I/O.
462
462
463 What we should really be doing is have workers read filenames from a
463 What we should really be doing is have workers read filenames from a
464 ordered queue. This preserves locality and also keeps any worker from
464 ordered queue. This preserves locality and also keeps any worker from
465 getting more than one file out of balance.
465 getting more than one file out of balance.
466 """
466 """
467 for i in range(nslices):
467 for i in range(nslices):
468 yield lst[i::nslices]
468 yield lst[i::nslices]
@@ -1,973 +1,973 b''
1 #testcases dirstate-v1 dirstate-v2
1 #testcases dirstate-v1 dirstate-v2
2
2
3 #if dirstate-v2
3 #if dirstate-v2
4 $ cat >> $HGRCPATH << EOF
4 $ cat >> $HGRCPATH << EOF
5 > [format]
5 > [format]
6 > use-dirstate-v2=1
6 > use-dirstate-v2=1
7 > [storage]
7 > [storage]
8 > dirstate-v2.slow-path=allow
8 > dirstate-v2.slow-path=allow
9 > EOF
9 > EOF
10 #endif
10 #endif
11
11
12 $ hg init repo1
12 $ hg init repo1
13 $ cd repo1
13 $ cd repo1
14 $ mkdir a b a/1 b/1 b/2
14 $ mkdir a b a/1 b/1 b/2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
16
16
17 hg status in repo root:
17 hg status in repo root:
18
18
19 $ hg status
19 $ hg status
20 ? a/1/in_a_1
20 ? a/1/in_a_1
21 ? a/in_a
21 ? a/in_a
22 ? b/1/in_b_1
22 ? b/1/in_b_1
23 ? b/2/in_b_2
23 ? b/2/in_b_2
24 ? b/in_b
24 ? b/in_b
25 ? in_root
25 ? in_root
26
26
27 hg status . in repo root:
27 hg status . in repo root:
28
28
29 $ hg status .
29 $ hg status .
30 ? a/1/in_a_1
30 ? a/1/in_a_1
31 ? a/in_a
31 ? a/in_a
32 ? b/1/in_b_1
32 ? b/1/in_b_1
33 ? b/2/in_b_2
33 ? b/2/in_b_2
34 ? b/in_b
34 ? b/in_b
35 ? in_root
35 ? in_root
36
36
37 $ hg status --cwd a
37 $ hg status --cwd a
38 ? a/1/in_a_1
38 ? a/1/in_a_1
39 ? a/in_a
39 ? a/in_a
40 ? b/1/in_b_1
40 ? b/1/in_b_1
41 ? b/2/in_b_2
41 ? b/2/in_b_2
42 ? b/in_b
42 ? b/in_b
43 ? in_root
43 ? in_root
44 $ hg status --cwd a .
44 $ hg status --cwd a .
45 ? 1/in_a_1
45 ? 1/in_a_1
46 ? in_a
46 ? in_a
47 $ hg status --cwd a ..
47 $ hg status --cwd a ..
48 ? 1/in_a_1
48 ? 1/in_a_1
49 ? in_a
49 ? in_a
50 ? ../b/1/in_b_1
50 ? ../b/1/in_b_1
51 ? ../b/2/in_b_2
51 ? ../b/2/in_b_2
52 ? ../b/in_b
52 ? ../b/in_b
53 ? ../in_root
53 ? ../in_root
54
54
55 $ hg status --cwd b
55 $ hg status --cwd b
56 ? a/1/in_a_1
56 ? a/1/in_a_1
57 ? a/in_a
57 ? a/in_a
58 ? b/1/in_b_1
58 ? b/1/in_b_1
59 ? b/2/in_b_2
59 ? b/2/in_b_2
60 ? b/in_b
60 ? b/in_b
61 ? in_root
61 ? in_root
62 $ hg status --cwd b .
62 $ hg status --cwd b .
63 ? 1/in_b_1
63 ? 1/in_b_1
64 ? 2/in_b_2
64 ? 2/in_b_2
65 ? in_b
65 ? in_b
66 $ hg status --cwd b ..
66 $ hg status --cwd b ..
67 ? ../a/1/in_a_1
67 ? ../a/1/in_a_1
68 ? ../a/in_a
68 ? ../a/in_a
69 ? 1/in_b_1
69 ? 1/in_b_1
70 ? 2/in_b_2
70 ? 2/in_b_2
71 ? in_b
71 ? in_b
72 ? ../in_root
72 ? ../in_root
73
73
74 $ hg status --cwd a/1
74 $ hg status --cwd a/1
75 ? a/1/in_a_1
75 ? a/1/in_a_1
76 ? a/in_a
76 ? a/in_a
77 ? b/1/in_b_1
77 ? b/1/in_b_1
78 ? b/2/in_b_2
78 ? b/2/in_b_2
79 ? b/in_b
79 ? b/in_b
80 ? in_root
80 ? in_root
81 $ hg status --cwd a/1 .
81 $ hg status --cwd a/1 .
82 ? in_a_1
82 ? in_a_1
83 $ hg status --cwd a/1 ..
83 $ hg status --cwd a/1 ..
84 ? in_a_1
84 ? in_a_1
85 ? ../in_a
85 ? ../in_a
86
86
87 $ hg status --cwd b/1
87 $ hg status --cwd b/1
88 ? a/1/in_a_1
88 ? a/1/in_a_1
89 ? a/in_a
89 ? a/in_a
90 ? b/1/in_b_1
90 ? b/1/in_b_1
91 ? b/2/in_b_2
91 ? b/2/in_b_2
92 ? b/in_b
92 ? b/in_b
93 ? in_root
93 ? in_root
94 $ hg status --cwd b/1 .
94 $ hg status --cwd b/1 .
95 ? in_b_1
95 ? in_b_1
96 $ hg status --cwd b/1 ..
96 $ hg status --cwd b/1 ..
97 ? in_b_1
97 ? in_b_1
98 ? ../2/in_b_2
98 ? ../2/in_b_2
99 ? ../in_b
99 ? ../in_b
100
100
101 $ hg status --cwd b/2
101 $ hg status --cwd b/2
102 ? a/1/in_a_1
102 ? a/1/in_a_1
103 ? a/in_a
103 ? a/in_a
104 ? b/1/in_b_1
104 ? b/1/in_b_1
105 ? b/2/in_b_2
105 ? b/2/in_b_2
106 ? b/in_b
106 ? b/in_b
107 ? in_root
107 ? in_root
108 $ hg status --cwd b/2 .
108 $ hg status --cwd b/2 .
109 ? in_b_2
109 ? in_b_2
110 $ hg status --cwd b/2 ..
110 $ hg status --cwd b/2 ..
111 ? ../1/in_b_1
111 ? ../1/in_b_1
112 ? in_b_2
112 ? in_b_2
113 ? ../in_b
113 ? ../in_b
114
114
115 combining patterns with root and patterns without a root works
115 combining patterns with root and patterns without a root works
116
116
117 $ hg st a/in_a re:.*b$
117 $ hg st a/in_a re:.*b$
118 ? a/in_a
118 ? a/in_a
119 ? b/in_b
119 ? b/in_b
120
120
121 tweaking defaults works
121 tweaking defaults works
122 $ hg status --cwd a --config ui.tweakdefaults=yes
122 $ hg status --cwd a --config ui.tweakdefaults=yes
123 ? 1/in_a_1
123 ? 1/in_a_1
124 ? in_a
124 ? in_a
125 ? ../b/1/in_b_1
125 ? ../b/1/in_b_1
126 ? ../b/2/in_b_2
126 ? ../b/2/in_b_2
127 ? ../b/in_b
127 ? ../b/in_b
128 ? ../in_root
128 ? ../in_root
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
130 ? a/1/in_a_1 (glob)
130 ? a/1/in_a_1 (glob)
131 ? a/in_a (glob)
131 ? a/in_a (glob)
132 ? b/1/in_b_1 (glob)
132 ? b/1/in_b_1 (glob)
133 ? b/2/in_b_2 (glob)
133 ? b/2/in_b_2 (glob)
134 ? b/in_b (glob)
134 ? b/in_b (glob)
135 ? in_root
135 ? in_root
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
137 ? 1/in_a_1
137 ? 1/in_a_1
138 ? in_a
138 ? in_a
139 ? ../b/1/in_b_1
139 ? ../b/1/in_b_1
140 ? ../b/2/in_b_2
140 ? ../b/2/in_b_2
141 ? ../b/in_b
141 ? ../b/in_b
142 ? ../in_root (glob)
142 ? ../in_root (glob)
143
143
144 relative paths can be requested
144 relative paths can be requested
145
145
146 $ hg status --cwd a --config ui.relative-paths=yes
146 $ hg status --cwd a --config ui.relative-paths=yes
147 ? 1/in_a_1
147 ? 1/in_a_1
148 ? in_a
148 ? in_a
149 ? ../b/1/in_b_1
149 ? ../b/1/in_b_1
150 ? ../b/2/in_b_2
150 ? ../b/2/in_b_2
151 ? ../b/in_b
151 ? ../b/in_b
152 ? ../in_root
152 ? ../in_root
153
153
154 $ hg status --cwd a . --config ui.relative-paths=legacy
154 $ hg status --cwd a . --config ui.relative-paths=legacy
155 ? 1/in_a_1
155 ? 1/in_a_1
156 ? in_a
156 ? in_a
157 $ hg status --cwd a . --config ui.relative-paths=no
157 $ hg status --cwd a . --config ui.relative-paths=no
158 ? a/1/in_a_1
158 ? a/1/in_a_1
159 ? a/in_a
159 ? a/in_a
160
160
161 commands.status.relative overrides ui.relative-paths
161 commands.status.relative overrides ui.relative-paths
162
162
163 $ cat >> $HGRCPATH <<EOF
163 $ cat >> $HGRCPATH <<EOF
164 > [ui]
164 > [ui]
165 > relative-paths = False
165 > relative-paths = False
166 > [commands]
166 > [commands]
167 > status.relative = True
167 > status.relative = True
168 > EOF
168 > EOF
169 $ hg status --cwd a
169 $ hg status --cwd a
170 ? 1/in_a_1
170 ? 1/in_a_1
171 ? in_a
171 ? in_a
172 ? ../b/1/in_b_1
172 ? ../b/1/in_b_1
173 ? ../b/2/in_b_2
173 ? ../b/2/in_b_2
174 ? ../b/in_b
174 ? ../b/in_b
175 ? ../in_root
175 ? ../in_root
176 $ HGPLAIN=1 hg status --cwd a
176 $ HGPLAIN=1 hg status --cwd a
177 ? a/1/in_a_1 (glob)
177 ? a/1/in_a_1 (glob)
178 ? a/in_a (glob)
178 ? a/in_a (glob)
179 ? b/1/in_b_1 (glob)
179 ? b/1/in_b_1 (glob)
180 ? b/2/in_b_2 (glob)
180 ? b/2/in_b_2 (glob)
181 ? b/in_b (glob)
181 ? b/in_b (glob)
182 ? in_root
182 ? in_root
183
183
184 if relative paths are explicitly off, tweakdefaults doesn't change it
184 if relative paths are explicitly off, tweakdefaults doesn't change it
185 $ cat >> $HGRCPATH <<EOF
185 $ cat >> $HGRCPATH <<EOF
186 > [commands]
186 > [commands]
187 > status.relative = False
187 > status.relative = False
188 > EOF
188 > EOF
189 $ hg status --cwd a --config ui.tweakdefaults=yes
189 $ hg status --cwd a --config ui.tweakdefaults=yes
190 ? a/1/in_a_1
190 ? a/1/in_a_1
191 ? a/in_a
191 ? a/in_a
192 ? b/1/in_b_1
192 ? b/1/in_b_1
193 ? b/2/in_b_2
193 ? b/2/in_b_2
194 ? b/in_b
194 ? b/in_b
195 ? in_root
195 ? in_root
196
196
197 $ cd ..
197 $ cd ..
198
198
199 $ hg init repo2
199 $ hg init repo2
200 $ cd repo2
200 $ cd repo2
201 $ touch modified removed deleted ignored
201 $ touch modified removed deleted ignored
202 $ echo "^ignored$" > .hgignore
202 $ echo "^ignored$" > .hgignore
203 $ hg ci -A -m 'initial checkin'
203 $ hg ci -A -m 'initial checkin'
204 adding .hgignore
204 adding .hgignore
205 adding deleted
205 adding deleted
206 adding modified
206 adding modified
207 adding removed
207 adding removed
208 $ touch modified added unknown ignored
208 $ touch modified added unknown ignored
209 $ hg add added
209 $ hg add added
210 $ hg remove removed
210 $ hg remove removed
211 $ rm deleted
211 $ rm deleted
212
212
213 hg status:
213 hg status:
214
214
215 $ hg status
215 $ hg status
216 A added
216 A added
217 R removed
217 R removed
218 ! deleted
218 ! deleted
219 ? unknown
219 ? unknown
220
220
221 hg status -n:
221 hg status -n:
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
223 added
223 added
224 removed
224 removed
225 deleted
225 deleted
226 unknown
226 unknown
227
227
228 hg status modified added removed deleted unknown never-existed ignored:
228 hg status modified added removed deleted unknown never-existed ignored:
229
229
230 $ hg status modified added removed deleted unknown never-existed ignored
230 $ hg status modified added removed deleted unknown never-existed ignored
231 never-existed: * (glob)
231 never-existed: * (glob)
232 A added
232 A added
233 R removed
233 R removed
234 ! deleted
234 ! deleted
235 ? unknown
235 ? unknown
236
236
237 $ hg copy modified copied
237 $ hg copy modified copied
238
238
239 hg status -C:
239 hg status -C:
240
240
241 $ hg status -C
241 $ hg status -C
242 A added
242 A added
243 A copied
243 A copied
244 modified
244 modified
245 R removed
245 R removed
246 ! deleted
246 ! deleted
247 ? unknown
247 ? unknown
248
248
249 hg status -A:
249 hg status -A:
250
250
251 $ hg status -A
251 $ hg status -A
252 A added
252 A added
253 A copied
253 A copied
254 modified
254 modified
255 R removed
255 R removed
256 ! deleted
256 ! deleted
257 ? unknown
257 ? unknown
258 I ignored
258 I ignored
259 C .hgignore
259 C .hgignore
260 C modified
260 C modified
261
261
262 $ hg status -A -T '{status} {path} {node|shortest}\n'
262 $ hg status -A -T '{status} {path} {node|shortest}\n'
263 A added ffff
263 A added ffff
264 A copied ffff
264 A copied ffff
265 R removed ffff
265 R removed ffff
266 ! deleted ffff
266 ! deleted ffff
267 ? unknown ffff
267 ? unknown ffff
268 I ignored ffff
268 I ignored ffff
269 C .hgignore ffff
269 C .hgignore ffff
270 C modified ffff
270 C modified ffff
271
271
272 $ hg status -A -Tjson
272 $ hg status -A -Tjson
273 [
273 [
274 {
274 {
275 "itemtype": "file",
275 "itemtype": "file",
276 "path": "added",
276 "path": "added",
277 "status": "A"
277 "status": "A"
278 },
278 },
279 {
279 {
280 "itemtype": "file",
280 "itemtype": "file",
281 "path": "copied",
281 "path": "copied",
282 "source": "modified",
282 "source": "modified",
283 "status": "A"
283 "status": "A"
284 },
284 },
285 {
285 {
286 "itemtype": "file",
286 "itemtype": "file",
287 "path": "removed",
287 "path": "removed",
288 "status": "R"
288 "status": "R"
289 },
289 },
290 {
290 {
291 "itemtype": "file",
291 "itemtype": "file",
292 "path": "deleted",
292 "path": "deleted",
293 "status": "!"
293 "status": "!"
294 },
294 },
295 {
295 {
296 "itemtype": "file",
296 "itemtype": "file",
297 "path": "unknown",
297 "path": "unknown",
298 "status": "?"
298 "status": "?"
299 },
299 },
300 {
300 {
301 "itemtype": "file",
301 "itemtype": "file",
302 "path": "ignored",
302 "path": "ignored",
303 "status": "I"
303 "status": "I"
304 },
304 },
305 {
305 {
306 "itemtype": "file",
306 "itemtype": "file",
307 "path": ".hgignore",
307 "path": ".hgignore",
308 "status": "C"
308 "status": "C"
309 },
309 },
310 {
310 {
311 "itemtype": "file",
311 "itemtype": "file",
312 "path": "modified",
312 "path": "modified",
313 "status": "C"
313 "status": "C"
314 }
314 }
315 ]
315 ]
316
316
317 $ hg status -A -Tpickle > pickle
317 $ hg status -A -Tpickle > pickle
318 >>> from __future__ import print_function
318 >>> from __future__ import print_function
319 >>> import pickle
319 >>> from mercurial import util
320 >>> from mercurial import util
320 >>> pickle = util.pickle
321 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
321 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
322 >>> for s, p in data: print("%s %s" % (s, p))
322 >>> for s, p in data: print("%s %s" % (s, p))
323 ! deleted
323 ! deleted
324 ? pickle
324 ? pickle
325 ? unknown
325 ? unknown
326 A added
326 A added
327 A copied
327 A copied
328 C .hgignore
328 C .hgignore
329 C modified
329 C modified
330 I ignored
330 I ignored
331 R removed
331 R removed
332 $ rm pickle
332 $ rm pickle
333
333
334 $ echo "^ignoreddir$" > .hgignore
334 $ echo "^ignoreddir$" > .hgignore
335 $ mkdir ignoreddir
335 $ mkdir ignoreddir
336 $ touch ignoreddir/file
336 $ touch ignoreddir/file
337
337
338 Test templater support:
338 Test templater support:
339
339
340 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
340 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
341 [M] .hgignore
341 [M] .hgignore
342 [A] added
342 [A] added
343 [A] modified -> copied
343 [A] modified -> copied
344 [R] removed
344 [R] removed
345 [!] deleted
345 [!] deleted
346 [?] ignored
346 [?] ignored
347 [?] unknown
347 [?] unknown
348 [I] ignoreddir/file
348 [I] ignoreddir/file
349 [C] modified
349 [C] modified
350 $ hg status -AT default
350 $ hg status -AT default
351 M .hgignore
351 M .hgignore
352 A added
352 A added
353 A copied
353 A copied
354 modified
354 modified
355 R removed
355 R removed
356 ! deleted
356 ! deleted
357 ? ignored
357 ? ignored
358 ? unknown
358 ? unknown
359 I ignoreddir/file
359 I ignoreddir/file
360 C modified
360 C modified
361 $ hg status -T compact
361 $ hg status -T compact
362 abort: "status" not in template map
362 abort: "status" not in template map
363 [255]
363 [255]
364
364
365 hg status ignoreddir/file:
365 hg status ignoreddir/file:
366
366
367 $ hg status ignoreddir/file
367 $ hg status ignoreddir/file
368
368
369 hg status -i ignoreddir/file:
369 hg status -i ignoreddir/file:
370
370
371 $ hg status -i ignoreddir/file
371 $ hg status -i ignoreddir/file
372 I ignoreddir/file
372 I ignoreddir/file
373 $ cd ..
373 $ cd ..
374
374
375 Check 'status -q' and some combinations
375 Check 'status -q' and some combinations
376
376
377 $ hg init repo3
377 $ hg init repo3
378 $ cd repo3
378 $ cd repo3
379 $ touch modified removed deleted ignored
379 $ touch modified removed deleted ignored
380 $ echo "^ignored$" > .hgignore
380 $ echo "^ignored$" > .hgignore
381 $ hg commit -A -m 'initial checkin'
381 $ hg commit -A -m 'initial checkin'
382 adding .hgignore
382 adding .hgignore
383 adding deleted
383 adding deleted
384 adding modified
384 adding modified
385 adding removed
385 adding removed
386 $ touch added unknown ignored
386 $ touch added unknown ignored
387 $ hg add added
387 $ hg add added
388 $ echo "test" >> modified
388 $ echo "test" >> modified
389 $ hg remove removed
389 $ hg remove removed
390 $ rm deleted
390 $ rm deleted
391 $ hg copy modified copied
391 $ hg copy modified copied
392
392
393 Specify working directory revision explicitly, that should be the same as
393 Specify working directory revision explicitly, that should be the same as
394 "hg status"
394 "hg status"
395
395
396 $ hg status --change "wdir()"
396 $ hg status --change "wdir()"
397 M modified
397 M modified
398 A added
398 A added
399 A copied
399 A copied
400 R removed
400 R removed
401 ! deleted
401 ! deleted
402 ? unknown
402 ? unknown
403
403
404 Run status with 2 different flags.
404 Run status with 2 different flags.
405 Check if result is the same or different.
405 Check if result is the same or different.
406 If result is not as expected, raise error
406 If result is not as expected, raise error
407
407
408 $ assert() {
408 $ assert() {
409 > hg status $1 > ../a
409 > hg status $1 > ../a
410 > hg status $2 > ../b
410 > hg status $2 > ../b
411 > if diff ../a ../b > /dev/null; then
411 > if diff ../a ../b > /dev/null; then
412 > out=0
412 > out=0
413 > else
413 > else
414 > out=1
414 > out=1
415 > fi
415 > fi
416 > if [ $3 -eq 0 ]; then
416 > if [ $3 -eq 0 ]; then
417 > df="same"
417 > df="same"
418 > else
418 > else
419 > df="different"
419 > df="different"
420 > fi
420 > fi
421 > if [ $out -ne $3 ]; then
421 > if [ $out -ne $3 ]; then
422 > echo "Error on $1 and $2, should be $df."
422 > echo "Error on $1 and $2, should be $df."
423 > fi
423 > fi
424 > }
424 > }
425
425
426 Assert flag1 flag2 [0-same | 1-different]
426 Assert flag1 flag2 [0-same | 1-different]
427
427
428 $ assert "-q" "-mard" 0
428 $ assert "-q" "-mard" 0
429 $ assert "-A" "-marduicC" 0
429 $ assert "-A" "-marduicC" 0
430 $ assert "-qA" "-mardcC" 0
430 $ assert "-qA" "-mardcC" 0
431 $ assert "-qAui" "-A" 0
431 $ assert "-qAui" "-A" 0
432 $ assert "-qAu" "-marducC" 0
432 $ assert "-qAu" "-marducC" 0
433 $ assert "-qAi" "-mardicC" 0
433 $ assert "-qAi" "-mardicC" 0
434 $ assert "-qu" "-u" 0
434 $ assert "-qu" "-u" 0
435 $ assert "-q" "-u" 1
435 $ assert "-q" "-u" 1
436 $ assert "-m" "-a" 1
436 $ assert "-m" "-a" 1
437 $ assert "-r" "-d" 1
437 $ assert "-r" "-d" 1
438 $ cd ..
438 $ cd ..
439
439
440 $ hg init repo4
440 $ hg init repo4
441 $ cd repo4
441 $ cd repo4
442 $ touch modified removed deleted
442 $ touch modified removed deleted
443 $ hg ci -q -A -m 'initial checkin'
443 $ hg ci -q -A -m 'initial checkin'
444 $ touch added unknown
444 $ touch added unknown
445 $ hg add added
445 $ hg add added
446 $ hg remove removed
446 $ hg remove removed
447 $ rm deleted
447 $ rm deleted
448 $ echo x > modified
448 $ echo x > modified
449 $ hg copy modified copied
449 $ hg copy modified copied
450 $ hg ci -m 'test checkin' -d "1000001 0"
450 $ hg ci -m 'test checkin' -d "1000001 0"
451 $ rm *
451 $ rm *
452 $ touch unrelated
452 $ touch unrelated
453 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
453 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
454
454
455 hg status --change 1:
455 hg status --change 1:
456
456
457 $ hg status --change 1
457 $ hg status --change 1
458 M modified
458 M modified
459 A added
459 A added
460 A copied
460 A copied
461 R removed
461 R removed
462
462
463 hg status --change 1 unrelated:
463 hg status --change 1 unrelated:
464
464
465 $ hg status --change 1 unrelated
465 $ hg status --change 1 unrelated
466
466
467 hg status -C --change 1 added modified copied removed deleted:
467 hg status -C --change 1 added modified copied removed deleted:
468
468
469 $ hg status -C --change 1 added modified copied removed deleted
469 $ hg status -C --change 1 added modified copied removed deleted
470 M modified
470 M modified
471 A added
471 A added
472 A copied
472 A copied
473 modified
473 modified
474 R removed
474 R removed
475
475
476 hg status -A --change 1 and revset:
476 hg status -A --change 1 and revset:
477
477
478 $ hg status -A --change '1|1'
478 $ hg status -A --change '1|1'
479 M modified
479 M modified
480 A added
480 A added
481 A copied
481 A copied
482 modified
482 modified
483 R removed
483 R removed
484 C deleted
484 C deleted
485
485
486 $ cd ..
486 $ cd ..
487
487
488 hg status with --rev and reverted changes:
488 hg status with --rev and reverted changes:
489
489
490 $ hg init reverted-changes-repo
490 $ hg init reverted-changes-repo
491 $ cd reverted-changes-repo
491 $ cd reverted-changes-repo
492 $ echo a > file
492 $ echo a > file
493 $ hg add file
493 $ hg add file
494 $ hg ci -m a
494 $ hg ci -m a
495 $ echo b > file
495 $ echo b > file
496 $ hg ci -m b
496 $ hg ci -m b
497
497
498 reverted file should appear clean
498 reverted file should appear clean
499
499
500 $ hg revert -r 0 .
500 $ hg revert -r 0 .
501 reverting file
501 reverting file
502 $ hg status -A --rev 0
502 $ hg status -A --rev 0
503 C file
503 C file
504
504
505 #if execbit
505 #if execbit
506 reverted file with changed flag should appear modified
506 reverted file with changed flag should appear modified
507
507
508 $ chmod +x file
508 $ chmod +x file
509 $ hg status -A --rev 0
509 $ hg status -A --rev 0
510 M file
510 M file
511
511
512 $ hg revert -r 0 .
512 $ hg revert -r 0 .
513 reverting file
513 reverting file
514
514
515 reverted and committed file with changed flag should appear modified
515 reverted and committed file with changed flag should appear modified
516
516
517 $ hg co -C .
517 $ hg co -C .
518 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
518 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
519 $ chmod +x file
519 $ chmod +x file
520 $ hg ci -m 'change flag'
520 $ hg ci -m 'change flag'
521 $ hg status -A --rev 1 --rev 2
521 $ hg status -A --rev 1 --rev 2
522 M file
522 M file
523 $ hg diff -r 1 -r 2
523 $ hg diff -r 1 -r 2
524
524
525 #endif
525 #endif
526
526
527 $ cd ..
527 $ cd ..
528
528
529 hg status of binary file starting with '\1\n', a separator for metadata:
529 hg status of binary file starting with '\1\n', a separator for metadata:
530
530
531 $ hg init repo5
531 $ hg init repo5
532 $ cd repo5
532 $ cd repo5
533 >>> open("010a", r"wb").write(b"\1\nfoo") and None
533 >>> open("010a", r"wb").write(b"\1\nfoo") and None
534 $ hg ci -q -A -m 'initial checkin'
534 $ hg ci -q -A -m 'initial checkin'
535 $ hg status -A
535 $ hg status -A
536 C 010a
536 C 010a
537
537
538 >>> open("010a", r"wb").write(b"\1\nbar") and None
538 >>> open("010a", r"wb").write(b"\1\nbar") and None
539 $ hg status -A
539 $ hg status -A
540 M 010a
540 M 010a
541 $ hg ci -q -m 'modify 010a'
541 $ hg ci -q -m 'modify 010a'
542 $ hg status -A --rev 0:1
542 $ hg status -A --rev 0:1
543 M 010a
543 M 010a
544
544
545 $ touch empty
545 $ touch empty
546 $ hg ci -q -A -m 'add another file'
546 $ hg ci -q -A -m 'add another file'
547 $ hg status -A --rev 1:2 010a
547 $ hg status -A --rev 1:2 010a
548 C 010a
548 C 010a
549
549
550 $ cd ..
550 $ cd ..
551
551
552 test "hg status" with "directory pattern" which matches against files
552 test "hg status" with "directory pattern" which matches against files
553 only known on target revision.
553 only known on target revision.
554
554
555 $ hg init repo6
555 $ hg init repo6
556 $ cd repo6
556 $ cd repo6
557
557
558 $ echo a > a.txt
558 $ echo a > a.txt
559 $ hg add a.txt
559 $ hg add a.txt
560 $ hg commit -m '#0'
560 $ hg commit -m '#0'
561 $ mkdir -p 1/2/3/4/5
561 $ mkdir -p 1/2/3/4/5
562 $ echo b > 1/2/3/4/5/b.txt
562 $ echo b > 1/2/3/4/5/b.txt
563 $ hg add 1/2/3/4/5/b.txt
563 $ hg add 1/2/3/4/5/b.txt
564 $ hg commit -m '#1'
564 $ hg commit -m '#1'
565
565
566 $ hg update -C 0 > /dev/null
566 $ hg update -C 0 > /dev/null
567 $ hg status -A
567 $ hg status -A
568 C a.txt
568 C a.txt
569
569
570 the directory matching against specified pattern should be removed,
570 the directory matching against specified pattern should be removed,
571 because directory existence prevents 'dirstate.walk()' from showing
571 because directory existence prevents 'dirstate.walk()' from showing
572 warning message about such pattern.
572 warning message about such pattern.
573
573
574 $ test ! -d 1
574 $ test ! -d 1
575 $ hg status -A --rev 1 1/2/3/4/5/b.txt
575 $ hg status -A --rev 1 1/2/3/4/5/b.txt
576 R 1/2/3/4/5/b.txt
576 R 1/2/3/4/5/b.txt
577 $ hg status -A --rev 1 1/2/3/4/5
577 $ hg status -A --rev 1 1/2/3/4/5
578 R 1/2/3/4/5/b.txt
578 R 1/2/3/4/5/b.txt
579 $ hg status -A --rev 1 1/2/3
579 $ hg status -A --rev 1 1/2/3
580 R 1/2/3/4/5/b.txt
580 R 1/2/3/4/5/b.txt
581 $ hg status -A --rev 1 1
581 $ hg status -A --rev 1 1
582 R 1/2/3/4/5/b.txt
582 R 1/2/3/4/5/b.txt
583
583
584 $ hg status --config ui.formatdebug=True --rev 1 1
584 $ hg status --config ui.formatdebug=True --rev 1 1
585 status = [
585 status = [
586 {
586 {
587 'itemtype': 'file',
587 'itemtype': 'file',
588 'path': '1/2/3/4/5/b.txt',
588 'path': '1/2/3/4/5/b.txt',
589 'status': 'R'
589 'status': 'R'
590 },
590 },
591 ]
591 ]
592
592
593 #if windows
593 #if windows
594 $ hg --config ui.slash=false status -A --rev 1 1
594 $ hg --config ui.slash=false status -A --rev 1 1
595 R 1\2\3\4\5\b.txt
595 R 1\2\3\4\5\b.txt
596 #endif
596 #endif
597
597
598 $ cd ..
598 $ cd ..
599
599
600 Status after move overwriting a file (issue4458)
600 Status after move overwriting a file (issue4458)
601 =================================================
601 =================================================
602
602
603
603
604 $ hg init issue4458
604 $ hg init issue4458
605 $ cd issue4458
605 $ cd issue4458
606 $ echo a > a
606 $ echo a > a
607 $ echo b > b
607 $ echo b > b
608 $ hg commit -Am base
608 $ hg commit -Am base
609 adding a
609 adding a
610 adding b
610 adding b
611
611
612
612
613 with --force
613 with --force
614
614
615 $ hg mv b --force a
615 $ hg mv b --force a
616 $ hg st --copies
616 $ hg st --copies
617 M a
617 M a
618 b
618 b
619 R b
619 R b
620 $ hg revert --all
620 $ hg revert --all
621 reverting a
621 reverting a
622 undeleting b
622 undeleting b
623 $ rm *.orig
623 $ rm *.orig
624
624
625 without force
625 without force
626
626
627 $ hg rm a
627 $ hg rm a
628 $ hg st --copies
628 $ hg st --copies
629 R a
629 R a
630 $ hg mv b a
630 $ hg mv b a
631 $ hg st --copies
631 $ hg st --copies
632 M a
632 M a
633 b
633 b
634 R b
634 R b
635
635
636 using ui.statuscopies setting
636 using ui.statuscopies setting
637 $ hg st --config ui.statuscopies=true
637 $ hg st --config ui.statuscopies=true
638 M a
638 M a
639 b
639 b
640 R b
640 R b
641 $ hg st --config ui.statuscopies=false
641 $ hg st --config ui.statuscopies=false
642 M a
642 M a
643 R b
643 R b
644 $ hg st --config ui.tweakdefaults=yes
644 $ hg st --config ui.tweakdefaults=yes
645 M a
645 M a
646 b
646 b
647 R b
647 R b
648
648
649 using log status template (issue5155)
649 using log status template (issue5155)
650 $ hg log -Tstatus -r 'wdir()' -C
650 $ hg log -Tstatus -r 'wdir()' -C
651 changeset: 2147483647:ffffffffffff
651 changeset: 2147483647:ffffffffffff
652 parent: 0:8c55c58b4c0e
652 parent: 0:8c55c58b4c0e
653 user: test
653 user: test
654 date: * (glob)
654 date: * (glob)
655 files:
655 files:
656 M a
656 M a
657 b
657 b
658 R b
658 R b
659
659
660 $ hg log -GTstatus -r 'wdir()' -C
660 $ hg log -GTstatus -r 'wdir()' -C
661 o changeset: 2147483647:ffffffffffff
661 o changeset: 2147483647:ffffffffffff
662 | parent: 0:8c55c58b4c0e
662 | parent: 0:8c55c58b4c0e
663 ~ user: test
663 ~ user: test
664 date: * (glob)
664 date: * (glob)
665 files:
665 files:
666 M a
666 M a
667 b
667 b
668 R b
668 R b
669
669
670
670
671 Other "bug" highlight, the revision status does not report the copy information.
671 Other "bug" highlight, the revision status does not report the copy information.
672 This is buggy behavior.
672 This is buggy behavior.
673
673
674 $ hg commit -m 'blah'
674 $ hg commit -m 'blah'
675 $ hg st --copies --change .
675 $ hg st --copies --change .
676 M a
676 M a
677 R b
677 R b
678
678
679 using log status template, the copy information is displayed correctly.
679 using log status template, the copy information is displayed correctly.
680 $ hg log -Tstatus -r. -C
680 $ hg log -Tstatus -r. -C
681 changeset: 1:6685fde43d21
681 changeset: 1:6685fde43d21
682 tag: tip
682 tag: tip
683 user: test
683 user: test
684 date: * (glob)
684 date: * (glob)
685 summary: blah
685 summary: blah
686 files:
686 files:
687 M a
687 M a
688 b
688 b
689 R b
689 R b
690
690
691
691
692 $ cd ..
692 $ cd ..
693
693
694 Make sure .hg doesn't show up even as a symlink
694 Make sure .hg doesn't show up even as a symlink
695
695
696 $ hg init repo0
696 $ hg init repo0
697 $ mkdir symlink-repo0
697 $ mkdir symlink-repo0
698 $ cd symlink-repo0
698 $ cd symlink-repo0
699 $ ln -s ../repo0/.hg
699 $ ln -s ../repo0/.hg
700 $ hg status
700 $ hg status
701
701
702 If the size hasn’t changed but mtime has, status needs to read the contents
702 If the size hasn’t changed but mtime has, status needs to read the contents
703 of the file to check whether it has changed
703 of the file to check whether it has changed
704
704
705 $ echo 1 > a
705 $ echo 1 > a
706 $ echo 1 > b
706 $ echo 1 > b
707 $ touch -t 200102030000 a b
707 $ touch -t 200102030000 a b
708 $ hg commit -Aqm '#0'
708 $ hg commit -Aqm '#0'
709 $ echo 2 > a
709 $ echo 2 > a
710 $ touch -t 200102040000 a b
710 $ touch -t 200102040000 a b
711 $ hg status
711 $ hg status
712 M a
712 M a
713
713
714 Asking specifically for the status of a deleted/removed file
714 Asking specifically for the status of a deleted/removed file
715
715
716 $ rm a
716 $ rm a
717 $ rm b
717 $ rm b
718 $ hg status a
718 $ hg status a
719 ! a
719 ! a
720 $ hg rm a
720 $ hg rm a
721 $ hg rm b
721 $ hg rm b
722 $ hg status a
722 $ hg status a
723 R a
723 R a
724 $ hg commit -qm '#1'
724 $ hg commit -qm '#1'
725 $ hg status a
725 $ hg status a
726 a: $ENOENT$
726 a: $ENOENT$
727
727
728 Check using include flag with pattern when status does not need to traverse
728 Check using include flag with pattern when status does not need to traverse
729 the working directory (issue6483)
729 the working directory (issue6483)
730
730
731 $ cd ..
731 $ cd ..
732 $ hg init issue6483
732 $ hg init issue6483
733 $ cd issue6483
733 $ cd issue6483
734 $ touch a.py b.rs
734 $ touch a.py b.rs
735 $ hg add a.py b.rs
735 $ hg add a.py b.rs
736 $ hg st -aI "*.py"
736 $ hg st -aI "*.py"
737 A a.py
737 A a.py
738
738
739 Also check exclude pattern
739 Also check exclude pattern
740
740
741 $ hg st -aX "*.rs"
741 $ hg st -aX "*.rs"
742 A a.py
742 A a.py
743
743
744 issue6335
744 issue6335
745 When a directory containing a tracked file gets symlinked, as of 5.8
745 When a directory containing a tracked file gets symlinked, as of 5.8
746 `hg st` only gives the correct answer about clean (or deleted) files
746 `hg st` only gives the correct answer about clean (or deleted) files
747 if also listing unknowns.
747 if also listing unknowns.
748 The tree-based dirstate and status algorithm fix this:
748 The tree-based dirstate and status algorithm fix this:
749
749
750 #if symlink no-dirstate-v1 rust
750 #if symlink no-dirstate-v1 rust
751
751
752 $ cd ..
752 $ cd ..
753 $ hg init issue6335
753 $ hg init issue6335
754 $ cd issue6335
754 $ cd issue6335
755 $ mkdir foo
755 $ mkdir foo
756 $ touch foo/a
756 $ touch foo/a
757 $ hg ci -Ama
757 $ hg ci -Ama
758 adding foo/a
758 adding foo/a
759 $ mv foo bar
759 $ mv foo bar
760 $ ln -s bar foo
760 $ ln -s bar foo
761 $ hg status
761 $ hg status
762 ! foo/a
762 ! foo/a
763 ? bar/a
763 ? bar/a
764 ? foo
764 ? foo
765
765
766 $ hg status -c # incorrect output without the Rust implementation
766 $ hg status -c # incorrect output without the Rust implementation
767 $ hg status -cu
767 $ hg status -cu
768 ? bar/a
768 ? bar/a
769 ? foo
769 ? foo
770 $ hg status -d # incorrect output without the Rust implementation
770 $ hg status -d # incorrect output without the Rust implementation
771 ! foo/a
771 ! foo/a
772 $ hg status -du
772 $ hg status -du
773 ! foo/a
773 ! foo/a
774 ? bar/a
774 ? bar/a
775 ? foo
775 ? foo
776
776
777 #endif
777 #endif
778
778
779
779
780 Create a repo with files in each possible status
780 Create a repo with files in each possible status
781
781
782 $ cd ..
782 $ cd ..
783 $ hg init repo7
783 $ hg init repo7
784 $ cd repo7
784 $ cd repo7
785 $ mkdir subdir
785 $ mkdir subdir
786 $ touch clean modified deleted removed
786 $ touch clean modified deleted removed
787 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
787 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
788 $ echo ignored > .hgignore
788 $ echo ignored > .hgignore
789 $ hg ci -Aqm '#0'
789 $ hg ci -Aqm '#0'
790 $ echo 1 > modified
790 $ echo 1 > modified
791 $ echo 1 > subdir/modified
791 $ echo 1 > subdir/modified
792 $ rm deleted
792 $ rm deleted
793 $ rm subdir/deleted
793 $ rm subdir/deleted
794 $ hg rm removed
794 $ hg rm removed
795 $ hg rm subdir/removed
795 $ hg rm subdir/removed
796 $ touch unknown ignored
796 $ touch unknown ignored
797 $ touch subdir/unknown subdir/ignored
797 $ touch subdir/unknown subdir/ignored
798
798
799 Check the output
799 Check the output
800
800
801 $ hg status
801 $ hg status
802 M modified
802 M modified
803 M subdir/modified
803 M subdir/modified
804 R removed
804 R removed
805 R subdir/removed
805 R subdir/removed
806 ! deleted
806 ! deleted
807 ! subdir/deleted
807 ! subdir/deleted
808 ? subdir/unknown
808 ? subdir/unknown
809 ? unknown
809 ? unknown
810
810
811 $ hg status -mard
811 $ hg status -mard
812 M modified
812 M modified
813 M subdir/modified
813 M subdir/modified
814 R removed
814 R removed
815 R subdir/removed
815 R subdir/removed
816 ! deleted
816 ! deleted
817 ! subdir/deleted
817 ! subdir/deleted
818
818
819 $ hg status -A
819 $ hg status -A
820 M modified
820 M modified
821 M subdir/modified
821 M subdir/modified
822 R removed
822 R removed
823 R subdir/removed
823 R subdir/removed
824 ! deleted
824 ! deleted
825 ! subdir/deleted
825 ! subdir/deleted
826 ? subdir/unknown
826 ? subdir/unknown
827 ? unknown
827 ? unknown
828 I ignored
828 I ignored
829 I subdir/ignored
829 I subdir/ignored
830 C .hgignore
830 C .hgignore
831 C clean
831 C clean
832 C subdir/clean
832 C subdir/clean
833
833
834 Note: `hg status some-name` creates a patternmatcher which is not supported
834 Note: `hg status some-name` creates a patternmatcher which is not supported
835 yet by the Rust implementation of status, but includematcher is supported.
835 yet by the Rust implementation of status, but includematcher is supported.
836 --include is used below for that reason
836 --include is used below for that reason
837
837
838 #if unix-permissions
838 #if unix-permissions
839
839
840 Not having permission to read a directory that contains tracked files makes
840 Not having permission to read a directory that contains tracked files makes
841 status emit a warning then behave as if the directory was empty or removed
841 status emit a warning then behave as if the directory was empty or removed
842 entirely:
842 entirely:
843
843
844 $ chmod 0 subdir
844 $ chmod 0 subdir
845 $ hg status --include subdir
845 $ hg status --include subdir
846 subdir: Permission denied
846 subdir: Permission denied
847 R subdir/removed
847 R subdir/removed
848 ! subdir/clean
848 ! subdir/clean
849 ! subdir/deleted
849 ! subdir/deleted
850 ! subdir/modified
850 ! subdir/modified
851 $ chmod 755 subdir
851 $ chmod 755 subdir
852
852
853 #endif
853 #endif
854
854
855 Remove a directory that contains tracked files
855 Remove a directory that contains tracked files
856
856
857 $ rm -r subdir
857 $ rm -r subdir
858 $ hg status --include subdir
858 $ hg status --include subdir
859 R subdir/removed
859 R subdir/removed
860 ! subdir/clean
860 ! subdir/clean
861 ! subdir/deleted
861 ! subdir/deleted
862 ! subdir/modified
862 ! subdir/modified
863
863
864 … and replace it by a file
864 … and replace it by a file
865
865
866 $ touch subdir
866 $ touch subdir
867 $ hg status --include subdir
867 $ hg status --include subdir
868 R subdir/removed
868 R subdir/removed
869 ! subdir/clean
869 ! subdir/clean
870 ! subdir/deleted
870 ! subdir/deleted
871 ! subdir/modified
871 ! subdir/modified
872 ? subdir
872 ? subdir
873
873
874 Replaced a deleted or removed file with a directory
874 Replaced a deleted or removed file with a directory
875
875
876 $ mkdir deleted removed
876 $ mkdir deleted removed
877 $ touch deleted/1 removed/1
877 $ touch deleted/1 removed/1
878 $ hg status --include deleted --include removed
878 $ hg status --include deleted --include removed
879 R removed
879 R removed
880 ! deleted
880 ! deleted
881 ? deleted/1
881 ? deleted/1
882 ? removed/1
882 ? removed/1
883 $ hg add removed/1
883 $ hg add removed/1
884 $ hg status --include deleted --include removed
884 $ hg status --include deleted --include removed
885 A removed/1
885 A removed/1
886 R removed
886 R removed
887 ! deleted
887 ! deleted
888 ? deleted/1
888 ? deleted/1
889
889
890 Deeply nested files in an ignored directory are still listed on request
890 Deeply nested files in an ignored directory are still listed on request
891
891
892 $ echo ignored-dir >> .hgignore
892 $ echo ignored-dir >> .hgignore
893 $ mkdir ignored-dir
893 $ mkdir ignored-dir
894 $ mkdir ignored-dir/subdir
894 $ mkdir ignored-dir/subdir
895 $ touch ignored-dir/subdir/1
895 $ touch ignored-dir/subdir/1
896 $ hg status --ignored
896 $ hg status --ignored
897 I ignored
897 I ignored
898 I ignored-dir/subdir/1
898 I ignored-dir/subdir/1
899
899
900 Check using include flag while listing ignored composes correctly (issue6514)
900 Check using include flag while listing ignored composes correctly (issue6514)
901
901
902 $ cd ..
902 $ cd ..
903 $ hg init issue6514
903 $ hg init issue6514
904 $ cd issue6514
904 $ cd issue6514
905 $ mkdir ignored-folder
905 $ mkdir ignored-folder
906 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
906 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
907 $ cat >.hgignore <<EOF
907 $ cat >.hgignore <<EOF
908 > A.hs
908 > A.hs
909 > B.hs
909 > B.hs
910 > ignored-folder/
910 > ignored-folder/
911 > EOF
911 > EOF
912 $ hg st -i -I 're:.*\.hs$'
912 $ hg st -i -I 're:.*\.hs$'
913 I A.hs
913 I A.hs
914 I B.hs
914 I B.hs
915 I ignored-folder/ctest.hs
915 I ignored-folder/ctest.hs
916
916
917 #if rust dirstate-v2
917 #if rust dirstate-v2
918
918
919 Check read_dir caching
919 Check read_dir caching
920
920
921 $ cd ..
921 $ cd ..
922 $ hg init repo8
922 $ hg init repo8
923 $ cd repo8
923 $ cd repo8
924 $ mkdir subdir
924 $ mkdir subdir
925 $ touch subdir/a subdir/b
925 $ touch subdir/a subdir/b
926 $ hg ci -Aqm '#0'
926 $ hg ci -Aqm '#0'
927
927
928 The cached mtime is initially unset
928 The cached mtime is initially unset
929
929
930 $ hg debugdirstate --all --no-dates | grep '^ '
930 $ hg debugdirstate --all --no-dates | grep '^ '
931 0 -1 unset subdir
931 0 -1 unset subdir
932
932
933 It is still not set when there are unknown files
933 It is still not set when there are unknown files
934
934
935 $ touch subdir/unknown
935 $ touch subdir/unknown
936 $ hg status
936 $ hg status
937 ? subdir/unknown
937 ? subdir/unknown
938 $ hg debugdirstate --all --no-dates | grep '^ '
938 $ hg debugdirstate --all --no-dates | grep '^ '
939 0 -1 unset subdir
939 0 -1 unset subdir
940
940
941 Now the directory is eligible for caching, so its mtime is save in the dirstate
941 Now the directory is eligible for caching, so its mtime is save in the dirstate
942
942
943 $ rm subdir/unknown
943 $ rm subdir/unknown
944 $ sleep 0.1 # ensure the kernel’s internal clock for mtimes has ticked
944 $ sleep 0.1 # ensure the kernel’s internal clock for mtimes has ticked
945 $ hg status
945 $ hg status
946 $ hg debugdirstate --all --no-dates | grep '^ '
946 $ hg debugdirstate --all --no-dates | grep '^ '
947 0 -1 set subdir
947 0 -1 set subdir
948
948
949 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
949 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
950
950
951 $ hg status
951 $ hg status
952
952
953 Creating a new file changes the directory’s mtime, invalidating the cache
953 Creating a new file changes the directory’s mtime, invalidating the cache
954
954
955 $ touch subdir/unknown
955 $ touch subdir/unknown
956 $ hg status
956 $ hg status
957 ? subdir/unknown
957 ? subdir/unknown
958
958
959 $ rm subdir/unknown
959 $ rm subdir/unknown
960 $ hg status
960 $ hg status
961
961
962 Removing a node from the dirstate resets the cache for its parent directory
962 Removing a node from the dirstate resets the cache for its parent directory
963
963
964 $ hg forget subdir/a
964 $ hg forget subdir/a
965 $ hg debugdirstate --all --no-dates | grep '^ '
965 $ hg debugdirstate --all --no-dates | grep '^ '
966 0 -1 set subdir
966 0 -1 set subdir
967 $ hg ci -qm '#1'
967 $ hg ci -qm '#1'
968 $ hg debugdirstate --all --no-dates | grep '^ '
968 $ hg debugdirstate --all --no-dates | grep '^ '
969 0 -1 unset subdir
969 0 -1 unset subdir
970 $ hg status
970 $ hg status
971 ? subdir/a
971 ? subdir/a
972
972
973 #endif
973 #endif
@@ -1,365 +1,366 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """
2 """
3 Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`.
3 Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`.
4 """
4 """
5 from __future__ import absolute_import
5 from __future__ import absolute_import
6
6
7 import contextlib
7 import contextlib
8 import errno
8 import errno
9 import os
9 import os
10 import pickle
10 import signal
11 import signal
11 import subprocess
12 import subprocess
12 import sys
13 import sys
13 import tempfile
14 import tempfile
14 import unittest
15 import unittest
15
16
16 from mercurial import pycompat, util
17 from mercurial import pycompat, util
17
18
18
19
19 if pycompat.ispy3:
20 if pycompat.ispy3:
20
21
21 def set_noninheritable(fd):
22 def set_noninheritable(fd):
22 # On Python 3, file descriptors are non-inheritable by default.
23 # On Python 3, file descriptors are non-inheritable by default.
23 pass
24 pass
24
25
25
26
26 else:
27 else:
27 if pycompat.iswindows:
28 if pycompat.iswindows:
28 # unused
29 # unused
29 set_noninheritable = None
30 set_noninheritable = None
30 else:
31 else:
31 import fcntl
32 import fcntl
32
33
33 def set_noninheritable(fd):
34 def set_noninheritable(fd):
34 old = fcntl.fcntl(fd, fcntl.F_GETFD)
35 old = fcntl.fcntl(fd, fcntl.F_GETFD)
35 fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
36 fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
36
37
37
38
38 TEST_BUFFERING_CHILD_SCRIPT = r'''
39 TEST_BUFFERING_CHILD_SCRIPT = r'''
39 import os
40 import os
40
41
41 from mercurial import dispatch
42 from mercurial import dispatch
42 from mercurial.utils import procutil
43 from mercurial.utils import procutil
43
44
44 dispatch.initstdio()
45 dispatch.initstdio()
45 procutil.{stream}.write(b'aaa')
46 procutil.{stream}.write(b'aaa')
46 os.write(procutil.{stream}.fileno(), b'[written aaa]')
47 os.write(procutil.{stream}.fileno(), b'[written aaa]')
47 procutil.{stream}.write(b'bbb\n')
48 procutil.{stream}.write(b'bbb\n')
48 os.write(procutil.{stream}.fileno(), b'[written bbb\\n]')
49 os.write(procutil.{stream}.fileno(), b'[written bbb\\n]')
49 '''
50 '''
50 UNBUFFERED = b'aaa[written aaa]bbb\n[written bbb\\n]'
51 UNBUFFERED = b'aaa[written aaa]bbb\n[written bbb\\n]'
51 LINE_BUFFERED = b'[written aaa]aaabbb\n[written bbb\\n]'
52 LINE_BUFFERED = b'[written aaa]aaabbb\n[written bbb\\n]'
52 FULLY_BUFFERED = b'[written aaa][written bbb\\n]aaabbb\n'
53 FULLY_BUFFERED = b'[written aaa][written bbb\\n]aaabbb\n'
53
54
54
55
55 TEST_LARGE_WRITE_CHILD_SCRIPT = r'''
56 TEST_LARGE_WRITE_CHILD_SCRIPT = r'''
56 import os
57 import os
57 import signal
58 import signal
58 import sys
59 import sys
59
60
60 from mercurial import dispatch
61 from mercurial import dispatch
61 from mercurial.utils import procutil
62 from mercurial.utils import procutil
62
63
63 signal.signal(signal.SIGINT, lambda *x: None)
64 signal.signal(signal.SIGINT, lambda *x: None)
64 dispatch.initstdio()
65 dispatch.initstdio()
65 write_result = procutil.{stream}.write(b'x' * 1048576)
66 write_result = procutil.{stream}.write(b'x' * 1048576)
66 with os.fdopen(
67 with os.fdopen(
67 os.open({write_result_fn!r}, os.O_WRONLY | getattr(os, 'O_TEMPORARY', 0)),
68 os.open({write_result_fn!r}, os.O_WRONLY | getattr(os, 'O_TEMPORARY', 0)),
68 'w',
69 'w',
69 ) as write_result_f:
70 ) as write_result_f:
70 write_result_f.write(str(write_result))
71 write_result_f.write(str(write_result))
71 '''
72 '''
72
73
73
74
74 TEST_BROKEN_PIPE_CHILD_SCRIPT = r'''
75 TEST_BROKEN_PIPE_CHILD_SCRIPT = r'''
75 import os
76 import os
76 import pickle
77 import pickle
77
78
78 from mercurial import dispatch
79 from mercurial import dispatch
79 from mercurial.utils import procutil
80 from mercurial.utils import procutil
80
81
81 dispatch.initstdio()
82 dispatch.initstdio()
82 procutil.stdin.read(1) # wait until parent process closed pipe
83 procutil.stdin.read(1) # wait until parent process closed pipe
83 try:
84 try:
84 procutil.{stream}.write(b'test')
85 procutil.{stream}.write(b'test')
85 procutil.{stream}.flush()
86 procutil.{stream}.flush()
86 except EnvironmentError as e:
87 except EnvironmentError as e:
87 with os.fdopen(
88 with os.fdopen(
88 os.open(
89 os.open(
89 {err_fn!r},
90 {err_fn!r},
90 os.O_WRONLY
91 os.O_WRONLY
91 | getattr(os, 'O_BINARY', 0)
92 | getattr(os, 'O_BINARY', 0)
92 | getattr(os, 'O_TEMPORARY', 0),
93 | getattr(os, 'O_TEMPORARY', 0),
93 ),
94 ),
94 'wb',
95 'wb',
95 ) as err_f:
96 ) as err_f:
96 pickle.dump(e, err_f)
97 pickle.dump(e, err_f)
97 # Exit early to suppress further broken pipe errors at interpreter shutdown.
98 # Exit early to suppress further broken pipe errors at interpreter shutdown.
98 os._exit(0)
99 os._exit(0)
99 '''
100 '''
100
101
101
102
102 @contextlib.contextmanager
103 @contextlib.contextmanager
103 def _closing(fds):
104 def _closing(fds):
104 try:
105 try:
105 yield
106 yield
106 finally:
107 finally:
107 for fd in fds:
108 for fd in fds:
108 try:
109 try:
109 os.close(fd)
110 os.close(fd)
110 except EnvironmentError:
111 except EnvironmentError:
111 pass
112 pass
112
113
113
114
114 # In the following, we set the FDs non-inheritable mainly to make it possible
115 # In the following, we set the FDs non-inheritable mainly to make it possible
115 # for tests to close the receiving end of the pipe / PTYs.
116 # for tests to close the receiving end of the pipe / PTYs.
116
117
117
118
118 @contextlib.contextmanager
119 @contextlib.contextmanager
119 def _devnull():
120 def _devnull():
120 devnull = os.open(os.devnull, os.O_WRONLY)
121 devnull = os.open(os.devnull, os.O_WRONLY)
121 # We don't have a receiving end, so it's not worth the effort on Python 2
122 # We don't have a receiving end, so it's not worth the effort on Python 2
122 # on Windows to make the FD non-inheritable.
123 # on Windows to make the FD non-inheritable.
123 with _closing([devnull]):
124 with _closing([devnull]):
124 yield (None, devnull)
125 yield (None, devnull)
125
126
126
127
127 @contextlib.contextmanager
128 @contextlib.contextmanager
128 def _pipes():
129 def _pipes():
129 rwpair = os.pipe()
130 rwpair = os.pipe()
130 # Pipes are already non-inheritable on Windows.
131 # Pipes are already non-inheritable on Windows.
131 if not pycompat.iswindows:
132 if not pycompat.iswindows:
132 set_noninheritable(rwpair[0])
133 set_noninheritable(rwpair[0])
133 set_noninheritable(rwpair[1])
134 set_noninheritable(rwpair[1])
134 with _closing(rwpair):
135 with _closing(rwpair):
135 yield rwpair
136 yield rwpair
136
137
137
138
138 @contextlib.contextmanager
139 @contextlib.contextmanager
139 def _ptys():
140 def _ptys():
140 if pycompat.iswindows:
141 if pycompat.iswindows:
141 raise unittest.SkipTest("PTYs are not supported on Windows")
142 raise unittest.SkipTest("PTYs are not supported on Windows")
142 import pty
143 import pty
143 import tty
144 import tty
144
145
145 rwpair = pty.openpty()
146 rwpair = pty.openpty()
146 set_noninheritable(rwpair[0])
147 set_noninheritable(rwpair[0])
147 set_noninheritable(rwpair[1])
148 set_noninheritable(rwpair[1])
148 with _closing(rwpair):
149 with _closing(rwpair):
149 tty.setraw(rwpair[0])
150 tty.setraw(rwpair[0])
150 yield rwpair
151 yield rwpair
151
152
152
153
153 def _readall(fd, buffer_size, initial_buf=None):
154 def _readall(fd, buffer_size, initial_buf=None):
154 buf = initial_buf or []
155 buf = initial_buf or []
155 while True:
156 while True:
156 try:
157 try:
157 s = os.read(fd, buffer_size)
158 s = os.read(fd, buffer_size)
158 except OSError as e:
159 except OSError as e:
159 if e.errno == errno.EIO:
160 if e.errno == errno.EIO:
160 # If the child-facing PTY got closed, reading from the
161 # If the child-facing PTY got closed, reading from the
161 # parent-facing PTY raises EIO.
162 # parent-facing PTY raises EIO.
162 break
163 break
163 raise
164 raise
164 if not s:
165 if not s:
165 break
166 break
166 buf.append(s)
167 buf.append(s)
167 return b''.join(buf)
168 return b''.join(buf)
168
169
169
170
170 class TestStdio(unittest.TestCase):
171 class TestStdio(unittest.TestCase):
171 def _test(
172 def _test(
172 self,
173 self,
173 child_script,
174 child_script,
174 stream,
175 stream,
175 rwpair_generator,
176 rwpair_generator,
176 check_output,
177 check_output,
177 python_args=[],
178 python_args=[],
178 post_child_check=None,
179 post_child_check=None,
179 stdin_generator=None,
180 stdin_generator=None,
180 ):
181 ):
181 assert stream in ('stdout', 'stderr')
182 assert stream in ('stdout', 'stderr')
182 if stdin_generator is None:
183 if stdin_generator is None:
183 stdin_generator = open(os.devnull, 'rb')
184 stdin_generator = open(os.devnull, 'rb')
184 with rwpair_generator() as (
185 with rwpair_generator() as (
185 stream_receiver,
186 stream_receiver,
186 child_stream,
187 child_stream,
187 ), stdin_generator as child_stdin:
188 ), stdin_generator as child_stdin:
188 proc = subprocess.Popen(
189 proc = subprocess.Popen(
189 [sys.executable] + python_args + ['-c', child_script],
190 [sys.executable] + python_args + ['-c', child_script],
190 stdin=child_stdin,
191 stdin=child_stdin,
191 stdout=child_stream if stream == 'stdout' else None,
192 stdout=child_stream if stream == 'stdout' else None,
192 stderr=child_stream if stream == 'stderr' else None,
193 stderr=child_stream if stream == 'stderr' else None,
193 )
194 )
194 try:
195 try:
195 os.close(child_stream)
196 os.close(child_stream)
196 if stream_receiver is not None:
197 if stream_receiver is not None:
197 check_output(stream_receiver, proc)
198 check_output(stream_receiver, proc)
198 except: # re-raises
199 except: # re-raises
199 proc.terminate()
200 proc.terminate()
200 raise
201 raise
201 finally:
202 finally:
202 retcode = proc.wait()
203 retcode = proc.wait()
203 self.assertEqual(retcode, 0)
204 self.assertEqual(retcode, 0)
204 if post_child_check is not None:
205 if post_child_check is not None:
205 post_child_check()
206 post_child_check()
206
207
207 def _test_buffering(
208 def _test_buffering(
208 self, stream, rwpair_generator, expected_output, python_args=[]
209 self, stream, rwpair_generator, expected_output, python_args=[]
209 ):
210 ):
210 def check_output(stream_receiver, proc):
211 def check_output(stream_receiver, proc):
211 self.assertEqual(_readall(stream_receiver, 1024), expected_output)
212 self.assertEqual(_readall(stream_receiver, 1024), expected_output)
212
213
213 self._test(
214 self._test(
214 TEST_BUFFERING_CHILD_SCRIPT.format(stream=stream),
215 TEST_BUFFERING_CHILD_SCRIPT.format(stream=stream),
215 stream,
216 stream,
216 rwpair_generator,
217 rwpair_generator,
217 check_output,
218 check_output,
218 python_args,
219 python_args,
219 )
220 )
220
221
221 def test_buffering_stdout_devnull(self):
222 def test_buffering_stdout_devnull(self):
222 self._test_buffering('stdout', _devnull, None)
223 self._test_buffering('stdout', _devnull, None)
223
224
224 def test_buffering_stdout_pipes(self):
225 def test_buffering_stdout_pipes(self):
225 self._test_buffering('stdout', _pipes, FULLY_BUFFERED)
226 self._test_buffering('stdout', _pipes, FULLY_BUFFERED)
226
227
227 def test_buffering_stdout_ptys(self):
228 def test_buffering_stdout_ptys(self):
228 self._test_buffering('stdout', _ptys, LINE_BUFFERED)
229 self._test_buffering('stdout', _ptys, LINE_BUFFERED)
229
230
230 def test_buffering_stdout_devnull_unbuffered(self):
231 def test_buffering_stdout_devnull_unbuffered(self):
231 self._test_buffering('stdout', _devnull, None, python_args=['-u'])
232 self._test_buffering('stdout', _devnull, None, python_args=['-u'])
232
233
233 def test_buffering_stdout_pipes_unbuffered(self):
234 def test_buffering_stdout_pipes_unbuffered(self):
234 self._test_buffering('stdout', _pipes, UNBUFFERED, python_args=['-u'])
235 self._test_buffering('stdout', _pipes, UNBUFFERED, python_args=['-u'])
235
236
236 def test_buffering_stdout_ptys_unbuffered(self):
237 def test_buffering_stdout_ptys_unbuffered(self):
237 self._test_buffering('stdout', _ptys, UNBUFFERED, python_args=['-u'])
238 self._test_buffering('stdout', _ptys, UNBUFFERED, python_args=['-u'])
238
239
239 if not pycompat.ispy3 and not pycompat.iswindows:
240 if not pycompat.ispy3 and not pycompat.iswindows:
240 # On Python 2 on non-Windows, we manually open stdout in line-buffered
241 # On Python 2 on non-Windows, we manually open stdout in line-buffered
241 # mode if connected to a TTY. We should check if Python was configured
242 # mode if connected to a TTY. We should check if Python was configured
242 # to use unbuffered stdout, but it's hard to do that.
243 # to use unbuffered stdout, but it's hard to do that.
243 test_buffering_stdout_ptys_unbuffered = unittest.expectedFailure(
244 test_buffering_stdout_ptys_unbuffered = unittest.expectedFailure(
244 test_buffering_stdout_ptys_unbuffered
245 test_buffering_stdout_ptys_unbuffered
245 )
246 )
246
247
247 def _test_large_write(self, stream, rwpair_generator, python_args=[]):
248 def _test_large_write(self, stream, rwpair_generator, python_args=[]):
248 if not pycompat.ispy3 and pycompat.isdarwin:
249 if not pycompat.ispy3 and pycompat.isdarwin:
249 # Python 2 doesn't always retry on EINTR, but the libc might retry.
250 # Python 2 doesn't always retry on EINTR, but the libc might retry.
250 # So far, it was observed only on macOS that EINTR is raised at the
251 # So far, it was observed only on macOS that EINTR is raised at the
251 # Python level. As Python 2 support will be dropped soon-ish, we
252 # Python level. As Python 2 support will be dropped soon-ish, we
252 # won't attempt to fix it.
253 # won't attempt to fix it.
253 raise unittest.SkipTest("raises EINTR on macOS")
254 raise unittest.SkipTest("raises EINTR on macOS")
254
255
255 def check_output(stream_receiver, proc):
256 def check_output(stream_receiver, proc):
256 if not pycompat.iswindows:
257 if not pycompat.iswindows:
257 # On Unix, we can provoke a partial write() by interrupting it
258 # On Unix, we can provoke a partial write() by interrupting it
258 # by a signal handler as soon as a bit of data was written.
259 # by a signal handler as soon as a bit of data was written.
259 # We test that write() is called until all data is written.
260 # We test that write() is called until all data is written.
260 buf = [os.read(stream_receiver, 1)]
261 buf = [os.read(stream_receiver, 1)]
261 proc.send_signal(signal.SIGINT)
262 proc.send_signal(signal.SIGINT)
262 else:
263 else:
263 # On Windows, there doesn't seem to be a way to cause partial
264 # On Windows, there doesn't seem to be a way to cause partial
264 # writes.
265 # writes.
265 buf = []
266 buf = []
266 self.assertEqual(
267 self.assertEqual(
267 _readall(stream_receiver, 131072, buf), b'x' * 1048576
268 _readall(stream_receiver, 131072, buf), b'x' * 1048576
268 )
269 )
269
270
270 def post_child_check():
271 def post_child_check():
271 write_result_str = write_result_f.read()
272 write_result_str = write_result_f.read()
272 if pycompat.ispy3:
273 if pycompat.ispy3:
273 # On Python 3, we test that the correct number of bytes is
274 # On Python 3, we test that the correct number of bytes is
274 # claimed to have been written.
275 # claimed to have been written.
275 expected_write_result_str = '1048576'
276 expected_write_result_str = '1048576'
276 else:
277 else:
277 # On Python 2, we only check that the large write does not
278 # On Python 2, we only check that the large write does not
278 # crash.
279 # crash.
279 expected_write_result_str = 'None'
280 expected_write_result_str = 'None'
280 self.assertEqual(write_result_str, expected_write_result_str)
281 self.assertEqual(write_result_str, expected_write_result_str)
281
282
282 with tempfile.NamedTemporaryFile('r') as write_result_f:
283 with tempfile.NamedTemporaryFile('r') as write_result_f:
283 self._test(
284 self._test(
284 TEST_LARGE_WRITE_CHILD_SCRIPT.format(
285 TEST_LARGE_WRITE_CHILD_SCRIPT.format(
285 stream=stream, write_result_fn=write_result_f.name
286 stream=stream, write_result_fn=write_result_f.name
286 ),
287 ),
287 stream,
288 stream,
288 rwpair_generator,
289 rwpair_generator,
289 check_output,
290 check_output,
290 python_args,
291 python_args,
291 post_child_check=post_child_check,
292 post_child_check=post_child_check,
292 )
293 )
293
294
294 def test_large_write_stdout_devnull(self):
295 def test_large_write_stdout_devnull(self):
295 self._test_large_write('stdout', _devnull)
296 self._test_large_write('stdout', _devnull)
296
297
297 def test_large_write_stdout_pipes(self):
298 def test_large_write_stdout_pipes(self):
298 self._test_large_write('stdout', _pipes)
299 self._test_large_write('stdout', _pipes)
299
300
300 def test_large_write_stdout_ptys(self):
301 def test_large_write_stdout_ptys(self):
301 self._test_large_write('stdout', _ptys)
302 self._test_large_write('stdout', _ptys)
302
303
303 def test_large_write_stdout_devnull_unbuffered(self):
304 def test_large_write_stdout_devnull_unbuffered(self):
304 self._test_large_write('stdout', _devnull, python_args=['-u'])
305 self._test_large_write('stdout', _devnull, python_args=['-u'])
305
306
306 def test_large_write_stdout_pipes_unbuffered(self):
307 def test_large_write_stdout_pipes_unbuffered(self):
307 self._test_large_write('stdout', _pipes, python_args=['-u'])
308 self._test_large_write('stdout', _pipes, python_args=['-u'])
308
309
309 def test_large_write_stdout_ptys_unbuffered(self):
310 def test_large_write_stdout_ptys_unbuffered(self):
310 self._test_large_write('stdout', _ptys, python_args=['-u'])
311 self._test_large_write('stdout', _ptys, python_args=['-u'])
311
312
312 def test_large_write_stderr_devnull(self):
313 def test_large_write_stderr_devnull(self):
313 self._test_large_write('stderr', _devnull)
314 self._test_large_write('stderr', _devnull)
314
315
315 def test_large_write_stderr_pipes(self):
316 def test_large_write_stderr_pipes(self):
316 self._test_large_write('stderr', _pipes)
317 self._test_large_write('stderr', _pipes)
317
318
318 def test_large_write_stderr_ptys(self):
319 def test_large_write_stderr_ptys(self):
319 self._test_large_write('stderr', _ptys)
320 self._test_large_write('stderr', _ptys)
320
321
321 def test_large_write_stderr_devnull_unbuffered(self):
322 def test_large_write_stderr_devnull_unbuffered(self):
322 self._test_large_write('stderr', _devnull, python_args=['-u'])
323 self._test_large_write('stderr', _devnull, python_args=['-u'])
323
324
324 def test_large_write_stderr_pipes_unbuffered(self):
325 def test_large_write_stderr_pipes_unbuffered(self):
325 self._test_large_write('stderr', _pipes, python_args=['-u'])
326 self._test_large_write('stderr', _pipes, python_args=['-u'])
326
327
327 def test_large_write_stderr_ptys_unbuffered(self):
328 def test_large_write_stderr_ptys_unbuffered(self):
328 self._test_large_write('stderr', _ptys, python_args=['-u'])
329 self._test_large_write('stderr', _ptys, python_args=['-u'])
329
330
330 def _test_broken_pipe(self, stream):
331 def _test_broken_pipe(self, stream):
331 assert stream in ('stdout', 'stderr')
332 assert stream in ('stdout', 'stderr')
332
333
333 def check_output(stream_receiver, proc):
334 def check_output(stream_receiver, proc):
334 os.close(stream_receiver)
335 os.close(stream_receiver)
335 proc.stdin.write(b'x')
336 proc.stdin.write(b'x')
336 proc.stdin.close()
337 proc.stdin.close()
337
338
338 def post_child_check():
339 def post_child_check():
339 err = util.pickle.load(err_f)
340 err = pickle.load(err_f)
340 self.assertEqual(err.errno, errno.EPIPE)
341 self.assertEqual(err.errno, errno.EPIPE)
341 self.assertEqual(err.strerror, "Broken pipe")
342 self.assertEqual(err.strerror, "Broken pipe")
342
343
343 with tempfile.NamedTemporaryFile('rb') as err_f:
344 with tempfile.NamedTemporaryFile('rb') as err_f:
344 self._test(
345 self._test(
345 TEST_BROKEN_PIPE_CHILD_SCRIPT.format(
346 TEST_BROKEN_PIPE_CHILD_SCRIPT.format(
346 stream=stream, err_fn=err_f.name
347 stream=stream, err_fn=err_f.name
347 ),
348 ),
348 stream,
349 stream,
349 _pipes,
350 _pipes,
350 check_output,
351 check_output,
351 post_child_check=post_child_check,
352 post_child_check=post_child_check,
352 stdin_generator=util.nullcontextmanager(subprocess.PIPE),
353 stdin_generator=util.nullcontextmanager(subprocess.PIPE),
353 )
354 )
354
355
355 def test_broken_pipe_stdout(self):
356 def test_broken_pipe_stdout(self):
356 self._test_broken_pipe('stdout')
357 self._test_broken_pipe('stdout')
357
358
358 def test_broken_pipe_stderr(self):
359 def test_broken_pipe_stderr(self):
359 self._test_broken_pipe('stderr')
360 self._test_broken_pipe('stderr')
360
361
361
362
362 if __name__ == '__main__':
363 if __name__ == '__main__':
363 import silenttestrunner
364 import silenttestrunner
364
365
365 silenttestrunner.main(__name__)
366 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now