##// 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 1 #!/usr/bin/env python3
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Olivia Mackall <olivia@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """style and portability checker for Mercurial
11 11
12 12 when a rule triggers wrong, do one of the following (prefer one from top):
13 13 * do the work-around the rule suggests
14 14 * doublecheck that it is a false match
15 15 * improve the rule pattern
16 16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 17 (you can append a short comment and match this, like: #re-raises)
18 18 * change the pattern to a warning and list the exception in test-check-code-hg
19 19 * ONLY use no--check-code for skipping entire files from external sources
20 20 """
21 21
22 22 from __future__ import absolute_import, print_function
23 23 import glob
24 24 import keyword
25 25 import optparse
26 26 import os
27 27 import re
28 28 import sys
29 29
30 30 if sys.version_info[0] < 3:
31 31 opentext = open
32 32 else:
33 33
34 34 def opentext(f):
35 35 return open(f, encoding='latin1')
36 36
37 37
38 38 try:
39 39 xrange
40 40 except NameError:
41 41 xrange = range
42 42 try:
43 43 import re2
44 44 except ImportError:
45 45 re2 = None
46 46
47 47 import testparseutil
48 48
49 49
50 50 def compilere(pat, multiline=False):
51 51 if multiline:
52 52 pat = '(?m)' + pat
53 53 if re2:
54 54 try:
55 55 return re2.compile(pat)
56 56 except re2.error:
57 57 pass
58 58 return re.compile(pat)
59 59
60 60
61 61 # check "rules depending on implementation of repquote()" in each
62 62 # patterns (especially pypats), before changing around repquote()
63 63 _repquotefixedmap = {
64 64 ' ': ' ',
65 65 '\n': '\n',
66 66 '.': 'p',
67 67 ':': 'q',
68 68 '%': '%',
69 69 '\\': 'b',
70 70 '*': 'A',
71 71 '+': 'P',
72 72 '-': 'M',
73 73 }
74 74
75 75
76 76 def _repquoteencodechr(i):
77 77 if i > 255:
78 78 return 'u'
79 79 c = chr(i)
80 80 if c in _repquotefixedmap:
81 81 return _repquotefixedmap[c]
82 82 if c.isalpha():
83 83 return 'x'
84 84 if c.isdigit():
85 85 return 'n'
86 86 return 'o'
87 87
88 88
89 89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
90 90
91 91
92 92 def repquote(m):
93 93 t = m.group('text')
94 94 t = t.translate(_repquotett)
95 95 return m.group('quote') + t + m.group('quote')
96 96
97 97
98 98 def reppython(m):
99 99 comment = m.group('comment')
100 100 if comment:
101 101 l = len(comment.rstrip())
102 102 return "#" * l + comment[l:]
103 103 return repquote(m)
104 104
105 105
106 106 def repcomment(m):
107 107 return m.group(1) + "#" * len(m.group(2))
108 108
109 109
110 110 def repccomment(m):
111 111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
112 112 return m.group(1) + t + "*/"
113 113
114 114
115 115 def repcallspaces(m):
116 116 t = re.sub(r"\n\s+", "\n", m.group(2))
117 117 return m.group(1) + t
118 118
119 119
120 120 def repinclude(m):
121 121 return m.group(1) + "<foo>"
122 122
123 123
124 124 def rephere(m):
125 125 t = re.sub(r"\S", "x", m.group(2))
126 126 return m.group(1) + t
127 127
128 128
129 129 testpats = [
130 130 [
131 131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
132 132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
133 133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
134 134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
135 135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
136 136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
137 137 (r'echo -n', "don't use 'echo -n', use printf"),
138 138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
139 139 (r'head -c', "don't use 'head -c', use 'dd'"),
140 140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
141 141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
142 142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
143 143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
144 144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
145 145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
146 146 (
147 147 r'\[[^\]]+==',
148 148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
149 149 ),
150 150 (
151 151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
152 152 "use egrep for extended grep syntax",
153 153 ),
154 154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
155 155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
156 156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
157 157 (r'[^\n]\Z', "no trailing newline"),
158 158 (r'export .*=', "don't export and assign at once"),
159 159 (r'^source\b', "don't use 'source', use '.'"),
160 160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
161 161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
162 162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
163 163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
164 164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
165 165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
166 166 (r'^alias\b.*=', "don't use alias, use a function"),
167 167 (r'if\s*!', "don't use '!' to negate exit status"),
168 168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
169 169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
170 170 (
171 171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
172 172 "put a backslash-escaped newline after sed 'i' command",
173 173 ),
174 174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
175 175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
176 176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
177 177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
178 178 (r'\butil\.Abort\b', "directly use error.Abort"),
179 179 (r'\|&', "don't use |&, use 2>&1"),
180 180 (r'\w = +\w', "only one space after = allowed"),
181 181 (
182 182 r'\bsed\b.*[^\\]\\n',
183 183 "don't use 'sed ... \\n', use a \\ and a newline",
184 184 ),
185 185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
186 186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
187 187 (r'grep.* -[ABC]', "don't use grep's context flags"),
188 188 (
189 189 r'find.*-printf',
190 190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 191 ),
192 192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
193 193 ],
194 194 # warnings
195 195 [
196 196 (r'^function', "don't use 'function', use old style"),
197 197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
198 198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`", "no-pwd-check"),
199 199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
200 200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 201 ],
202 202 ]
203 203
204 204 testfilters = [
205 205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
206 206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
207 207 ]
208 208
209 209 uprefix = r"^ \$ "
210 210 utestpats = [
211 211 [
212 212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
213 213 (
214 214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
215 215 "use regex test output patterns instead of sed",
216 216 ),
217 217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
218 218 (
219 219 uprefix + r'.*\|\| echo.*(fail|error)',
220 220 "explicit exit code checks unnecessary",
221 221 ),
222 222 (uprefix + r'set -e', "don't use set -e"),
223 223 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
224 224 (
225 225 uprefix + r'.*:\.\S*/',
226 226 "x:.y in a path does not work on msys, rewrite "
227 227 "as x://.y, or see `hg log -k msys` for alternatives",
228 228 r'-\S+:\.|' '# no-msys', # -Rxxx
229 229 ), # in test-pull.t which is skipped on windows
230 230 (
231 231 r'^ [^$>].*27\.0\.0\.1',
232 232 'use $LOCALIP not an explicit loopback address',
233 233 ),
234 234 (
235 235 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
236 236 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
237 237 ),
238 238 (
239 239 r'^ (cat|find): .*: \$ENOENT\$',
240 240 'use test -f to test for file existence',
241 241 ),
242 242 (
243 243 r'^ diff -[^ -]*p',
244 244 "don't use (external) diff with -p for portability",
245 245 ),
246 246 (r' readlink ', 'use readlink.py instead of readlink'),
247 247 (
248 248 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
249 249 "glob timezone field in diff output for portability",
250 250 ),
251 251 (
252 252 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
253 253 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
254 254 ),
255 255 (
256 256 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
257 257 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
258 258 ),
259 259 (
260 260 r'^ @@ -[0-9]+ [+][0-9]+ @@',
261 261 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
262 262 ),
263 263 (
264 264 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
265 265 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
266 266 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
267 267 ),
268 268 ],
269 269 # warnings
270 270 [
271 271 (
272 272 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
273 273 "glob match with no glob string (?, *, /, and $LOCALIP)",
274 274 ),
275 275 ],
276 276 ]
277 277
278 278 # transform plain test rules to unified test's
279 279 for i in [0, 1]:
280 280 for tp in testpats[i]:
281 281 p = tp[0]
282 282 m = tp[1]
283 283 if p.startswith('^'):
284 284 p = "^ [$>] (%s)" % p[1:]
285 285 else:
286 286 p = "^ [$>] .*(%s)" % p
287 287 utestpats[i].append((p, m) + tp[2:])
288 288
289 289 # don't transform the following rules:
290 290 # " > \t" and " \t" should be allowed in unified tests
291 291 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
292 292 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
293 293
294 294 utestfilters = [
295 295 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
296 296 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
297 297 ]
298 298
299 299 # common patterns to check *.py
300 300 commonpypats = [
301 301 [
302 302 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
303 303 (
304 304 r'^\s*def\s*\w+\s*\(.*,\s*\(',
305 305 "tuple parameter unpacking not available in Python 3+",
306 306 ),
307 307 (
308 308 r'lambda\s*\(.*,.*\)',
309 309 "tuple parameter unpacking not available in Python 3+",
310 310 ),
311 311 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
312 312 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
313 313 (
314 314 r'\bdict\(.*=',
315 315 'dict() is different in Py2 and 3 and is slower than {}',
316 316 'dict-from-generator',
317 317 ),
318 318 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
319 319 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
320 320 (r'^\s*\t', "don't use tabs"),
321 321 (r'\S;\s*\n', "semicolon"),
322 322 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
323 323 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
324 324 (r'(\w|\)),\w', "missing whitespace after ,"),
325 325 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
326 326 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
327 327 (
328 328 (
329 329 # a line ending with a colon, potentially with trailing comments
330 330 r':([ \t]*#[^\n]*)?\n'
331 331 # one that is not a pass and not only a comment
332 332 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
333 333 # more lines at the same indent level
334 334 r'((?P=indent)[^\n]+\n)*'
335 335 # a pass at the same indent level, which is bogus
336 336 r'(?P=indent)pass[ \t\n#]'
337 337 ),
338 338 'omit superfluous pass',
339 339 ),
340 340 (r'[^\n]\Z', "no trailing newline"),
341 341 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
342 342 (
343 343 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
344 344 "linebreak after :",
345 345 ),
346 346 (
347 347 r'class\s[^( \n]+:',
348 348 "old-style class, use class foo(object)",
349 349 r'#.*old-style',
350 350 ),
351 351 (
352 352 r'class\s[^( \n]+\(\):',
353 353 "class foo() creates old style object, use class foo(object)",
354 354 r'#.*old-style',
355 355 ),
356 356 (
357 357 r'\b(%s)\('
358 358 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
359 359 "Python keyword is not a function",
360 360 ),
361 361 # (r'class\s[A-Z][^\(]*\((?!Exception)',
362 362 # "don't capitalize non-exception classes"),
363 363 # (r'in range\(', "use xrange"),
364 364 # (r'^\s*print\s+', "avoid using print in core and extensions"),
365 365 (r'[\x80-\xff]', "non-ASCII character literal"),
366 366 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
367 367 (
368 368 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
369 369 "gratuitous whitespace in () or []",
370 370 ),
371 371 # (r'\s\s=', "gratuitous whitespace before ="),
372 372 (
373 373 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
374 374 "missing whitespace around operator",
375 375 ),
376 376 (
377 377 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
378 378 "missing whitespace around operator",
379 379 ),
380 380 (
381 381 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
382 382 "missing whitespace around operator",
383 383 ),
384 384 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
385 385 (
386 386 r'\([^()]*( =[^=]|[^<>!=]= )',
387 387 "no whitespace around = for named parameters",
388 388 ),
389 389 (
390 390 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
391 391 "don't use old-style two-argument raise, use Exception(message)",
392 392 ),
393 393 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
394 394 (
395 395 r' [=!]=\s+(True|False|None)',
396 396 "comparison with singleton, use 'is' or 'is not' instead",
397 397 ),
398 398 (
399 399 r'^\s*(while|if) [01]:',
400 400 "use True/False for constant Boolean expression",
401 401 ),
402 402 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
403 403 (
404 404 r'(?:(?<!def)\s+|\()hasattr\(',
405 405 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
406 406 'instead',
407 407 r'#.*hasattr-py3-only',
408 408 ),
409 409 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
410 410 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
411 411 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
412 412 (r'\.debug\(\_', "don't mark debug messages for translation"),
413 413 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
414 414 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
415 415 (
416 416 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
417 417 'legacy exception syntax; use "as" instead of ","',
418 418 ),
419 419 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
420 420 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
421 421 (
422 422 r'os\.path\.join\(.*, *(""|\'\')\)',
423 423 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
424 424 ),
425 425 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
426 426 # XXX only catch mutable arguments on the first line of the definition
427 427 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
428 428 (r'\butil\.Abort\b', "directly use error.Abort"),
429 429 (
430 430 r'^@(\w*\.)?cachefunc',
431 431 "module-level @cachefunc is risky, please avoid",
432 432 ),
433 433 (
434 434 r'^import Queue',
435 435 "don't use Queue, use pycompat.queue.Queue + "
436 436 "pycompat.queue.Empty",
437 437 ),
438 438 (
439 439 r'^import cStringIO',
440 440 "don't use cStringIO.StringIO, use util.stringio",
441 441 ),
442 442 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
443 443 (
444 444 r'^import SocketServer',
445 445 "don't use SockerServer, use util.socketserver",
446 446 ),
447 447 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
448 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 449 (r'^import httplib', "don't use httplib, use util.httplib"),
452 450 (r'^import BaseHTTPServer', "use util.httpserver instead"),
453 451 (
454 452 r'^(from|import) mercurial\.(cext|pure|cffi)',
455 453 "use mercurial.policy.importmod instead",
456 454 ),
457 455 (r'\.next\(\)', "don't use .next(), use next(...)"),
458 456 (
459 457 r'([a-z]*).revision\(\1\.node\(',
460 458 "don't convert rev to node before passing to revision(nodeorrev)",
461 459 ),
462 460 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
463 461 ],
464 462 # warnings
465 463 [],
466 464 ]
467 465
468 466 # patterns to check normal *.py files
469 467 pypats = [
470 468 [
471 469 # Ideally, these should be placed in "commonpypats" for
472 470 # consistency of coding rules in Mercurial source tree.
473 471 # But on the other hand, these are not so seriously required for
474 472 # python code fragments embedded in test scripts. Fixing test
475 473 # scripts for these patterns requires many changes, and has less
476 474 # profit than effort.
477 475 (r'raise Exception', "don't raise generic exceptions"),
478 476 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
479 477 (
480 478 r'[\s\(](open|file)\([^)]*\)\.write\(',
481 479 "use util.writefile() instead",
482 480 ),
483 481 (
484 482 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
485 483 "always assign an opened file to a variable, and close it afterwards",
486 484 ),
487 485 (
488 486 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
489 487 "always assign an opened file to a variable, and close it afterwards",
490 488 ),
491 489 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
492 490 (r'^import atexit', "don't use atexit, use ui.atexit"),
493 491 # rules depending on implementation of repquote()
494 492 (
495 493 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
496 494 'string join across lines with no space',
497 495 ),
498 496 (
499 497 r'''(?x)ui\.(status|progress|write|note|warn)\(
500 498 [ \t\n#]*
501 499 (?# any strings/comments might precede a string, which
502 500 # contains translatable message)
503 501 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
504 502 (?# sequence consisting of below might precede translatable message
505 503 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
506 504 # - escaped character: "\\", "\n", "\0" ...
507 505 # - character other than '%', 'b' as '\', and 'x' as alphabet)
508 506 (['"]|\'\'\'|""")
509 507 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
510 508 (?# this regexp can't use [^...] style,
511 509 # because _preparepats forcibly adds "\n" into [^...],
512 510 # even though this regexp wants match it against "\n")''',
513 511 "missing _() in ui message (use () to hide false-positives)",
514 512 ),
515 513 ]
516 514 + commonpypats[0],
517 515 # warnings
518 516 [
519 517 # rules depending on implementation of repquote()
520 518 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
521 519 ]
522 520 + commonpypats[1],
523 521 ]
524 522
525 523 # patterns to check *.py for embedded ones in test script
526 524 embeddedpypats = [
527 525 [] + commonpypats[0],
528 526 # warnings
529 527 [] + commonpypats[1],
530 528 ]
531 529
532 530 # common filters to convert *.py
533 531 commonpyfilters = [
534 532 (
535 533 r"""(?msx)(?P<comment>\#.*?$)|
536 534 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
537 535 (?P<text>(([^\\]|\\.)*?))
538 536 (?P=quote))""",
539 537 reppython,
540 538 ),
541 539 ]
542 540
543 541 # pattern only for mercurial and extensions
544 542 core_py_pats = [
545 543 [
546 544 # Windows tend to get confused about capitalization of the drive letter
547 545 #
548 546 # see mercurial.windows.abspath for details
549 547 (
550 548 r'os\.path\.abspath',
551 549 "use util.abspath instead (windows)",
552 550 r'#.*re-exports',
553 551 ),
554 552 ],
555 553 # warnings
556 554 [],
557 555 ]
558 556
559 557 # filters to convert normal *.py files
560 558 pyfilters = [] + commonpyfilters
561 559
562 560 # non-filter patterns
563 561 pynfpats = [
564 562 [
565 563 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
566 564 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
567 565 (
568 566 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
569 567 "use pycompat.isdarwin",
570 568 ),
571 569 ],
572 570 # warnings
573 571 [],
574 572 ]
575 573
576 574 # filters to convert *.py for embedded ones in test script
577 575 embeddedpyfilters = [] + commonpyfilters
578 576
579 577 # extension non-filter patterns
580 578 pyextnfpats = [
581 579 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
582 580 # warnings
583 581 [],
584 582 ]
585 583
586 584 txtfilters = []
587 585
588 586 txtpats = [
589 587 [
590 588 (r'\s$', 'trailing whitespace'),
591 589 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
592 590 ],
593 591 [],
594 592 ]
595 593
596 594 cpats = [
597 595 [
598 596 (r'//', "don't use //-style comments"),
599 597 (r'\S\t', "don't use tabs except for indent"),
600 598 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
601 599 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
602 600 (r'return\(', "return is not a function"),
603 601 (r' ;', "no space before ;"),
604 602 (r'[^;] \)', "no space before )"),
605 603 (r'[)][{]', "space between ) and {"),
606 604 (r'\w+\* \w+', "use int *foo, not int* foo"),
607 605 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
608 606 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
609 607 (r'\w,\w', "missing whitespace after ,"),
610 608 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
611 609 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
612 610 (r'^#\s+\w', "use #foo, not # foo"),
613 611 (r'[^\n]\Z', "no trailing newline"),
614 612 (r'^\s*#import\b', "use only #include in standard C code"),
615 613 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
616 614 (r'strcat\(', "don't use strcat"),
617 615 # rules depending on implementation of repquote()
618 616 ],
619 617 # warnings
620 618 [
621 619 # rules depending on implementation of repquote()
622 620 ],
623 621 ]
624 622
625 623 cfilters = [
626 624 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
627 625 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
628 626 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
629 627 (r'(\()([^)]+\))', repcallspaces),
630 628 ]
631 629
632 630 inutilpats = [
633 631 [
634 632 (r'\bui\.', "don't use ui in util"),
635 633 ],
636 634 # warnings
637 635 [],
638 636 ]
639 637
640 638 inrevlogpats = [
641 639 [
642 640 (r'\brepo\.', "don't use repo in revlog"),
643 641 ],
644 642 # warnings
645 643 [],
646 644 ]
647 645
648 646 webtemplatefilters = []
649 647
650 648 webtemplatepats = [
651 649 [],
652 650 [
653 651 (
654 652 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
655 653 'follow desc keyword with either firstline or websub',
656 654 ),
657 655 ],
658 656 ]
659 657
660 658 allfilesfilters = []
661 659
662 660 allfilespats = [
663 661 [
664 662 (
665 663 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
666 664 'use mercurial-scm.org domain URL',
667 665 ),
668 666 (
669 667 r'mercurial@selenic\.com',
670 668 'use mercurial-scm.org domain for mercurial ML address',
671 669 ),
672 670 (
673 671 r'mercurial-devel@selenic\.com',
674 672 'use mercurial-scm.org domain for mercurial-devel ML address',
675 673 ),
676 674 ],
677 675 # warnings
678 676 [],
679 677 ]
680 678
681 679 py3pats = [
682 680 [
683 681 (
684 682 r'os\.environ',
685 683 "use encoding.environ instead (py3)",
686 684 r'#.*re-exports',
687 685 ),
688 686 (r'os\.name', "use pycompat.osname instead (py3)"),
689 687 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
690 688 (r'os\.sep', "use pycompat.ossep instead (py3)"),
691 689 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
692 690 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
693 691 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
694 692 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
695 693 (r'os\.getenv', "use encoding.environ.get instead"),
696 694 (r'os\.setenv', "modifying the environ dict is not preferred"),
697 695 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
698 696 ],
699 697 # warnings
700 698 [],
701 699 ]
702 700
703 701 checks = [
704 702 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
705 703 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
706 704 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
707 705 (
708 706 'python 3',
709 707 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
710 708 '',
711 709 pyfilters,
712 710 py3pats,
713 711 ),
714 712 (
715 713 'core files',
716 714 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
717 715 '',
718 716 pyfilters,
719 717 core_py_pats,
720 718 ),
721 719 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
722 720 ('c', r'.*\.[ch]$', '', cfilters, cpats),
723 721 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
724 722 (
725 723 'layering violation repo in revlog',
726 724 r'mercurial/revlog\.py',
727 725 '',
728 726 pyfilters,
729 727 inrevlogpats,
730 728 ),
731 729 (
732 730 'layering violation ui in util',
733 731 r'mercurial/util\.py',
734 732 '',
735 733 pyfilters,
736 734 inutilpats,
737 735 ),
738 736 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
739 737 (
740 738 'web template',
741 739 r'mercurial/templates/.*\.tmpl',
742 740 '',
743 741 webtemplatefilters,
744 742 webtemplatepats,
745 743 ),
746 744 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
747 745 ]
748 746
749 747 # (desc,
750 748 # func to pick up embedded code fragments,
751 749 # list of patterns to convert target files
752 750 # list of patterns to detect errors/warnings)
753 751 embeddedchecks = [
754 752 (
755 753 'embedded python',
756 754 testparseutil.pyembedded,
757 755 embeddedpyfilters,
758 756 embeddedpypats,
759 757 )
760 758 ]
761 759
762 760
763 761 def _preparepats():
764 762 def preparefailandwarn(failandwarn):
765 763 for pats in failandwarn:
766 764 for i, pseq in enumerate(pats):
767 765 # fix-up regexes for multi-line searches
768 766 p = pseq[0]
769 767 # \s doesn't match \n (done in two steps)
770 768 # first, we replace \s that appears in a set already
771 769 p = re.sub(r'\[\\s', r'[ \\t', p)
772 770 # now we replace other \s instances.
773 771 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
774 772 # [^...] doesn't match newline
775 773 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
776 774
777 775 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
778 776
779 777 def preparefilters(filters):
780 778 for i, flt in enumerate(filters):
781 779 filters[i] = re.compile(flt[0]), flt[1]
782 780
783 781 for cs in (checks, embeddedchecks):
784 782 for c in cs:
785 783 failandwarn = c[-1]
786 784 preparefailandwarn(failandwarn)
787 785
788 786 filters = c[-2]
789 787 preparefilters(filters)
790 788
791 789
792 790 class norepeatlogger(object):
793 791 def __init__(self):
794 792 self._lastseen = None
795 793
796 794 def log(self, fname, lineno, line, msg, blame):
797 795 """print error related a to given line of a given file.
798 796
799 797 The faulty line will also be printed but only once in the case
800 798 of multiple errors.
801 799
802 800 :fname: filename
803 801 :lineno: line number
804 802 :line: actual content of the line
805 803 :msg: error message
806 804 """
807 805 msgid = fname, lineno, line
808 806 if msgid != self._lastseen:
809 807 if blame:
810 808 print("%s:%d (%s):" % (fname, lineno, blame))
811 809 else:
812 810 print("%s:%d:" % (fname, lineno))
813 811 print(" > %s" % line)
814 812 self._lastseen = msgid
815 813 print(" " + msg)
816 814
817 815
818 816 _defaultlogger = norepeatlogger()
819 817
820 818
821 819 def getblame(f):
822 820 lines = []
823 821 for l in os.popen('hg annotate -un %s' % f):
824 822 start, line = l.split(':', 1)
825 823 user, rev = start.split()
826 824 lines.append((line[1:-1], user, rev))
827 825 return lines
828 826
829 827
830 828 def checkfile(
831 829 f,
832 830 logfunc=_defaultlogger.log,
833 831 maxerr=None,
834 832 warnings=False,
835 833 blame=False,
836 834 debug=False,
837 835 lineno=True,
838 836 ):
839 837 """checks style and portability of a given file
840 838
841 839 :f: filepath
842 840 :logfunc: function used to report error
843 841 logfunc(filename, linenumber, linecontent, errormessage)
844 842 :maxerr: number of error to display before aborting.
845 843 Set to false (default) to report all errors
846 844
847 845 return True if no error is found, False otherwise.
848 846 """
849 847 result = True
850 848
851 849 try:
852 850 with opentext(f) as fp:
853 851 try:
854 852 pre = fp.read()
855 853 except UnicodeDecodeError as e:
856 854 print("%s while reading %s" % (e, f))
857 855 return result
858 856 except IOError as e:
859 857 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
860 858 return result
861 859
862 860 # context information shared while single checkfile() invocation
863 861 context = {'blamecache': None}
864 862
865 863 for name, match, magic, filters, pats in checks:
866 864 if debug:
867 865 print(name, f)
868 866 if not (re.match(match, f) or (magic and re.search(magic, pre))):
869 867 if debug:
870 868 print(
871 869 "Skipping %s for %s it doesn't match %s" % (name, match, f)
872 870 )
873 871 continue
874 872 if "no-" "check-code" in pre:
875 873 # If you're looking at this line, it's because a file has:
876 874 # no- check- code
877 875 # but the reason to output skipping is to make life for
878 876 # tests easier. So, instead of writing it with a normal
879 877 # spelling, we write it with the expected spelling from
880 878 # tests/test-check-code.t
881 879 print("Skipping %s it has no-che?k-code (glob)" % f)
882 880 return "Skip" # skip checking this file
883 881
884 882 fc = _checkfiledata(
885 883 name,
886 884 f,
887 885 pre,
888 886 filters,
889 887 pats,
890 888 context,
891 889 logfunc,
892 890 maxerr,
893 891 warnings,
894 892 blame,
895 893 debug,
896 894 lineno,
897 895 )
898 896 if fc:
899 897 result = False
900 898
901 899 if f.endswith('.t') and "no-" "check-code" not in pre:
902 900 if debug:
903 901 print("Checking embedded code in %s" % f)
904 902
905 903 prelines = pre.splitlines()
906 904 embeddederros = []
907 905 for name, embedded, filters, pats in embeddedchecks:
908 906 # "reset curmax at each repetition" treats maxerr as "max
909 907 # nubmer of errors in an actual file per entry of
910 908 # (embedded)checks"
911 909 curmaxerr = maxerr
912 910
913 911 for found in embedded(f, prelines, embeddederros):
914 912 filename, starts, ends, code = found
915 913 fc = _checkfiledata(
916 914 name,
917 915 f,
918 916 code,
919 917 filters,
920 918 pats,
921 919 context,
922 920 logfunc,
923 921 curmaxerr,
924 922 warnings,
925 923 blame,
926 924 debug,
927 925 lineno,
928 926 offset=starts - 1,
929 927 )
930 928 if fc:
931 929 result = False
932 930 if curmaxerr:
933 931 if fc >= curmaxerr:
934 932 break
935 933 curmaxerr -= fc
936 934
937 935 return result
938 936
939 937
940 938 def _checkfiledata(
941 939 name,
942 940 f,
943 941 filedata,
944 942 filters,
945 943 pats,
946 944 context,
947 945 logfunc,
948 946 maxerr,
949 947 warnings,
950 948 blame,
951 949 debug,
952 950 lineno,
953 951 offset=None,
954 952 ):
955 953 """Execute actual error check for file data
956 954
957 955 :name: of the checking category
958 956 :f: filepath
959 957 :filedata: content of a file
960 958 :filters: to be applied before checking
961 959 :pats: to detect errors
962 960 :context: a dict of information shared while single checkfile() invocation
963 961 Valid keys: 'blamecache'.
964 962 :logfunc: function used to report error
965 963 logfunc(filename, linenumber, linecontent, errormessage)
966 964 :maxerr: number of error to display before aborting, or False to
967 965 report all errors
968 966 :warnings: whether warning level checks should be applied
969 967 :blame: whether blame information should be displayed at error reporting
970 968 :debug: whether debug information should be displayed
971 969 :lineno: whether lineno should be displayed at error reporting
972 970 :offset: line number offset of 'filedata' in 'f' for checking
973 971 an embedded code fragment, or None (offset=0 is different
974 972 from offset=None)
975 973
976 974 returns number of detected errors.
977 975 """
978 976 blamecache = context['blamecache']
979 977 if offset is None:
980 978 lineoffset = 0
981 979 else:
982 980 lineoffset = offset
983 981
984 982 fc = 0
985 983 pre = post = filedata
986 984
987 985 if True: # TODO: get rid of this redundant 'if' block
988 986 for p, r in filters:
989 987 post = re.sub(p, r, post)
990 988 nerrs = len(pats[0]) # nerr elements are errors
991 989 if warnings:
992 990 pats = pats[0] + pats[1]
993 991 else:
994 992 pats = pats[0]
995 993 # print post # uncomment to show filtered version
996 994
997 995 if debug:
998 996 print("Checking %s for %s" % (name, f))
999 997
1000 998 prelines = None
1001 999 errors = []
1002 1000 for i, pat in enumerate(pats):
1003 1001 if len(pat) == 3:
1004 1002 p, msg, ignore = pat
1005 1003 else:
1006 1004 p, msg = pat
1007 1005 ignore = None
1008 1006 if i >= nerrs:
1009 1007 msg = "warning: " + msg
1010 1008
1011 1009 pos = 0
1012 1010 n = 0
1013 1011 for m in p.finditer(post):
1014 1012 if prelines is None:
1015 1013 prelines = pre.splitlines()
1016 1014 postlines = post.splitlines(True)
1017 1015
1018 1016 start = m.start()
1019 1017 while n < len(postlines):
1020 1018 step = len(postlines[n])
1021 1019 if pos + step > start:
1022 1020 break
1023 1021 pos += step
1024 1022 n += 1
1025 1023 l = prelines[n]
1026 1024
1027 1025 if ignore and re.search(ignore, l, re.MULTILINE):
1028 1026 if debug:
1029 1027 print(
1030 1028 "Skipping %s for %s:%s (ignore pattern)"
1031 1029 % (name, f, (n + lineoffset))
1032 1030 )
1033 1031 continue
1034 1032 bd = ""
1035 1033 if blame:
1036 1034 bd = 'working directory'
1037 1035 if blamecache is None:
1038 1036 blamecache = getblame(f)
1039 1037 context['blamecache'] = blamecache
1040 1038 if (n + lineoffset) < len(blamecache):
1041 1039 bl, bu, br = blamecache[(n + lineoffset)]
1042 1040 if offset is None and bl == l:
1043 1041 bd = '%s@%s' % (bu, br)
1044 1042 elif offset is not None and bl.endswith(l):
1045 1043 # "offset is not None" means "checking
1046 1044 # embedded code fragment". In this case,
1047 1045 # "l" does not have information about the
1048 1046 # beginning of an *original* line in the
1049 1047 # file (e.g. ' > ').
1050 1048 # Therefore, use "str.endswith()", and
1051 1049 # show "maybe" for a little loose
1052 1050 # examination.
1053 1051 bd = '%s@%s, maybe' % (bu, br)
1054 1052
1055 1053 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1056 1054
1057 1055 errors.sort()
1058 1056 for e in errors:
1059 1057 logfunc(*e)
1060 1058 fc += 1
1061 1059 if maxerr and fc >= maxerr:
1062 1060 print(" (too many errors, giving up)")
1063 1061 break
1064 1062
1065 1063 return fc
1066 1064
1067 1065
1068 1066 def main():
1069 1067 parser = optparse.OptionParser("%prog [options] [files | -]")
1070 1068 parser.add_option(
1071 1069 "-w",
1072 1070 "--warnings",
1073 1071 action="store_true",
1074 1072 help="include warning-level checks",
1075 1073 )
1076 1074 parser.add_option(
1077 1075 "-p", "--per-file", type="int", help="max warnings per file"
1078 1076 )
1079 1077 parser.add_option(
1080 1078 "-b",
1081 1079 "--blame",
1082 1080 action="store_true",
1083 1081 help="use annotate to generate blame info",
1084 1082 )
1085 1083 parser.add_option(
1086 1084 "", "--debug", action="store_true", help="show debug information"
1087 1085 )
1088 1086 parser.add_option(
1089 1087 "",
1090 1088 "--nolineno",
1091 1089 action="store_false",
1092 1090 dest='lineno',
1093 1091 help="don't show line numbers",
1094 1092 )
1095 1093
1096 1094 parser.set_defaults(
1097 1095 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1098 1096 )
1099 1097 (options, args) = parser.parse_args()
1100 1098
1101 1099 if len(args) == 0:
1102 1100 check = glob.glob("*")
1103 1101 elif args == ['-']:
1104 1102 # read file list from stdin
1105 1103 check = sys.stdin.read().splitlines()
1106 1104 else:
1107 1105 check = args
1108 1106
1109 1107 _preparepats()
1110 1108
1111 1109 ret = 0
1112 1110 for f in check:
1113 1111 if not checkfile(
1114 1112 f,
1115 1113 maxerr=options.per_file,
1116 1114 warnings=options.warnings,
1117 1115 blame=options.blame,
1118 1116 debug=options.debug,
1119 1117 lineno=options.lineno,
1120 1118 ):
1121 1119 ret = 1
1122 1120 return ret
1123 1121
1124 1122
1125 1123 if __name__ == "__main__":
1126 1124 sys.exit(main())
@@ -1,598 +1,598 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import base64
10 10 import datetime
11 11 import errno
12 12 import os
13 import pickle
13 14 import re
14 15 import shlex
15 16 import subprocess
16 17
17 18 from mercurial.i18n import _
18 19 from mercurial.pycompat import open
19 20 from mercurial import (
20 21 encoding,
21 22 error,
22 23 phases,
23 24 pycompat,
24 25 util,
25 26 )
26 27 from mercurial.utils import procutil
27 28
28 pickle = util.pickle
29 29 propertycache = util.propertycache
30 30
31 31
32 32 def _encodeornone(d):
33 33 if d is None:
34 34 return
35 35 return d.encode('latin1')
36 36
37 37
38 38 class _shlexpy3proxy(object):
39 39 def __init__(self, l):
40 40 self._l = l
41 41
42 42 def __iter__(self):
43 43 return (_encodeornone(v) for v in self._l)
44 44
45 45 def get_token(self):
46 46 return _encodeornone(self._l.get_token())
47 47
48 48 @property
49 49 def infile(self):
50 50 return self._l.infile or b'<unknown>'
51 51
52 52 @property
53 53 def lineno(self):
54 54 return self._l.lineno
55 55
56 56
57 57 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
58 58 if data is None:
59 59 if pycompat.ispy3:
60 60 data = open(filepath, b'r', encoding='latin1')
61 61 else:
62 62 data = open(filepath, b'r')
63 63 else:
64 64 if filepath is not None:
65 65 raise error.ProgrammingError(
66 66 b'shlexer only accepts data or filepath, not both'
67 67 )
68 68 if pycompat.ispy3:
69 69 data = data.decode('latin1')
70 70 l = shlex.shlex(data, infile=filepath, posix=True)
71 71 if whitespace is not None:
72 72 l.whitespace_split = True
73 73 if pycompat.ispy3:
74 74 l.whitespace += whitespace.decode('latin1')
75 75 else:
76 76 l.whitespace += whitespace
77 77 if wordchars is not None:
78 78 if pycompat.ispy3:
79 79 l.wordchars += wordchars.decode('latin1')
80 80 else:
81 81 l.wordchars += wordchars
82 82 if pycompat.ispy3:
83 83 return _shlexpy3proxy(l)
84 84 return l
85 85
86 86
87 87 if pycompat.ispy3:
88 88 base64_encodebytes = base64.encodebytes
89 89 base64_decodebytes = base64.decodebytes
90 90 else:
91 91 base64_encodebytes = base64.encodestring
92 92 base64_decodebytes = base64.decodestring
93 93
94 94
95 95 def encodeargs(args):
96 96 def encodearg(s):
97 97 lines = base64_encodebytes(s)
98 98 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
99 99 return b''.join(lines)
100 100
101 101 s = pickle.dumps(args)
102 102 return encodearg(s)
103 103
104 104
105 105 def decodeargs(s):
106 106 s = base64_decodebytes(s)
107 107 return pickle.loads(s)
108 108
109 109
110 110 class MissingTool(Exception):
111 111 pass
112 112
113 113
114 114 def checktool(exe, name=None, abort=True):
115 115 name = name or exe
116 116 if not procutil.findexe(exe):
117 117 if abort:
118 118 exc = error.Abort
119 119 else:
120 120 exc = MissingTool
121 121 raise exc(_(b'cannot find required "%s" tool') % name)
122 122
123 123
124 124 class NoRepo(Exception):
125 125 pass
126 126
127 127
128 128 SKIPREV = b'SKIP'
129 129
130 130
131 131 class commit(object):
132 132 def __init__(
133 133 self,
134 134 author,
135 135 date,
136 136 desc,
137 137 parents,
138 138 branch=None,
139 139 rev=None,
140 140 extra=None,
141 141 sortkey=None,
142 142 saverev=True,
143 143 phase=phases.draft,
144 144 optparents=None,
145 145 ctx=None,
146 146 ):
147 147 self.author = author or b'unknown'
148 148 self.date = date or b'0 0'
149 149 self.desc = desc
150 150 self.parents = parents # will be converted and used as parents
151 151 self.optparents = optparents or [] # will be used if already converted
152 152 self.branch = branch
153 153 self.rev = rev
154 154 self.extra = extra or {}
155 155 self.sortkey = sortkey
156 156 self.saverev = saverev
157 157 self.phase = phase
158 158 self.ctx = ctx # for hg to hg conversions
159 159
160 160
161 161 class converter_source(object):
162 162 """Conversion source interface"""
163 163
164 164 def __init__(self, ui, repotype, path=None, revs=None):
165 165 """Initialize conversion source (or raise NoRepo("message")
166 166 exception if path is not a valid repository)"""
167 167 self.ui = ui
168 168 self.path = path
169 169 self.revs = revs
170 170 self.repotype = repotype
171 171
172 172 self.encoding = b'utf-8'
173 173
174 174 def checkhexformat(self, revstr, mapname=b'splicemap'):
175 175 """fails if revstr is not a 40 byte hex. mercurial and git both uses
176 176 such format for their revision numbering
177 177 """
178 178 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
179 179 raise error.Abort(
180 180 _(b'%s entry %s is not a valid revision identifier')
181 181 % (mapname, revstr)
182 182 )
183 183
184 184 def before(self):
185 185 pass
186 186
187 187 def after(self):
188 188 pass
189 189
190 190 def targetfilebelongstosource(self, targetfilename):
191 191 """Returns true if the given targetfile belongs to the source repo. This
192 192 is useful when only a subdirectory of the target belongs to the source
193 193 repo."""
194 194 # For normal full repo converts, this is always True.
195 195 return True
196 196
197 197 def setrevmap(self, revmap):
198 198 """set the map of already-converted revisions"""
199 199
200 200 def getheads(self):
201 201 """Return a list of this repository's heads"""
202 202 raise NotImplementedError
203 203
204 204 def getfile(self, name, rev):
205 205 """Return a pair (data, mode) where data is the file content
206 206 as a string and mode one of '', 'x' or 'l'. rev is the
207 207 identifier returned by a previous call to getchanges().
208 208 Data is None if file is missing/deleted in rev.
209 209 """
210 210 raise NotImplementedError
211 211
212 212 def getchanges(self, version, full):
213 213 """Returns a tuple of (files, copies, cleanp2).
214 214
215 215 files is a sorted list of (filename, id) tuples for all files
216 216 changed between version and its first parent returned by
217 217 getcommit(). If full, all files in that revision is returned.
218 218 id is the source revision id of the file.
219 219
220 220 copies is a dictionary of dest: source
221 221
222 222 cleanp2 is the set of files filenames that are clean against p2.
223 223 (Files that are clean against p1 are already not in files (unless
224 224 full). This makes it possible to handle p2 clean files similarly.)
225 225 """
226 226 raise NotImplementedError
227 227
228 228 def getcommit(self, version):
229 229 """Return the commit object for version"""
230 230 raise NotImplementedError
231 231
232 232 def numcommits(self):
233 233 """Return the number of commits in this source.
234 234
235 235 If unknown, return None.
236 236 """
237 237 return None
238 238
239 239 def gettags(self):
240 240 """Return the tags as a dictionary of name: revision
241 241
242 242 Tag names must be UTF-8 strings.
243 243 """
244 244 raise NotImplementedError
245 245
246 246 def recode(self, s, encoding=None):
247 247 if not encoding:
248 248 encoding = self.encoding or b'utf-8'
249 249
250 250 if isinstance(s, pycompat.unicode):
251 251 return s.encode("utf-8")
252 252 try:
253 253 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
254 254 except UnicodeError:
255 255 try:
256 256 return s.decode("latin-1").encode("utf-8")
257 257 except UnicodeError:
258 258 return s.decode(pycompat.sysstr(encoding), "replace").encode(
259 259 "utf-8"
260 260 )
261 261
262 262 def getchangedfiles(self, rev, i):
263 263 """Return the files changed by rev compared to parent[i].
264 264
265 265 i is an index selecting one of the parents of rev. The return
266 266 value should be the list of files that are different in rev and
267 267 this parent.
268 268
269 269 If rev has no parents, i is None.
270 270
271 271 This function is only needed to support --filemap
272 272 """
273 273 raise NotImplementedError
274 274
275 275 def converted(self, rev, sinkrev):
276 276 '''Notify the source that a revision has been converted.'''
277 277
278 278 def hasnativeorder(self):
279 279 """Return true if this source has a meaningful, native revision
280 280 order. For instance, Mercurial revisions are store sequentially
281 281 while there is no such global ordering with Darcs.
282 282 """
283 283 return False
284 284
285 285 def hasnativeclose(self):
286 286 """Return true if this source has ability to close branch."""
287 287 return False
288 288
289 289 def lookuprev(self, rev):
290 290 """If rev is a meaningful revision reference in source, return
291 291 the referenced identifier in the same format used by getcommit().
292 292 return None otherwise.
293 293 """
294 294 return None
295 295
296 296 def getbookmarks(self):
297 297 """Return the bookmarks as a dictionary of name: revision
298 298
299 299 Bookmark names are to be UTF-8 strings.
300 300 """
301 301 return {}
302 302
303 303 def checkrevformat(self, revstr, mapname=b'splicemap'):
304 304 """revstr is a string that describes a revision in the given
305 305 source control system. Return true if revstr has correct
306 306 format.
307 307 """
308 308 return True
309 309
310 310
311 311 class converter_sink(object):
312 312 """Conversion sink (target) interface"""
313 313
314 314 def __init__(self, ui, repotype, path):
315 315 """Initialize conversion sink (or raise NoRepo("message")
316 316 exception if path is not a valid repository)
317 317
318 318 created is a list of paths to remove if a fatal error occurs
319 319 later"""
320 320 self.ui = ui
321 321 self.path = path
322 322 self.created = []
323 323 self.repotype = repotype
324 324
325 325 def revmapfile(self):
326 326 """Path to a file that will contain lines
327 327 source_rev_id sink_rev_id
328 328 mapping equivalent revision identifiers for each system."""
329 329 raise NotImplementedError
330 330
331 331 def authorfile(self):
332 332 """Path to a file that will contain lines
333 333 srcauthor=dstauthor
334 334 mapping equivalent authors identifiers for each system."""
335 335 return None
336 336
337 337 def putcommit(
338 338 self, files, copies, parents, commit, source, revmap, full, cleanp2
339 339 ):
340 340 """Create a revision with all changed files listed in 'files'
341 341 and having listed parents. 'commit' is a commit object
342 342 containing at a minimum the author, date, and message for this
343 343 changeset. 'files' is a list of (path, version) tuples,
344 344 'copies' is a dictionary mapping destinations to sources,
345 345 'source' is the source repository, and 'revmap' is a mapfile
346 346 of source revisions to converted revisions. Only getfile() and
347 347 lookuprev() should be called on 'source'. 'full' means that 'files'
348 348 is complete and all other files should be removed.
349 349 'cleanp2' is a set of the filenames that are unchanged from p2
350 350 (only in the common merge case where there two parents).
351 351
352 352 Note that the sink repository is not told to update itself to
353 353 a particular revision (or even what that revision would be)
354 354 before it receives the file data.
355 355 """
356 356 raise NotImplementedError
357 357
358 358 def puttags(self, tags):
359 359 """Put tags into sink.
360 360
361 361 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
362 362 Return a pair (tag_revision, tag_parent_revision), or (None, None)
363 363 if nothing was changed.
364 364 """
365 365 raise NotImplementedError
366 366
367 367 def setbranch(self, branch, pbranches):
368 368 """Set the current branch name. Called before the first putcommit
369 369 on the branch.
370 370 branch: branch name for subsequent commits
371 371 pbranches: (converted parent revision, parent branch) tuples"""
372 372
373 373 def setfilemapmode(self, active):
374 374 """Tell the destination that we're using a filemap
375 375
376 376 Some converter_sources (svn in particular) can claim that a file
377 377 was changed in a revision, even if there was no change. This method
378 378 tells the destination that we're using a filemap and that it should
379 379 filter empty revisions.
380 380 """
381 381
382 382 def before(self):
383 383 pass
384 384
385 385 def after(self):
386 386 pass
387 387
388 388 def putbookmarks(self, bookmarks):
389 389 """Put bookmarks into sink.
390 390
391 391 bookmarks: {bookmarkname: sink_rev_id, ...}
392 392 where bookmarkname is an UTF-8 string.
393 393 """
394 394
395 395 def hascommitfrommap(self, rev):
396 396 """Return False if a rev mentioned in a filemap is known to not be
397 397 present."""
398 398 raise NotImplementedError
399 399
400 400 def hascommitforsplicemap(self, rev):
401 401 """This method is for the special needs for splicemap handling and not
402 402 for general use. Returns True if the sink contains rev, aborts on some
403 403 special cases."""
404 404 raise NotImplementedError
405 405
406 406
407 407 class commandline(object):
408 408 def __init__(self, ui, command):
409 409 self.ui = ui
410 410 self.command = command
411 411
412 412 def prerun(self):
413 413 pass
414 414
415 415 def postrun(self):
416 416 pass
417 417
418 418 def _cmdline(self, cmd, *args, **kwargs):
419 419 kwargs = pycompat.byteskwargs(kwargs)
420 420 cmdline = [self.command, cmd] + list(args)
421 421 for k, v in pycompat.iteritems(kwargs):
422 422 if len(k) == 1:
423 423 cmdline.append(b'-' + k)
424 424 else:
425 425 cmdline.append(b'--' + k.replace(b'_', b'-'))
426 426 try:
427 427 if len(k) == 1:
428 428 cmdline.append(b'' + v)
429 429 else:
430 430 cmdline[-1] += b'=' + v
431 431 except TypeError:
432 432 pass
433 433 cmdline = [procutil.shellquote(arg) for arg in cmdline]
434 434 if not self.ui.debugflag:
435 435 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
436 436 cmdline = b' '.join(cmdline)
437 437 return cmdline
438 438
439 439 def _run(self, cmd, *args, **kwargs):
440 440 def popen(cmdline):
441 441 p = subprocess.Popen(
442 442 procutil.tonativestr(cmdline),
443 443 shell=True,
444 444 bufsize=-1,
445 445 close_fds=procutil.closefds,
446 446 stdout=subprocess.PIPE,
447 447 )
448 448 return p
449 449
450 450 return self._dorun(popen, cmd, *args, **kwargs)
451 451
452 452 def _run2(self, cmd, *args, **kwargs):
453 453 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
454 454
455 455 def _run3(self, cmd, *args, **kwargs):
456 456 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
457 457
458 458 def _dorun(self, openfunc, cmd, *args, **kwargs):
459 459 cmdline = self._cmdline(cmd, *args, **kwargs)
460 460 self.ui.debug(b'running: %s\n' % (cmdline,))
461 461 self.prerun()
462 462 try:
463 463 return openfunc(cmdline)
464 464 finally:
465 465 self.postrun()
466 466
467 467 def run(self, cmd, *args, **kwargs):
468 468 p = self._run(cmd, *args, **kwargs)
469 469 output = p.communicate()[0]
470 470 self.ui.debug(output)
471 471 return output, p.returncode
472 472
473 473 def runlines(self, cmd, *args, **kwargs):
474 474 p = self._run(cmd, *args, **kwargs)
475 475 output = p.stdout.readlines()
476 476 p.wait()
477 477 self.ui.debug(b''.join(output))
478 478 return output, p.returncode
479 479
480 480 def checkexit(self, status, output=b''):
481 481 if status:
482 482 if output:
483 483 self.ui.warn(_(b'%s error:\n') % self.command)
484 484 self.ui.warn(output)
485 485 msg = procutil.explainexit(status)
486 486 raise error.Abort(b'%s %s' % (self.command, msg))
487 487
488 488 def run0(self, cmd, *args, **kwargs):
489 489 output, status = self.run(cmd, *args, **kwargs)
490 490 self.checkexit(status, output)
491 491 return output
492 492
493 493 def runlines0(self, cmd, *args, **kwargs):
494 494 output, status = self.runlines(cmd, *args, **kwargs)
495 495 self.checkexit(status, b''.join(output))
496 496 return output
497 497
498 498 @propertycache
499 499 def argmax(self):
500 500 # POSIX requires at least 4096 bytes for ARG_MAX
501 501 argmax = 4096
502 502 try:
503 503 argmax = os.sysconf("SC_ARG_MAX")
504 504 except (AttributeError, ValueError):
505 505 pass
506 506
507 507 # Windows shells impose their own limits on command line length,
508 508 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
509 509 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
510 510 # details about cmd.exe limitations.
511 511
512 512 # Since ARG_MAX is for command line _and_ environment, lower our limit
513 513 # (and make happy Windows shells while doing this).
514 514 return argmax // 2 - 1
515 515
516 516 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
517 517 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
518 518 limit = self.argmax - cmdlen
519 519 numbytes = 0
520 520 fl = []
521 521 for fn in arglist:
522 522 b = len(fn) + 3
523 523 if numbytes + b < limit or len(fl) == 0:
524 524 fl.append(fn)
525 525 numbytes += b
526 526 else:
527 527 yield fl
528 528 fl = [fn]
529 529 numbytes = b
530 530 if fl:
531 531 yield fl
532 532
533 533 def xargs(self, arglist, cmd, *args, **kwargs):
534 534 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
535 535 self.run0(cmd, *(list(args) + l), **kwargs)
536 536
537 537
538 538 class mapfile(dict):
539 539 def __init__(self, ui, path):
540 540 super(mapfile, self).__init__()
541 541 self.ui = ui
542 542 self.path = path
543 543 self.fp = None
544 544 self.order = []
545 545 self._read()
546 546
547 547 def _read(self):
548 548 if not self.path:
549 549 return
550 550 try:
551 551 fp = open(self.path, b'rb')
552 552 except IOError as err:
553 553 if err.errno != errno.ENOENT:
554 554 raise
555 555 return
556 556 for i, line in enumerate(util.iterfile(fp)):
557 557 line = line.splitlines()[0].rstrip()
558 558 if not line:
559 559 # Ignore blank lines
560 560 continue
561 561 try:
562 562 key, value = line.rsplit(b' ', 1)
563 563 except ValueError:
564 564 raise error.Abort(
565 565 _(b'syntax error in %s(%d): key/value pair expected')
566 566 % (self.path, i + 1)
567 567 )
568 568 if key not in self:
569 569 self.order.append(key)
570 570 super(mapfile, self).__setitem__(key, value)
571 571 fp.close()
572 572
573 573 def __setitem__(self, key, value):
574 574 if self.fp is None:
575 575 try:
576 576 self.fp = open(self.path, b'ab')
577 577 except IOError as err:
578 578 raise error.Abort(
579 579 _(b'could not open map file %r: %s')
580 580 % (self.path, encoding.strtolocal(err.strerror))
581 581 )
582 582 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
583 583 self.fp.flush()
584 584 super(mapfile, self).__setitem__(key, value)
585 585
586 586 def close(self):
587 587 if self.fp:
588 588 self.fp.close()
589 589 self.fp = None
590 590
591 591
592 592 def makedatetimestamp(t):
593 593 """Like dateutil.makedate() but for time t instead of current time"""
594 594 delta = datetime.datetime.utcfromtimestamp(
595 595 t
596 596 ) - datetime.datetime.fromtimestamp(t)
597 597 tz = delta.days * 86400 + delta.seconds
598 598 return t, tz
@@ -1,1070 +1,1069 b''
1 1 # Mercurial built-in replacement for cvsps.
2 2 #
3 3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import functools
10 10 import os
11 import pickle
11 12 import re
12 13
13 14 from mercurial.i18n import _
14 15 from mercurial.pycompat import open
15 16 from mercurial import (
16 17 encoding,
17 18 error,
18 19 hook,
19 20 pycompat,
20 21 util,
21 22 )
22 23 from mercurial.utils import (
23 24 dateutil,
24 25 procutil,
25 26 stringutil,
26 27 )
27 28
28 pickle = util.pickle
29
30 29
31 30 class logentry(object):
32 31 """Class logentry has the following attributes:
33 32 .author - author name as CVS knows it
34 33 .branch - name of branch this revision is on
35 34 .branches - revision tuple of branches starting at this revision
36 35 .comment - commit message
37 36 .commitid - CVS commitid or None
38 37 .date - the commit date as a (time, tz) tuple
39 38 .dead - true if file revision is dead
40 39 .file - Name of file
41 40 .lines - a tuple (+lines, -lines) or None
42 41 .parent - Previous revision of this entry
43 42 .rcs - name of file as returned from CVS
44 43 .revision - revision number as tuple
45 44 .tags - list of tags on the file
46 45 .synthetic - is this a synthetic "file ... added on ..." revision?
47 46 .mergepoint - the branch that has been merged from (if present in
48 47 rlog output) or None
49 48 .branchpoints - the branches that start at the current entry or empty
50 49 """
51 50
52 51 def __init__(self, **entries):
53 52 self.synthetic = False
54 53 self.__dict__.update(entries)
55 54
56 55 def __repr__(self):
57 56 items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__))
58 57 return "%s(%s)" % (type(self).__name__, ", ".join(items))
59 58
60 59
61 60 class logerror(Exception):
62 61 pass
63 62
64 63
65 64 def getrepopath(cvspath):
66 65 """Return the repository path from a CVS path.
67 66
68 67 >>> getrepopath(b'/foo/bar')
69 68 '/foo/bar'
70 69 >>> getrepopath(b'c:/foo/bar')
71 70 '/foo/bar'
72 71 >>> getrepopath(b':pserver:10/foo/bar')
73 72 '/foo/bar'
74 73 >>> getrepopath(b':pserver:10c:/foo/bar')
75 74 '/foo/bar'
76 75 >>> getrepopath(b':pserver:/foo/bar')
77 76 '/foo/bar'
78 77 >>> getrepopath(b':pserver:c:/foo/bar')
79 78 '/foo/bar'
80 79 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
81 80 '/foo/bar'
82 81 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
83 82 '/foo/bar'
84 83 >>> getrepopath(b'user@server/path/to/repository')
85 84 '/path/to/repository'
86 85 """
87 86 # According to CVS manual, CVS paths are expressed like:
88 87 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
89 88 #
90 89 # CVSpath is splitted into parts and then position of the first occurrence
91 90 # of the '/' char after the '@' is located. The solution is the rest of the
92 91 # string after that '/' sign including it
93 92
94 93 parts = cvspath.split(b':')
95 94 atposition = parts[-1].find(b'@')
96 95 start = 0
97 96
98 97 if atposition != -1:
99 98 start = atposition
100 99
101 100 repopath = parts[-1][parts[-1].find(b'/', start) :]
102 101 return repopath
103 102
104 103
105 104 def createlog(ui, directory=None, root=b"", rlog=True, cache=None):
106 105 '''Collect the CVS rlog'''
107 106
108 107 # Because we store many duplicate commit log messages, reusing strings
109 108 # saves a lot of memory and pickle storage space.
110 109 _scache = {}
111 110
112 111 def scache(s):
113 112 """return a shared version of a string"""
114 113 return _scache.setdefault(s, s)
115 114
116 115 ui.status(_(b'collecting CVS rlog\n'))
117 116
118 117 log = [] # list of logentry objects containing the CVS state
119 118
120 119 # patterns to match in CVS (r)log output, by state of use
121 120 re_00 = re.compile(b'RCS file: (.+)$')
122 121 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
123 122 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
124 123 re_03 = re.compile(
125 124 b"(Cannot access.+CVSROOT)|(can't create temporary directory.+)$"
126 125 )
127 126 re_10 = re.compile(b'Working file: (.+)$')
128 127 re_20 = re.compile(b'symbolic names:')
129 128 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
130 129 re_31 = re.compile(b'----------------------------$')
131 130 re_32 = re.compile(
132 131 b'======================================='
133 132 b'======================================$'
134 133 )
135 134 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
136 135 re_60 = re.compile(
137 136 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
138 137 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
139 138 br'(\s+commitid:\s+([^;]+);)?'
140 139 br'(.*mergepoint:\s+([^;]+);)?'
141 140 )
142 141 re_70 = re.compile(b'branches: (.+);$')
143 142
144 143 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
145 144
146 145 prefix = b'' # leading path to strip of what we get from CVS
147 146
148 147 if directory is None:
149 148 # Current working directory
150 149
151 150 # Get the real directory in the repository
152 151 try:
153 152 with open(os.path.join(b'CVS', b'Repository'), b'rb') as f:
154 153 prefix = f.read().strip()
155 154 directory = prefix
156 155 if prefix == b".":
157 156 prefix = b""
158 157 except IOError:
159 158 raise logerror(_(b'not a CVS sandbox'))
160 159
161 160 if prefix and not prefix.endswith(pycompat.ossep):
162 161 prefix += pycompat.ossep
163 162
164 163 # Use the Root file in the sandbox, if it exists
165 164 try:
166 165 root = open(os.path.join(b'CVS', b'Root'), b'rb').read().strip()
167 166 except IOError:
168 167 pass
169 168
170 169 if not root:
171 170 root = encoding.environ.get(b'CVSROOT', b'')
172 171
173 172 # read log cache if one exists
174 173 oldlog = []
175 174 date = None
176 175
177 176 if cache:
178 177 cachedir = os.path.expanduser(b'~/.hg.cvsps')
179 178 if not os.path.exists(cachedir):
180 179 os.mkdir(cachedir)
181 180
182 181 # The cvsps cache pickle needs a uniquified name, based on the
183 182 # repository location. The address may have all sort of nasties
184 183 # in it, slashes, colons and such. So here we take just the
185 184 # alphanumeric characters, concatenated in a way that does not
186 185 # mix up the various components, so that
187 186 # :pserver:user@server:/path
188 187 # and
189 188 # /pserver/user/server/path
190 189 # are mapped to different cache file names.
191 190 cachefile = root.split(b":") + [directory, b"cache"]
192 191 cachefile = [b'-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
193 192 cachefile = os.path.join(
194 193 cachedir, b'.'.join([s for s in cachefile if s])
195 194 )
196 195
197 196 if cache == b'update':
198 197 try:
199 198 ui.note(_(b'reading cvs log cache %s\n') % cachefile)
200 199 oldlog = pickle.load(open(cachefile, b'rb'))
201 200 for e in oldlog:
202 201 if not (
203 202 util.safehasattr(e, b'branchpoints')
204 203 and util.safehasattr(e, b'commitid')
205 204 and util.safehasattr(e, b'mergepoint')
206 205 ):
207 206 ui.status(_(b'ignoring old cache\n'))
208 207 oldlog = []
209 208 break
210 209
211 210 ui.note(_(b'cache has %d log entries\n') % len(oldlog))
212 211 except Exception as e:
213 212 ui.note(_(b'error reading cache: %r\n') % e)
214 213
215 214 if oldlog:
216 215 date = oldlog[-1].date # last commit date as a (time,tz) tuple
217 216 date = dateutil.datestr(date, b'%Y/%m/%d %H:%M:%S %1%2')
218 217
219 218 # build the CVS commandline
220 219 cmd = [b'cvs', b'-q']
221 220 if root:
222 221 cmd.append(b'-d%s' % root)
223 222 p = util.normpath(getrepopath(root))
224 223 if not p.endswith(b'/'):
225 224 p += b'/'
226 225 if prefix:
227 226 # looks like normpath replaces "" by "."
228 227 prefix = p + util.normpath(prefix)
229 228 else:
230 229 prefix = p
231 230 cmd.append([b'log', b'rlog'][rlog])
232 231 if date:
233 232 # no space between option and date string
234 233 cmd.append(b'-d>%s' % date)
235 234 cmd.append(directory)
236 235
237 236 # state machine begins here
238 237 tags = {} # dictionary of revisions on current file with their tags
239 238 branchmap = {} # mapping between branch names and revision numbers
240 239 rcsmap = {}
241 240 state = 0
242 241 store = False # set when a new record can be appended
243 242
244 243 cmd = [procutil.shellquote(arg) for arg in cmd]
245 244 ui.note(_(b"running %s\n") % (b' '.join(cmd)))
246 245 ui.debug(b"prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
247 246
248 247 pfp = procutil.popen(b' '.join(cmd), b'rb')
249 248 peek = util.fromnativeeol(pfp.readline())
250 249 while True:
251 250 line = peek
252 251 if line == b'':
253 252 break
254 253 peek = util.fromnativeeol(pfp.readline())
255 254 if line.endswith(b'\n'):
256 255 line = line[:-1]
257 256 # ui.debug('state=%d line=%r\n' % (state, line))
258 257
259 258 if state == 0:
260 259 # initial state, consume input until we see 'RCS file'
261 260 match = re_00.match(line)
262 261 if match:
263 262 rcs = match.group(1)
264 263 tags = {}
265 264 if rlog:
266 265 filename = util.normpath(rcs[:-2])
267 266 if filename.startswith(prefix):
268 267 filename = filename[len(prefix) :]
269 268 if filename.startswith(b'/'):
270 269 filename = filename[1:]
271 270 if filename.startswith(b'Attic/'):
272 271 filename = filename[6:]
273 272 else:
274 273 filename = filename.replace(b'/Attic/', b'/')
275 274 state = 2
276 275 continue
277 276 state = 1
278 277 continue
279 278 match = re_01.match(line)
280 279 if match:
281 280 raise logerror(match.group(1))
282 281 match = re_02.match(line)
283 282 if match:
284 283 raise logerror(match.group(2))
285 284 if re_03.match(line):
286 285 raise logerror(line)
287 286
288 287 elif state == 1:
289 288 # expect 'Working file' (only when using log instead of rlog)
290 289 match = re_10.match(line)
291 290 assert match, _(b'RCS file must be followed by working file')
292 291 filename = util.normpath(match.group(1))
293 292 state = 2
294 293
295 294 elif state == 2:
296 295 # expect 'symbolic names'
297 296 if re_20.match(line):
298 297 branchmap = {}
299 298 state = 3
300 299
301 300 elif state == 3:
302 301 # read the symbolic names and store as tags
303 302 match = re_30.match(line)
304 303 if match:
305 304 rev = [int(x) for x in match.group(2).split(b'.')]
306 305
307 306 # Convert magic branch number to an odd-numbered one
308 307 revn = len(rev)
309 308 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
310 309 rev = rev[:-2] + rev[-1:]
311 310 rev = tuple(rev)
312 311
313 312 if rev not in tags:
314 313 tags[rev] = []
315 314 tags[rev].append(match.group(1))
316 315 branchmap[match.group(1)] = match.group(2)
317 316
318 317 elif re_31.match(line):
319 318 state = 5
320 319 elif re_32.match(line):
321 320 state = 0
322 321
323 322 elif state == 4:
324 323 # expecting '------' separator before first revision
325 324 if re_31.match(line):
326 325 state = 5
327 326 else:
328 327 assert not re_32.match(line), _(
329 328 b'must have at least some revisions'
330 329 )
331 330
332 331 elif state == 5:
333 332 # expecting revision number and possibly (ignored) lock indication
334 333 # we create the logentry here from values stored in states 0 to 4,
335 334 # as this state is re-entered for subsequent revisions of a file.
336 335 match = re_50.match(line)
337 336 assert match, _(b'expected revision number')
338 337 e = logentry(
339 338 rcs=scache(rcs),
340 339 file=scache(filename),
341 340 revision=tuple([int(x) for x in match.group(1).split(b'.')]),
342 341 branches=[],
343 342 parent=None,
344 343 commitid=None,
345 344 mergepoint=None,
346 345 branchpoints=set(),
347 346 )
348 347
349 348 state = 6
350 349
351 350 elif state == 6:
352 351 # expecting date, author, state, lines changed
353 352 match = re_60.match(line)
354 353 assert match, _(b'revision must be followed by date line')
355 354 d = match.group(1)
356 355 if d[2] == b'/':
357 356 # Y2K
358 357 d = b'19' + d
359 358
360 359 if len(d.split()) != 3:
361 360 # cvs log dates always in GMT
362 361 d = d + b' UTC'
363 362 e.date = dateutil.parsedate(
364 363 d,
365 364 [
366 365 b'%y/%m/%d %H:%M:%S',
367 366 b'%Y/%m/%d %H:%M:%S',
368 367 b'%Y-%m-%d %H:%M:%S',
369 368 ],
370 369 )
371 370 e.author = scache(match.group(2))
372 371 e.dead = match.group(3).lower() == b'dead'
373 372
374 373 if match.group(5):
375 374 if match.group(6):
376 375 e.lines = (int(match.group(5)), int(match.group(6)))
377 376 else:
378 377 e.lines = (int(match.group(5)), 0)
379 378 elif match.group(6):
380 379 e.lines = (0, int(match.group(6)))
381 380 else:
382 381 e.lines = None
383 382
384 383 if match.group(7): # cvs 1.12 commitid
385 384 e.commitid = match.group(8)
386 385
387 386 if match.group(9): # cvsnt mergepoint
388 387 myrev = match.group(10).split(b'.')
389 388 if len(myrev) == 2: # head
390 389 e.mergepoint = b'HEAD'
391 390 else:
392 391 myrev = b'.'.join(myrev[:-2] + [b'0', myrev[-2]])
393 392 branches = [b for b in branchmap if branchmap[b] == myrev]
394 393 assert len(branches) == 1, (
395 394 b'unknown branch: %s' % e.mergepoint
396 395 )
397 396 e.mergepoint = branches[0]
398 397
399 398 e.comment = []
400 399 state = 7
401 400
402 401 elif state == 7:
403 402 # read the revision numbers of branches that start at this revision
404 403 # or store the commit log message otherwise
405 404 m = re_70.match(line)
406 405 if m:
407 406 e.branches = [
408 407 tuple([int(y) for y in x.strip().split(b'.')])
409 408 for x in m.group(1).split(b';')
410 409 ]
411 410 state = 8
412 411 elif re_31.match(line) and re_50.match(peek):
413 412 state = 5
414 413 store = True
415 414 elif re_32.match(line):
416 415 state = 0
417 416 store = True
418 417 else:
419 418 e.comment.append(line)
420 419
421 420 elif state == 8:
422 421 # store commit log message
423 422 if re_31.match(line):
424 423 cpeek = peek
425 424 if cpeek.endswith(b'\n'):
426 425 cpeek = cpeek[:-1]
427 426 if re_50.match(cpeek):
428 427 state = 5
429 428 store = True
430 429 else:
431 430 e.comment.append(line)
432 431 elif re_32.match(line):
433 432 state = 0
434 433 store = True
435 434 else:
436 435 e.comment.append(line)
437 436
438 437 # When a file is added on a branch B1, CVS creates a synthetic
439 438 # dead trunk revision 1.1 so that the branch has a root.
440 439 # Likewise, if you merge such a file to a later branch B2 (one
441 440 # that already existed when the file was added on B1), CVS
442 441 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
443 442 # these revisions now, but mark them synthetic so
444 443 # createchangeset() can take care of them.
445 444 if (
446 445 store
447 446 and e.dead
448 447 and e.revision[-1] == 1
449 448 and len(e.comment) == 1 # 1.1 or 1.1.x.1
450 449 and file_added_re.match(e.comment[0])
451 450 ):
452 451 ui.debug(
453 452 b'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
454 453 )
455 454 e.synthetic = True
456 455
457 456 if store:
458 457 # clean up the results and save in the log.
459 458 store = False
460 459 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
461 460 e.comment = scache(b'\n'.join(e.comment))
462 461
463 462 revn = len(e.revision)
464 463 if revn > 3 and (revn % 2) == 0:
465 464 e.branch = tags.get(e.revision[:-1], [None])[0]
466 465 else:
467 466 e.branch = None
468 467
469 468 # find the branches starting from this revision
470 469 branchpoints = set()
471 470 for branch, revision in pycompat.iteritems(branchmap):
472 471 revparts = tuple([int(i) for i in revision.split(b'.')])
473 472 if len(revparts) < 2: # bad tags
474 473 continue
475 474 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
476 475 # normal branch
477 476 if revparts[:-2] == e.revision:
478 477 branchpoints.add(branch)
479 478 elif revparts == (1, 1, 1): # vendor branch
480 479 if revparts in e.branches:
481 480 branchpoints.add(branch)
482 481 e.branchpoints = branchpoints
483 482
484 483 log.append(e)
485 484
486 485 rcsmap[e.rcs.replace(b'/Attic/', b'/')] = e.rcs
487 486
488 487 if len(log) % 100 == 0:
489 488 ui.status(
490 489 stringutil.ellipsis(b'%d %s' % (len(log), e.file), 80)
491 490 + b'\n'
492 491 )
493 492
494 493 log.sort(key=lambda x: (x.rcs, x.revision))
495 494
496 495 # find parent revisions of individual files
497 496 versions = {}
498 497 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
499 498 rcs = e.rcs.replace(b'/Attic/', b'/')
500 499 if rcs in rcsmap:
501 500 e.rcs = rcsmap[rcs]
502 501 branch = e.revision[:-1]
503 502 versions[(e.rcs, branch)] = e.revision
504 503
505 504 for e in log:
506 505 branch = e.revision[:-1]
507 506 p = versions.get((e.rcs, branch), None)
508 507 if p is None:
509 508 p = e.revision[:-2]
510 509 e.parent = p
511 510 versions[(e.rcs, branch)] = e.revision
512 511
513 512 # update the log cache
514 513 if cache:
515 514 if log:
516 515 # join up the old and new logs
517 516 log.sort(key=lambda x: x.date)
518 517
519 518 if oldlog and oldlog[-1].date >= log[0].date:
520 519 raise logerror(
521 520 _(
522 521 b'log cache overlaps with new log entries,'
523 522 b' re-run without cache.'
524 523 )
525 524 )
526 525
527 526 log = oldlog + log
528 527
529 528 # write the new cachefile
530 529 ui.note(_(b'writing cvs log cache %s\n') % cachefile)
531 530 pickle.dump(log, open(cachefile, b'wb'))
532 531 else:
533 532 log = oldlog
534 533
535 534 ui.status(_(b'%d log entries\n') % len(log))
536 535
537 536 encodings = ui.configlist(b'convert', b'cvsps.logencoding')
538 537 if encodings:
539 538
540 539 def revstr(r):
541 540 # this is needed, because logentry.revision is a tuple of "int"
542 541 # (e.g. (1, 2) for "1.2")
543 542 return b'.'.join(pycompat.maplist(pycompat.bytestr, r))
544 543
545 544 for entry in log:
546 545 comment = entry.comment
547 546 for e in encodings:
548 547 try:
549 548 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
550 549 'utf-8'
551 550 )
552 551 if ui.debugflag:
553 552 ui.debug(
554 553 b"transcoding by %s: %s of %s\n"
555 554 % (e, revstr(entry.revision), entry.file)
556 555 )
557 556 break
558 557 except UnicodeDecodeError:
559 558 pass # try next encoding
560 559 except LookupError as inst: # unknown encoding, maybe
561 560 raise error.Abort(
562 561 pycompat.bytestr(inst),
563 562 hint=_(
564 563 b'check convert.cvsps.logencoding configuration'
565 564 ),
566 565 )
567 566 else:
568 567 raise error.Abort(
569 568 _(
570 569 b"no encoding can transcode"
571 570 b" CVS log message for %s of %s"
572 571 )
573 572 % (revstr(entry.revision), entry.file),
574 573 hint=_(b'check convert.cvsps.logencoding configuration'),
575 574 )
576 575
577 576 hook.hook(ui, None, b"cvslog", True, log=log)
578 577
579 578 return log
580 579
581 580
582 581 class changeset(object):
583 582 """Class changeset has the following attributes:
584 583 .id - integer identifying this changeset (list index)
585 584 .author - author name as CVS knows it
586 585 .branch - name of branch this changeset is on, or None
587 586 .comment - commit message
588 587 .commitid - CVS commitid or None
589 588 .date - the commit date as a (time,tz) tuple
590 589 .entries - list of logentry objects in this changeset
591 590 .parents - list of one or two parent changesets
592 591 .tags - list of tags on this changeset
593 592 .synthetic - from synthetic revision "file ... added on branch ..."
594 593 .mergepoint- the branch that has been merged from or None
595 594 .branchpoints- the branches that start at the current entry or empty
596 595 """
597 596
598 597 def __init__(self, **entries):
599 598 self.id = None
600 599 self.synthetic = False
601 600 self.__dict__.update(entries)
602 601
603 602 def __repr__(self):
604 603 items = (
605 604 b"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
606 605 )
607 606 return b"%s(%s)" % (type(self).__name__, b", ".join(items))
608 607
609 608
610 609 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
611 610 '''Convert log into changesets.'''
612 611
613 612 ui.status(_(b'creating changesets\n'))
614 613
615 614 # try to order commitids by date
616 615 mindate = {}
617 616 for e in log:
618 617 if e.commitid:
619 618 if e.commitid not in mindate:
620 619 mindate[e.commitid] = e.date
621 620 else:
622 621 mindate[e.commitid] = min(e.date, mindate[e.commitid])
623 622
624 623 # Merge changesets
625 624 log.sort(
626 625 key=lambda x: (
627 626 mindate.get(x.commitid, (-1, 0)),
628 627 x.commitid or b'',
629 628 x.comment,
630 629 x.author,
631 630 x.branch or b'',
632 631 x.date,
633 632 x.branchpoints,
634 633 )
635 634 )
636 635
637 636 changesets = []
638 637 files = set()
639 638 c = None
640 639 for i, e in enumerate(log):
641 640
642 641 # Check if log entry belongs to the current changeset or not.
643 642
644 643 # Since CVS is file-centric, two different file revisions with
645 644 # different branchpoints should be treated as belonging to two
646 645 # different changesets (and the ordering is important and not
647 646 # honoured by cvsps at this point).
648 647 #
649 648 # Consider the following case:
650 649 # foo 1.1 branchpoints: [MYBRANCH]
651 650 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
652 651 #
653 652 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
654 653 # later version of foo may be in MYBRANCH2, so foo should be the
655 654 # first changeset and bar the next and MYBRANCH and MYBRANCH2
656 655 # should both start off of the bar changeset. No provisions are
657 656 # made to ensure that this is, in fact, what happens.
658 657 if not (
659 658 c
660 659 and e.branchpoints == c.branchpoints
661 660 and ( # cvs commitids
662 661 (e.commitid is not None and e.commitid == c.commitid)
663 662 or ( # no commitids, use fuzzy commit detection
664 663 (e.commitid is None or c.commitid is None)
665 664 and e.comment == c.comment
666 665 and e.author == c.author
667 666 and e.branch == c.branch
668 667 and (
669 668 (c.date[0] + c.date[1])
670 669 <= (e.date[0] + e.date[1])
671 670 <= (c.date[0] + c.date[1]) + fuzz
672 671 )
673 672 and e.file not in files
674 673 )
675 674 )
676 675 ):
677 676 c = changeset(
678 677 comment=e.comment,
679 678 author=e.author,
680 679 branch=e.branch,
681 680 date=e.date,
682 681 entries=[],
683 682 mergepoint=e.mergepoint,
684 683 branchpoints=e.branchpoints,
685 684 commitid=e.commitid,
686 685 )
687 686 changesets.append(c)
688 687
689 688 files = set()
690 689 if len(changesets) % 100 == 0:
691 690 t = b'%d %s' % (len(changesets), repr(e.comment)[1:-1])
692 691 ui.status(stringutil.ellipsis(t, 80) + b'\n')
693 692
694 693 c.entries.append(e)
695 694 files.add(e.file)
696 695 c.date = e.date # changeset date is date of latest commit in it
697 696
698 697 # Mark synthetic changesets
699 698
700 699 for c in changesets:
701 700 # Synthetic revisions always get their own changeset, because
702 701 # the log message includes the filename. E.g. if you add file3
703 702 # and file4 on a branch, you get four log entries and three
704 703 # changesets:
705 704 # "File file3 was added on branch ..." (synthetic, 1 entry)
706 705 # "File file4 was added on branch ..." (synthetic, 1 entry)
707 706 # "Add file3 and file4 to fix ..." (real, 2 entries)
708 707 # Hence the check for 1 entry here.
709 708 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
710 709
711 710 # Sort files in each changeset
712 711
713 712 def entitycompare(l, r):
714 713 """Mimic cvsps sorting order"""
715 714 l = l.file.split(b'/')
716 715 r = r.file.split(b'/')
717 716 nl = len(l)
718 717 nr = len(r)
719 718 n = min(nl, nr)
720 719 for i in range(n):
721 720 if i + 1 == nl and nl < nr:
722 721 return -1
723 722 elif i + 1 == nr and nl > nr:
724 723 return +1
725 724 elif l[i] < r[i]:
726 725 return -1
727 726 elif l[i] > r[i]:
728 727 return +1
729 728 return 0
730 729
731 730 for c in changesets:
732 731 c.entries.sort(key=functools.cmp_to_key(entitycompare))
733 732
734 733 # Sort changesets by date
735 734
736 735 odd = set()
737 736
738 737 def cscmp(l, r):
739 738 d = sum(l.date) - sum(r.date)
740 739 if d:
741 740 return d
742 741
743 742 # detect vendor branches and initial commits on a branch
744 743 le = {}
745 744 for e in l.entries:
746 745 le[e.rcs] = e.revision
747 746 re = {}
748 747 for e in r.entries:
749 748 re[e.rcs] = e.revision
750 749
751 750 d = 0
752 751 for e in l.entries:
753 752 if re.get(e.rcs, None) == e.parent:
754 753 assert not d
755 754 d = 1
756 755 break
757 756
758 757 for e in r.entries:
759 758 if le.get(e.rcs, None) == e.parent:
760 759 if d:
761 760 odd.add((l, r))
762 761 d = -1
763 762 break
764 763 # By this point, the changesets are sufficiently compared that
765 764 # we don't really care about ordering. However, this leaves
766 765 # some race conditions in the tests, so we compare on the
767 766 # number of files modified, the files contained in each
768 767 # changeset, and the branchpoints in the change to ensure test
769 768 # output remains stable.
770 769
771 770 # recommended replacement for cmp from
772 771 # https://docs.python.org/3.0/whatsnew/3.0.html
773 772 c = lambda x, y: (x > y) - (x < y)
774 773 # Sort bigger changes first.
775 774 if not d:
776 775 d = c(len(l.entries), len(r.entries))
777 776 # Try sorting by filename in the change.
778 777 if not d:
779 778 d = c([e.file for e in l.entries], [e.file for e in r.entries])
780 779 # Try and put changes without a branch point before ones with
781 780 # a branch point.
782 781 if not d:
783 782 d = c(len(l.branchpoints), len(r.branchpoints))
784 783 return d
785 784
786 785 changesets.sort(key=functools.cmp_to_key(cscmp))
787 786
788 787 # Collect tags
789 788
790 789 globaltags = {}
791 790 for c in changesets:
792 791 for e in c.entries:
793 792 for tag in e.tags:
794 793 # remember which is the latest changeset to have this tag
795 794 globaltags[tag] = c
796 795
797 796 for c in changesets:
798 797 tags = set()
799 798 for e in c.entries:
800 799 tags.update(e.tags)
801 800 # remember tags only if this is the latest changeset to have it
802 801 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
803 802
804 803 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
805 804 # by inserting dummy changesets with two parents, and handle
806 805 # {{mergefrombranch BRANCHNAME}} by setting two parents.
807 806
808 807 if mergeto is None:
809 808 mergeto = br'{{mergetobranch ([-\w]+)}}'
810 809 if mergeto:
811 810 mergeto = re.compile(mergeto)
812 811
813 812 if mergefrom is None:
814 813 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
815 814 if mergefrom:
816 815 mergefrom = re.compile(mergefrom)
817 816
818 817 versions = {} # changeset index where we saw any particular file version
819 818 branches = {} # changeset index where we saw a branch
820 819 n = len(changesets)
821 820 i = 0
822 821 while i < n:
823 822 c = changesets[i]
824 823
825 824 for f in c.entries:
826 825 versions[(f.rcs, f.revision)] = i
827 826
828 827 p = None
829 828 if c.branch in branches:
830 829 p = branches[c.branch]
831 830 else:
832 831 # first changeset on a new branch
833 832 # the parent is a changeset with the branch in its
834 833 # branchpoints such that it is the latest possible
835 834 # commit without any intervening, unrelated commits.
836 835
837 836 for candidate in pycompat.xrange(i):
838 837 if c.branch not in changesets[candidate].branchpoints:
839 838 if p is not None:
840 839 break
841 840 continue
842 841 p = candidate
843 842
844 843 c.parents = []
845 844 if p is not None:
846 845 p = changesets[p]
847 846
848 847 # Ensure no changeset has a synthetic changeset as a parent.
849 848 while p.synthetic:
850 849 assert len(p.parents) <= 1, _(
851 850 b'synthetic changeset cannot have multiple parents'
852 851 )
853 852 if p.parents:
854 853 p = p.parents[0]
855 854 else:
856 855 p = None
857 856 break
858 857
859 858 if p is not None:
860 859 c.parents.append(p)
861 860
862 861 if c.mergepoint:
863 862 if c.mergepoint == b'HEAD':
864 863 c.mergepoint = None
865 864 c.parents.append(changesets[branches[c.mergepoint]])
866 865
867 866 if mergefrom:
868 867 m = mergefrom.search(c.comment)
869 868 if m:
870 869 m = m.group(1)
871 870 if m == b'HEAD':
872 871 m = None
873 872 try:
874 873 candidate = changesets[branches[m]]
875 874 except KeyError:
876 875 ui.warn(
877 876 _(
878 877 b"warning: CVS commit message references "
879 878 b"non-existent branch %r:\n%s\n"
880 879 )
881 880 % (pycompat.bytestr(m), c.comment)
882 881 )
883 882 if m in branches and c.branch != m and not candidate.synthetic:
884 883 c.parents.append(candidate)
885 884
886 885 if mergeto:
887 886 m = mergeto.search(c.comment)
888 887 if m:
889 888 if m.groups():
890 889 m = m.group(1)
891 890 if m == b'HEAD':
892 891 m = None
893 892 else:
894 893 m = None # if no group found then merge to HEAD
895 894 if m in branches and c.branch != m:
896 895 # insert empty changeset for merge
897 896 cc = changeset(
898 897 author=c.author,
899 898 branch=m,
900 899 date=c.date,
901 900 comment=b'convert-repo: CVS merge from branch %s'
902 901 % c.branch,
903 902 entries=[],
904 903 tags=[],
905 904 parents=[changesets[branches[m]], c],
906 905 )
907 906 changesets.insert(i + 1, cc)
908 907 branches[m] = i + 1
909 908
910 909 # adjust our loop counters now we have inserted a new entry
911 910 n += 1
912 911 i += 2
913 912 continue
914 913
915 914 branches[c.branch] = i
916 915 i += 1
917 916
918 917 # Drop synthetic changesets (safe now that we have ensured no other
919 918 # changesets can have them as parents).
920 919 i = 0
921 920 while i < len(changesets):
922 921 if changesets[i].synthetic:
923 922 del changesets[i]
924 923 else:
925 924 i += 1
926 925
927 926 # Number changesets
928 927
929 928 for i, c in enumerate(changesets):
930 929 c.id = i + 1
931 930
932 931 if odd:
933 932 for l, r in odd:
934 933 if l.id is not None and r.id is not None:
935 934 ui.warn(
936 935 _(b'changeset %d is both before and after %d\n')
937 936 % (l.id, r.id)
938 937 )
939 938
940 939 ui.status(_(b'%d changeset entries\n') % len(changesets))
941 940
942 941 hook.hook(ui, None, b"cvschangesets", True, changesets=changesets)
943 942
944 943 return changesets
945 944
946 945
947 946 def debugcvsps(ui, *args, **opts):
948 947 """Read CVS rlog for current directory or named path in
949 948 repository, and convert the log to changesets based on matching
950 949 commit log entries and dates.
951 950 """
952 951 opts = pycompat.byteskwargs(opts)
953 952 if opts[b"new_cache"]:
954 953 cache = b"write"
955 954 elif opts[b"update_cache"]:
956 955 cache = b"update"
957 956 else:
958 957 cache = None
959 958
960 959 revisions = opts[b"revisions"]
961 960
962 961 try:
963 962 if args:
964 963 log = []
965 964 for d in args:
966 965 log += createlog(ui, d, root=opts[b"root"], cache=cache)
967 966 else:
968 967 log = createlog(ui, root=opts[b"root"], cache=cache)
969 968 except logerror as e:
970 969 ui.write(b"%r\n" % e)
971 970 return
972 971
973 972 changesets = createchangeset(ui, log, opts[b"fuzz"])
974 973 del log
975 974
976 975 # Print changesets (optionally filtered)
977 976
978 977 off = len(revisions)
979 978 branches = {} # latest version number in each branch
980 979 ancestors = {} # parent branch
981 980 for cs in changesets:
982 981
983 982 if opts[b"ancestors"]:
984 983 if cs.branch not in branches and cs.parents and cs.parents[0].id:
985 984 ancestors[cs.branch] = (
986 985 changesets[cs.parents[0].id - 1].branch,
987 986 cs.parents[0].id,
988 987 )
989 988 branches[cs.branch] = cs.id
990 989
991 990 # limit by branches
992 991 if (
993 992 opts[b"branches"]
994 993 and (cs.branch or b'HEAD') not in opts[b"branches"]
995 994 ):
996 995 continue
997 996
998 997 if not off:
999 998 # Note: trailing spaces on several lines here are needed to have
1000 999 # bug-for-bug compatibility with cvsps.
1001 1000 ui.write(b'---------------------\n')
1002 1001 ui.write((b'PatchSet %d \n' % cs.id))
1003 1002 ui.write(
1004 1003 (
1005 1004 b'Date: %s\n'
1006 1005 % dateutil.datestr(cs.date, b'%Y/%m/%d %H:%M:%S %1%2')
1007 1006 )
1008 1007 )
1009 1008 ui.write((b'Author: %s\n' % cs.author))
1010 1009 ui.write((b'Branch: %s\n' % (cs.branch or b'HEAD')))
1011 1010 ui.write(
1012 1011 (
1013 1012 b'Tag%s: %s \n'
1014 1013 % (
1015 1014 [b'', b's'][len(cs.tags) > 1],
1016 1015 b','.join(cs.tags) or b'(none)',
1017 1016 )
1018 1017 )
1019 1018 )
1020 1019 if cs.branchpoints:
1021 1020 ui.writenoi18n(
1022 1021 b'Branchpoints: %s \n' % b', '.join(sorted(cs.branchpoints))
1023 1022 )
1024 1023 if opts[b"parents"] and cs.parents:
1025 1024 if len(cs.parents) > 1:
1026 1025 ui.write(
1027 1026 (
1028 1027 b'Parents: %s\n'
1029 1028 % (b','.join([(b"%d" % p.id) for p in cs.parents]))
1030 1029 )
1031 1030 )
1032 1031 else:
1033 1032 ui.write((b'Parent: %d\n' % cs.parents[0].id))
1034 1033
1035 1034 if opts[b"ancestors"]:
1036 1035 b = cs.branch
1037 1036 r = []
1038 1037 while b:
1039 1038 b, c = ancestors[b]
1040 1039 r.append(b'%s:%d:%d' % (b or b"HEAD", c, branches[b]))
1041 1040 if r:
1042 1041 ui.write((b'Ancestors: %s\n' % (b','.join(r))))
1043 1042
1044 1043 ui.writenoi18n(b'Log:\n')
1045 1044 ui.write(b'%s\n\n' % cs.comment)
1046 1045 ui.writenoi18n(b'Members: \n')
1047 1046 for f in cs.entries:
1048 1047 fn = f.file
1049 1048 if fn.startswith(opts[b"prefix"]):
1050 1049 fn = fn[len(opts[b"prefix"]) :]
1051 1050 ui.write(
1052 1051 b'\t%s:%s->%s%s \n'
1053 1052 % (
1054 1053 fn,
1055 1054 b'.'.join([b"%d" % x for x in f.parent]) or b'INITIAL',
1056 1055 b'.'.join([(b"%d" % x) for x in f.revision]),
1057 1056 [b'', b'(DEAD)'][f.dead],
1058 1057 )
1059 1058 )
1060 1059 ui.write(b'\n')
1061 1060
1062 1061 # have we seen the start tag?
1063 1062 if revisions and off:
1064 1063 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
1065 1064 off = False
1066 1065
1067 1066 # see if we reached the end tag
1068 1067 if len(revisions) > 1 and not off:
1069 1068 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
1070 1069 break
@@ -1,1741 +1,1741 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 from __future__ import absolute_import
5 5
6 6 import codecs
7 7 import locale
8 8 import os
9 import pickle
9 10 import re
10 11 import xml.dom.minidom
11 12
12 13 from mercurial.i18n import _
13 14 from mercurial.pycompat import open
14 15 from mercurial import (
15 16 encoding,
16 17 error,
17 18 pycompat,
18 19 util,
19 20 vfs as vfsmod,
20 21 )
21 22 from mercurial.utils import (
22 23 dateutil,
23 24 procutil,
24 25 stringutil,
25 26 )
26 27
27 28 from . import common
28 29
29 pickle = util.pickle
30 30 stringio = util.stringio
31 31 propertycache = util.propertycache
32 32 urlerr = util.urlerr
33 33 urlreq = util.urlreq
34 34
35 35 commandline = common.commandline
36 36 commit = common.commit
37 37 converter_sink = common.converter_sink
38 38 converter_source = common.converter_source
39 39 decodeargs = common.decodeargs
40 40 encodeargs = common.encodeargs
41 41 makedatetimestamp = common.makedatetimestamp
42 42 mapfile = common.mapfile
43 43 MissingTool = common.MissingTool
44 44 NoRepo = common.NoRepo
45 45
46 46 # Subversion stuff. Works best with very recent Python SVN bindings
47 47 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
48 48 # these bindings.
49 49
50 50 try:
51 51 import svn
52 52 import svn.client
53 53 import svn.core
54 54 import svn.ra
55 55 import svn.delta
56 56 from . import transport
57 57 import warnings
58 58
59 59 warnings.filterwarnings(
60 60 'ignore', module='svn.core', category=DeprecationWarning
61 61 )
62 62 svn.core.SubversionException # trigger import to catch error
63 63
64 64 except ImportError:
65 65 svn = None
66 66
67 67
68 68 # In Subversion, paths and URLs are Unicode (encoded as UTF-8), which
69 69 # Subversion converts from / to native strings when interfacing with the OS.
70 70 # When passing paths and URLs to Subversion, we have to recode them such that
71 71 # it roundstrips with what Subversion is doing.
72 72
73 73 fsencoding = None
74 74
75 75
76 76 def init_fsencoding():
77 77 global fsencoding, fsencoding_is_utf8
78 78 if fsencoding is not None:
79 79 return
80 80 if pycompat.iswindows:
81 81 # On Windows, filenames are Unicode, but we store them using the MBCS
82 82 # encoding.
83 83 fsencoding = 'mbcs'
84 84 else:
85 85 # This is the encoding used to convert UTF-8 back to natively-encoded
86 86 # strings in Subversion 1.14.0 or earlier with APR 1.7.0 or earlier.
87 87 with util.with_lc_ctype():
88 88 fsencoding = locale.nl_langinfo(locale.CODESET) or 'ISO-8859-1'
89 89 fsencoding = codecs.lookup(fsencoding).name
90 90 fsencoding_is_utf8 = fsencoding == codecs.lookup('utf-8').name
91 91
92 92
93 93 def fs2svn(s):
94 94 if fsencoding_is_utf8:
95 95 return s
96 96 else:
97 97 return s.decode(fsencoding).encode('utf-8')
98 98
99 99
100 100 def formatsvndate(date):
101 101 return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
102 102
103 103
104 104 def parsesvndate(s):
105 105 # Example SVN datetime. Includes microseconds.
106 106 # ISO-8601 conformant
107 107 # '2007-01-04T17:35:00.902377Z'
108 108 return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
109 109
110 110
111 111 class SvnPathNotFound(Exception):
112 112 pass
113 113
114 114
115 115 def revsplit(rev):
116 116 """Parse a revision string and return (uuid, path, revnum).
117 117 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
118 118 ... b'/proj%20B/mytrunk/mytrunk@1')
119 119 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
120 120 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
121 121 ('', '', 1)
122 122 >>> revsplit(b'@7')
123 123 ('', '', 7)
124 124 >>> revsplit(b'7')
125 125 ('', '', 0)
126 126 >>> revsplit(b'bad')
127 127 ('', '', 0)
128 128 """
129 129 parts = rev.rsplit(b'@', 1)
130 130 revnum = 0
131 131 if len(parts) > 1:
132 132 revnum = int(parts[1])
133 133 parts = parts[0].split(b'/', 1)
134 134 uuid = b''
135 135 mod = b''
136 136 if len(parts) > 1 and parts[0].startswith(b'svn:'):
137 137 uuid = parts[0][4:]
138 138 mod = b'/' + parts[1]
139 139 return uuid, mod, revnum
140 140
141 141
142 142 def quote(s):
143 143 # As of svn 1.7, many svn calls expect "canonical" paths. In
144 144 # theory, we should call svn.core.*canonicalize() on all paths
145 145 # before passing them to the API. Instead, we assume the base url
146 146 # is canonical and copy the behaviour of svn URL encoding function
147 147 # so we can extend it safely with new components. The "safe"
148 148 # characters were taken from the "svn_uri__char_validity" table in
149 149 # libsvn_subr/path.c.
150 150 return urlreq.quote(s, b"!$&'()*+,-./:=@_~")
151 151
152 152
153 153 def geturl(path):
154 154 """Convert path or URL to a SVN URL, encoded in UTF-8.
155 155
156 156 This can raise UnicodeDecodeError if the path or URL can't be converted to
157 157 unicode using `fsencoding`.
158 158 """
159 159 try:
160 160 return svn.client.url_from_path(
161 161 svn.core.svn_path_canonicalize(fs2svn(path))
162 162 )
163 163 except svn.core.SubversionException:
164 164 # svn.client.url_from_path() fails with local repositories
165 165 pass
166 166 if os.path.isdir(path):
167 167 path = os.path.normpath(util.abspath(path))
168 168 if pycompat.iswindows:
169 169 path = b'/' + util.normpath(path)
170 170 # Module URL is later compared with the repository URL returned
171 171 # by svn API, which is UTF-8.
172 172 path = fs2svn(path)
173 173 path = b'file://%s' % quote(path)
174 174 return svn.core.svn_path_canonicalize(path)
175 175
176 176
177 177 def optrev(number):
178 178 optrev = svn.core.svn_opt_revision_t()
179 179 optrev.kind = svn.core.svn_opt_revision_number
180 180 optrev.value.number = number
181 181 return optrev
182 182
183 183
184 184 class changedpath(object):
185 185 def __init__(self, p):
186 186 self.copyfrom_path = p.copyfrom_path
187 187 self.copyfrom_rev = p.copyfrom_rev
188 188 self.action = p.action
189 189
190 190
191 191 def get_log_child(
192 192 fp,
193 193 url,
194 194 paths,
195 195 start,
196 196 end,
197 197 limit=0,
198 198 discover_changed_paths=True,
199 199 strict_node_history=False,
200 200 ):
201 201 protocol = -1
202 202
203 203 def receiver(orig_paths, revnum, author, date, message, pool):
204 204 paths = {}
205 205 if orig_paths is not None:
206 206 for k, v in pycompat.iteritems(orig_paths):
207 207 paths[k] = changedpath(v)
208 208 pickle.dump((paths, revnum, author, date, message), fp, protocol)
209 209
210 210 try:
211 211 # Use an ra of our own so that our parent can consume
212 212 # our results without confusing the server.
213 213 t = transport.SvnRaTransport(url=url)
214 214 svn.ra.get_log(
215 215 t.ra,
216 216 paths,
217 217 start,
218 218 end,
219 219 limit,
220 220 discover_changed_paths,
221 221 strict_node_history,
222 222 receiver,
223 223 )
224 224 except IOError:
225 225 # Caller may interrupt the iteration
226 226 pickle.dump(None, fp, protocol)
227 227 except Exception as inst:
228 228 pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
229 229 else:
230 230 pickle.dump(None, fp, protocol)
231 231 fp.flush()
232 232 # With large history, cleanup process goes crazy and suddenly
233 233 # consumes *huge* amount of memory. The output file being closed,
234 234 # there is no need for clean termination.
235 235 os._exit(0)
236 236
237 237
238 238 def debugsvnlog(ui, **opts):
239 239 """Fetch SVN log in a subprocess and channel them back to parent to
240 240 avoid memory collection issues.
241 241 """
242 242 with util.with_lc_ctype():
243 243 if svn is None:
244 244 raise error.Abort(
245 245 _(b'debugsvnlog could not load Subversion python bindings')
246 246 )
247 247
248 248 args = decodeargs(ui.fin.read())
249 249 get_log_child(ui.fout, *args)
250 250
251 251
252 252 class logstream(object):
253 253 """Interruptible revision log iterator."""
254 254
255 255 def __init__(self, stdout):
256 256 self._stdout = stdout
257 257
258 258 def __iter__(self):
259 259 while True:
260 260 try:
261 261 entry = pickle.load(self._stdout)
262 262 except EOFError:
263 263 raise error.Abort(
264 264 _(
265 265 b'Mercurial failed to run itself, check'
266 266 b' hg executable is in PATH'
267 267 )
268 268 )
269 269 try:
270 270 orig_paths, revnum, author, date, message = entry
271 271 except (TypeError, ValueError):
272 272 if entry is None:
273 273 break
274 274 raise error.Abort(_(b"log stream exception '%s'") % entry)
275 275 yield entry
276 276
277 277 def close(self):
278 278 if self._stdout:
279 279 self._stdout.close()
280 280 self._stdout = None
281 281
282 282
283 283 class directlogstream(list):
284 284 """Direct revision log iterator.
285 285 This can be used for debugging and development but it will probably leak
286 286 memory and is not suitable for real conversions."""
287 287
288 288 def __init__(
289 289 self,
290 290 url,
291 291 paths,
292 292 start,
293 293 end,
294 294 limit=0,
295 295 discover_changed_paths=True,
296 296 strict_node_history=False,
297 297 ):
298 298 def receiver(orig_paths, revnum, author, date, message, pool):
299 299 paths = {}
300 300 if orig_paths is not None:
301 301 for k, v in pycompat.iteritems(orig_paths):
302 302 paths[k] = changedpath(v)
303 303 self.append((paths, revnum, author, date, message))
304 304
305 305 # Use an ra of our own so that our parent can consume
306 306 # our results without confusing the server.
307 307 t = transport.SvnRaTransport(url=url)
308 308 svn.ra.get_log(
309 309 t.ra,
310 310 paths,
311 311 start,
312 312 end,
313 313 limit,
314 314 discover_changed_paths,
315 315 strict_node_history,
316 316 receiver,
317 317 )
318 318
319 319 def close(self):
320 320 pass
321 321
322 322
323 323 # Check to see if the given path is a local Subversion repo. Verify this by
324 324 # looking for several svn-specific files and directories in the given
325 325 # directory.
326 326 def filecheck(ui, path, proto):
327 327 for x in (b'locks', b'hooks', b'format', b'db'):
328 328 if not os.path.exists(os.path.join(path, x)):
329 329 return False
330 330 return True
331 331
332 332
333 333 # Check to see if a given path is the root of an svn repo over http. We verify
334 334 # this by requesting a version-controlled URL we know can't exist and looking
335 335 # for the svn-specific "not found" XML.
336 336 def httpcheck(ui, path, proto):
337 337 try:
338 338 opener = urlreq.buildopener()
339 339 rsp = opener.open(
340 340 pycompat.strurl(b'%s://%s/!svn/ver/0/.svn' % (proto, path)), b'rb'
341 341 )
342 342 data = rsp.read()
343 343 except urlerr.httperror as inst:
344 344 if inst.code != 404:
345 345 # Except for 404 we cannot know for sure this is not an svn repo
346 346 ui.warn(
347 347 _(
348 348 b'svn: cannot probe remote repository, assume it could '
349 349 b'be a subversion repository. Use --source-type if you '
350 350 b'know better.\n'
351 351 )
352 352 )
353 353 return True
354 354 data = inst.fp.read()
355 355 except Exception:
356 356 # Could be urlerr.urlerror if the URL is invalid or anything else.
357 357 return False
358 358 return b'<m:human-readable errcode="160013">' in data
359 359
360 360
361 361 protomap = {
362 362 b'http': httpcheck,
363 363 b'https': httpcheck,
364 364 b'file': filecheck,
365 365 }
366 366
367 367
368 368 class NonUtf8PercentEncodedBytes(Exception):
369 369 pass
370 370
371 371
372 372 # Subversion paths are Unicode. Since the percent-decoding is done on
373 373 # UTF-8-encoded strings, percent-encoded bytes are interpreted as UTF-8.
374 374 def url2pathname_like_subversion(unicodepath):
375 375 if pycompat.ispy3:
376 376 # On Python 3, we have to pass unicode to urlreq.url2pathname().
377 377 # Percent-decoded bytes get decoded using UTF-8 and the 'replace' error
378 378 # handler.
379 379 unicodepath = urlreq.url2pathname(unicodepath)
380 380 if u'\N{REPLACEMENT CHARACTER}' in unicodepath:
381 381 raise NonUtf8PercentEncodedBytes
382 382 else:
383 383 return unicodepath
384 384 else:
385 385 # If we passed unicode on Python 2, it would be converted using the
386 386 # latin-1 encoding. Therefore, we pass UTF-8-encoded bytes.
387 387 unicodepath = urlreq.url2pathname(unicodepath.encode('utf-8'))
388 388 try:
389 389 return unicodepath.decode('utf-8')
390 390 except UnicodeDecodeError:
391 391 raise NonUtf8PercentEncodedBytes
392 392
393 393
394 394 def issvnurl(ui, url):
395 395 try:
396 396 proto, path = url.split(b'://', 1)
397 397 if proto == b'file':
398 398 if (
399 399 pycompat.iswindows
400 400 and path[:1] == b'/'
401 401 and path[1:2].isalpha()
402 402 and path[2:6].lower() == b'%3a/'
403 403 ):
404 404 path = path[:2] + b':/' + path[6:]
405 405 try:
406 406 unicodepath = path.decode(fsencoding)
407 407 except UnicodeDecodeError:
408 408 ui.warn(
409 409 _(
410 410 b'Subversion requires that file URLs can be converted '
411 411 b'to Unicode using the current locale encoding (%s)\n'
412 412 )
413 413 % pycompat.sysbytes(fsencoding)
414 414 )
415 415 return False
416 416 try:
417 417 unicodepath = url2pathname_like_subversion(unicodepath)
418 418 except NonUtf8PercentEncodedBytes:
419 419 ui.warn(
420 420 _(
421 421 b'Subversion does not support non-UTF-8 '
422 422 b'percent-encoded bytes in file URLs\n'
423 423 )
424 424 )
425 425 return False
426 426 # Below, we approximate how Subversion checks the path. On Unix, we
427 427 # should therefore convert the path to bytes using `fsencoding`
428 428 # (like Subversion does). On Windows, the right thing would
429 429 # actually be to leave the path as unicode. For now, we restrict
430 430 # the path to MBCS.
431 431 path = unicodepath.encode(fsencoding)
432 432 except ValueError:
433 433 proto = b'file'
434 434 path = util.abspath(url)
435 435 try:
436 436 path.decode(fsencoding)
437 437 except UnicodeDecodeError:
438 438 ui.warn(
439 439 _(
440 440 b'Subversion requires that paths can be converted to '
441 441 b'Unicode using the current locale encoding (%s)\n'
442 442 )
443 443 % pycompat.sysbytes(fsencoding)
444 444 )
445 445 return False
446 446 if proto == b'file':
447 447 path = util.pconvert(path)
448 448 elif proto in (b'http', 'https'):
449 449 if not encoding.isasciistr(path):
450 450 ui.warn(
451 451 _(
452 452 b"Subversion sources don't support non-ASCII characters in "
453 453 b"HTTP(S) URLs. Please percent-encode them.\n"
454 454 )
455 455 )
456 456 return False
457 457 check = protomap.get(proto, lambda *args: False)
458 458 while b'/' in path:
459 459 if check(ui, path, proto):
460 460 return True
461 461 path = path.rsplit(b'/', 1)[0]
462 462 return False
463 463
464 464
465 465 # SVN conversion code stolen from bzr-svn and tailor
466 466 #
467 467 # Subversion looks like a versioned filesystem, branches structures
468 468 # are defined by conventions and not enforced by the tool. First,
469 469 # we define the potential branches (modules) as "trunk" and "branches"
470 470 # children directories. Revisions are then identified by their
471 471 # module and revision number (and a repository identifier).
472 472 #
473 473 # The revision graph is really a tree (or a forest). By default, a
474 474 # revision parent is the previous revision in the same module. If the
475 475 # module directory is copied/moved from another module then the
476 476 # revision is the module root and its parent the source revision in
477 477 # the parent module. A revision has at most one parent.
478 478 #
479 479 class svn_source(converter_source):
480 480 def __init__(self, ui, repotype, url, revs=None):
481 481 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
482 482
483 483 init_fsencoding()
484 484 if not (
485 485 url.startswith(b'svn://')
486 486 or url.startswith(b'svn+ssh://')
487 487 or (
488 488 os.path.exists(url)
489 489 and os.path.exists(os.path.join(url, b'.svn'))
490 490 )
491 491 or issvnurl(ui, url)
492 492 ):
493 493 raise NoRepo(
494 494 _(b"%s does not look like a Subversion repository") % url
495 495 )
496 496 if svn is None:
497 497 raise MissingTool(_(b'could not load Subversion python bindings'))
498 498
499 499 try:
500 500 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
501 501 if version < (1, 4):
502 502 raise MissingTool(
503 503 _(
504 504 b'Subversion python bindings %d.%d found, '
505 505 b'1.4 or later required'
506 506 )
507 507 % version
508 508 )
509 509 except AttributeError:
510 510 raise MissingTool(
511 511 _(
512 512 b'Subversion python bindings are too old, 1.4 '
513 513 b'or later required'
514 514 )
515 515 )
516 516
517 517 self.lastrevs = {}
518 518
519 519 latest = None
520 520 try:
521 521 # Support file://path@rev syntax. Useful e.g. to convert
522 522 # deleted branches.
523 523 at = url.rfind(b'@')
524 524 if at >= 0:
525 525 latest = int(url[at + 1 :])
526 526 url = url[:at]
527 527 except ValueError:
528 528 pass
529 529 self.url = geturl(url)
530 530 self.encoding = b'UTF-8' # Subversion is always nominal UTF-8
531 531 try:
532 532 with util.with_lc_ctype():
533 533 self.transport = transport.SvnRaTransport(url=self.url)
534 534 self.ra = self.transport.ra
535 535 self.ctx = self.transport.client
536 536 self.baseurl = svn.ra.get_repos_root(self.ra)
537 537 # Module is either empty or a repository path starting with
538 538 # a slash and not ending with a slash.
539 539 self.module = urlreq.unquote(self.url[len(self.baseurl) :])
540 540 self.prevmodule = None
541 541 self.rootmodule = self.module
542 542 self.commits = {}
543 543 self.paths = {}
544 544 self.uuid = svn.ra.get_uuid(self.ra)
545 545 except svn.core.SubversionException:
546 546 ui.traceback()
547 547 svnversion = b'%d.%d.%d' % (
548 548 svn.core.SVN_VER_MAJOR,
549 549 svn.core.SVN_VER_MINOR,
550 550 svn.core.SVN_VER_MICRO,
551 551 )
552 552 raise NoRepo(
553 553 _(
554 554 b"%s does not look like a Subversion repository "
555 555 b"to libsvn version %s"
556 556 )
557 557 % (self.url, svnversion)
558 558 )
559 559
560 560 if revs:
561 561 if len(revs) > 1:
562 562 raise error.Abort(
563 563 _(
564 564 b'subversion source does not support '
565 565 b'specifying multiple revisions'
566 566 )
567 567 )
568 568 try:
569 569 latest = int(revs[0])
570 570 except ValueError:
571 571 raise error.Abort(
572 572 _(b'svn: revision %s is not an integer') % revs[0]
573 573 )
574 574
575 575 trunkcfg = self.ui.config(b'convert', b'svn.trunk')
576 576 if trunkcfg is None:
577 577 trunkcfg = b'trunk'
578 578 self.trunkname = trunkcfg.strip(b'/')
579 579 self.startrev = self.ui.config(b'convert', b'svn.startrev')
580 580 try:
581 581 self.startrev = int(self.startrev)
582 582 if self.startrev < 0:
583 583 self.startrev = 0
584 584 except ValueError:
585 585 raise error.Abort(
586 586 _(b'svn: start revision %s is not an integer') % self.startrev
587 587 )
588 588
589 589 try:
590 590 with util.with_lc_ctype():
591 591 self.head = self.latest(self.module, latest)
592 592 except SvnPathNotFound:
593 593 self.head = None
594 594 if not self.head:
595 595 raise error.Abort(
596 596 _(b'no revision found in module %s') % self.module
597 597 )
598 598 self.last_changed = self.revnum(self.head)
599 599
600 600 self._changescache = (None, None)
601 601
602 602 if os.path.exists(os.path.join(url, b'.svn/entries')):
603 603 self.wc = url
604 604 else:
605 605 self.wc = None
606 606 self.convertfp = None
607 607
608 608 def before(self):
609 609 self.with_lc_ctype = util.with_lc_ctype()
610 610 self.with_lc_ctype.__enter__()
611 611
612 612 def after(self):
613 613 self.with_lc_ctype.__exit__(None, None, None)
614 614
615 615 def setrevmap(self, revmap):
616 616 lastrevs = {}
617 617 for revid in revmap:
618 618 uuid, module, revnum = revsplit(revid)
619 619 lastrevnum = lastrevs.setdefault(module, revnum)
620 620 if revnum > lastrevnum:
621 621 lastrevs[module] = revnum
622 622 self.lastrevs = lastrevs
623 623
624 624 def exists(self, path, optrev):
625 625 try:
626 626 svn.client.ls(
627 627 self.url.rstrip(b'/') + b'/' + quote(path),
628 628 optrev,
629 629 False,
630 630 self.ctx,
631 631 )
632 632 return True
633 633 except svn.core.SubversionException:
634 634 return False
635 635
636 636 def getheads(self):
637 637 def isdir(path, revnum):
638 638 kind = self._checkpath(path, revnum)
639 639 return kind == svn.core.svn_node_dir
640 640
641 641 def getcfgpath(name, rev):
642 642 cfgpath = self.ui.config(b'convert', b'svn.' + name)
643 643 if cfgpath is not None and cfgpath.strip() == b'':
644 644 return None
645 645 path = (cfgpath or name).strip(b'/')
646 646 if not self.exists(path, rev):
647 647 if self.module.endswith(path) and name == b'trunk':
648 648 # we are converting from inside this directory
649 649 return None
650 650 if cfgpath:
651 651 raise error.Abort(
652 652 _(b'expected %s to be at %r, but not found')
653 653 % (name, path)
654 654 )
655 655 return None
656 656 self.ui.note(
657 657 _(b'found %s at %r\n') % (name, pycompat.bytestr(path))
658 658 )
659 659 return path
660 660
661 661 rev = optrev(self.last_changed)
662 662 oldmodule = b''
663 663 trunk = getcfgpath(b'trunk', rev)
664 664 self.tags = getcfgpath(b'tags', rev)
665 665 branches = getcfgpath(b'branches', rev)
666 666
667 667 # If the project has a trunk or branches, we will extract heads
668 668 # from them. We keep the project root otherwise.
669 669 if trunk:
670 670 oldmodule = self.module or b''
671 671 self.module += b'/' + trunk
672 672 self.head = self.latest(self.module, self.last_changed)
673 673 if not self.head:
674 674 raise error.Abort(
675 675 _(b'no revision found in module %s') % self.module
676 676 )
677 677
678 678 # First head in the list is the module's head
679 679 self.heads = [self.head]
680 680 if self.tags is not None:
681 681 self.tags = b'%s/%s' % (oldmodule, (self.tags or b'tags'))
682 682
683 683 # Check if branches bring a few more heads to the list
684 684 if branches:
685 685 rpath = self.url.strip(b'/')
686 686 branchnames = svn.client.ls(
687 687 rpath + b'/' + quote(branches), rev, False, self.ctx
688 688 )
689 689 for branch in sorted(branchnames):
690 690 module = b'%s/%s/%s' % (oldmodule, branches, branch)
691 691 if not isdir(module, self.last_changed):
692 692 continue
693 693 brevid = self.latest(module, self.last_changed)
694 694 if not brevid:
695 695 self.ui.note(_(b'ignoring empty branch %s\n') % branch)
696 696 continue
697 697 self.ui.note(
698 698 _(b'found branch %s at %d\n')
699 699 % (branch, self.revnum(brevid))
700 700 )
701 701 self.heads.append(brevid)
702 702
703 703 if self.startrev and self.heads:
704 704 if len(self.heads) > 1:
705 705 raise error.Abort(
706 706 _(
707 707 b'svn: start revision is not supported '
708 708 b'with more than one branch'
709 709 )
710 710 )
711 711 revnum = self.revnum(self.heads[0])
712 712 if revnum < self.startrev:
713 713 raise error.Abort(
714 714 _(b'svn: no revision found after start revision %d')
715 715 % self.startrev
716 716 )
717 717
718 718 return self.heads
719 719
720 720 def _getchanges(self, rev, full):
721 721 (paths, parents) = self.paths[rev]
722 722 copies = {}
723 723 if parents:
724 724 files, self.removed, copies = self.expandpaths(rev, paths, parents)
725 725 if full or not parents:
726 726 # Perform a full checkout on roots
727 727 uuid, module, revnum = revsplit(rev)
728 728 entries = svn.client.ls(
729 729 self.baseurl + quote(module), optrev(revnum), True, self.ctx
730 730 )
731 731 files = [
732 732 n
733 733 for n, e in pycompat.iteritems(entries)
734 734 if e.kind == svn.core.svn_node_file
735 735 ]
736 736 self.removed = set()
737 737
738 738 files.sort()
739 739 files = pycompat.ziplist(files, [rev] * len(files))
740 740 return (files, copies)
741 741
742 742 def getchanges(self, rev, full):
743 743 # reuse cache from getchangedfiles
744 744 if self._changescache[0] == rev and not full:
745 745 (files, copies) = self._changescache[1]
746 746 else:
747 747 (files, copies) = self._getchanges(rev, full)
748 748 # caller caches the result, so free it here to release memory
749 749 del self.paths[rev]
750 750 return (files, copies, set())
751 751
752 752 def getchangedfiles(self, rev, i):
753 753 # called from filemap - cache computed values for reuse in getchanges
754 754 (files, copies) = self._getchanges(rev, False)
755 755 self._changescache = (rev, (files, copies))
756 756 return [f[0] for f in files]
757 757
758 758 def getcommit(self, rev):
759 759 if rev not in self.commits:
760 760 uuid, module, revnum = revsplit(rev)
761 761 self.module = module
762 762 self.reparent(module)
763 763 # We assume that:
764 764 # - requests for revisions after "stop" come from the
765 765 # revision graph backward traversal. Cache all of them
766 766 # down to stop, they will be used eventually.
767 767 # - requests for revisions before "stop" come to get
768 768 # isolated branches parents. Just fetch what is needed.
769 769 stop = self.lastrevs.get(module, 0)
770 770 if revnum < stop:
771 771 stop = revnum + 1
772 772 self._fetch_revisions(revnum, stop)
773 773 if rev not in self.commits:
774 774 raise error.Abort(_(b'svn: revision %s not found') % revnum)
775 775 revcommit = self.commits[rev]
776 776 # caller caches the result, so free it here to release memory
777 777 del self.commits[rev]
778 778 return revcommit
779 779
780 780 def checkrevformat(self, revstr, mapname=b'splicemap'):
781 781 """fails if revision format does not match the correct format"""
782 782 if not re.match(
783 783 br'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
784 784 br'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
785 785 br'{12,12}(.*)@[0-9]+$',
786 786 revstr,
787 787 ):
788 788 raise error.Abort(
789 789 _(b'%s entry %s is not a valid revision identifier')
790 790 % (mapname, revstr)
791 791 )
792 792
793 793 def numcommits(self):
794 794 return int(self.head.rsplit(b'@', 1)[1]) - self.startrev
795 795
796 796 def gettags(self):
797 797 tags = {}
798 798 if self.tags is None:
799 799 return tags
800 800
801 801 # svn tags are just a convention, project branches left in a
802 802 # 'tags' directory. There is no other relationship than
803 803 # ancestry, which is expensive to discover and makes them hard
804 804 # to update incrementally. Worse, past revisions may be
805 805 # referenced by tags far away in the future, requiring a deep
806 806 # history traversal on every calculation. Current code
807 807 # performs a single backward traversal, tracking moves within
808 808 # the tags directory (tag renaming) and recording a new tag
809 809 # everytime a project is copied from outside the tags
810 810 # directory. It also lists deleted tags, this behaviour may
811 811 # change in the future.
812 812 pendings = []
813 813 tagspath = self.tags
814 814 start = svn.ra.get_latest_revnum(self.ra)
815 815 stream = self._getlog([self.tags], start, self.startrev)
816 816 try:
817 817 for entry in stream:
818 818 origpaths, revnum, author, date, message = entry
819 819 if not origpaths:
820 820 origpaths = []
821 821 copies = [
822 822 (e.copyfrom_path, e.copyfrom_rev, p)
823 823 for p, e in pycompat.iteritems(origpaths)
824 824 if e.copyfrom_path
825 825 ]
826 826 # Apply moves/copies from more specific to general
827 827 copies.sort(reverse=True)
828 828
829 829 srctagspath = tagspath
830 830 if copies and copies[-1][2] == tagspath:
831 831 # Track tags directory moves
832 832 srctagspath = copies.pop()[0]
833 833
834 834 for source, sourcerev, dest in copies:
835 835 if not dest.startswith(tagspath + b'/'):
836 836 continue
837 837 for tag in pendings:
838 838 if tag[0].startswith(dest):
839 839 tagpath = source + tag[0][len(dest) :]
840 840 tag[:2] = [tagpath, sourcerev]
841 841 break
842 842 else:
843 843 pendings.append([source, sourcerev, dest])
844 844
845 845 # Filter out tags with children coming from different
846 846 # parts of the repository like:
847 847 # /tags/tag.1 (from /trunk:10)
848 848 # /tags/tag.1/foo (from /branches/foo:12)
849 849 # Here/tags/tag.1 discarded as well as its children.
850 850 # It happens with tools like cvs2svn. Such tags cannot
851 851 # be represented in mercurial.
852 852 addeds = {
853 853 p: e.copyfrom_path
854 854 for p, e in pycompat.iteritems(origpaths)
855 855 if e.action == b'A' and e.copyfrom_path
856 856 }
857 857 badroots = set()
858 858 for destroot in addeds:
859 859 for source, sourcerev, dest in pendings:
860 860 if not dest.startswith(
861 861 destroot + b'/'
862 862 ) or source.startswith(addeds[destroot] + b'/'):
863 863 continue
864 864 badroots.add(destroot)
865 865 break
866 866
867 867 for badroot in badroots:
868 868 pendings = [
869 869 p
870 870 for p in pendings
871 871 if p[2] != badroot
872 872 and not p[2].startswith(badroot + b'/')
873 873 ]
874 874
875 875 # Tell tag renamings from tag creations
876 876 renamings = []
877 877 for source, sourcerev, dest in pendings:
878 878 tagname = dest.split(b'/')[-1]
879 879 if source.startswith(srctagspath):
880 880 renamings.append([source, sourcerev, tagname])
881 881 continue
882 882 if tagname in tags:
883 883 # Keep the latest tag value
884 884 continue
885 885 # From revision may be fake, get one with changes
886 886 try:
887 887 tagid = self.latest(source, sourcerev)
888 888 if tagid and tagname not in tags:
889 889 tags[tagname] = tagid
890 890 except SvnPathNotFound:
891 891 # It happens when we are following directories
892 892 # we assumed were copied with their parents
893 893 # but were really created in the tag
894 894 # directory.
895 895 pass
896 896 pendings = renamings
897 897 tagspath = srctagspath
898 898 finally:
899 899 stream.close()
900 900 return tags
901 901
902 902 def converted(self, rev, destrev):
903 903 if not self.wc:
904 904 return
905 905 if self.convertfp is None:
906 906 self.convertfp = open(
907 907 os.path.join(self.wc, b'.svn', b'hg-shamap'), b'ab'
908 908 )
909 909 self.convertfp.write(
910 910 util.tonativeeol(b'%s %d\n' % (destrev, self.revnum(rev)))
911 911 )
912 912 self.convertfp.flush()
913 913
914 914 def revid(self, revnum, module=None):
915 915 return b'svn:%s%s@%d' % (self.uuid, module or self.module, revnum)
916 916
917 917 def revnum(self, rev):
918 918 return int(rev.split(b'@')[-1])
919 919
920 920 def latest(self, path, stop=None):
921 921 """Find the latest revid affecting path, up to stop revision
922 922 number. If stop is None, default to repository latest
923 923 revision. It may return a revision in a different module,
924 924 since a branch may be moved without a change being
925 925 reported. Return None if computed module does not belong to
926 926 rootmodule subtree.
927 927 """
928 928
929 929 def findchanges(path, start, stop=None):
930 930 stream = self._getlog([path], start, stop or 1)
931 931 try:
932 932 for entry in stream:
933 933 paths, revnum, author, date, message = entry
934 934 if stop is None and paths:
935 935 # We do not know the latest changed revision,
936 936 # keep the first one with changed paths.
937 937 break
938 938 if stop is not None and revnum <= stop:
939 939 break
940 940
941 941 for p in paths:
942 942 if not path.startswith(p) or not paths[p].copyfrom_path:
943 943 continue
944 944 newpath = paths[p].copyfrom_path + path[len(p) :]
945 945 self.ui.debug(
946 946 b"branch renamed from %s to %s at %d\n"
947 947 % (path, newpath, revnum)
948 948 )
949 949 path = newpath
950 950 break
951 951 if not paths:
952 952 revnum = None
953 953 return revnum, path
954 954 finally:
955 955 stream.close()
956 956
957 957 if not path.startswith(self.rootmodule):
958 958 # Requests on foreign branches may be forbidden at server level
959 959 self.ui.debug(b'ignoring foreign branch %r\n' % path)
960 960 return None
961 961
962 962 if stop is None:
963 963 stop = svn.ra.get_latest_revnum(self.ra)
964 964 try:
965 965 prevmodule = self.reparent(b'')
966 966 dirent = svn.ra.stat(self.ra, path.strip(b'/'), stop)
967 967 self.reparent(prevmodule)
968 968 except svn.core.SubversionException:
969 969 dirent = None
970 970 if not dirent:
971 971 raise SvnPathNotFound(
972 972 _(b'%s not found up to revision %d') % (path, stop)
973 973 )
974 974
975 975 # stat() gives us the previous revision on this line of
976 976 # development, but it might be in *another module*. Fetch the
977 977 # log and detect renames down to the latest revision.
978 978 revnum, realpath = findchanges(path, stop, dirent.created_rev)
979 979 if revnum is None:
980 980 # Tools like svnsync can create empty revision, when
981 981 # synchronizing only a subtree for instance. These empty
982 982 # revisions created_rev still have their original values
983 983 # despite all changes having disappeared and can be
984 984 # returned by ra.stat(), at least when stating the root
985 985 # module. In that case, do not trust created_rev and scan
986 986 # the whole history.
987 987 revnum, realpath = findchanges(path, stop)
988 988 if revnum is None:
989 989 self.ui.debug(b'ignoring empty branch %r\n' % realpath)
990 990 return None
991 991
992 992 if not realpath.startswith(self.rootmodule):
993 993 self.ui.debug(b'ignoring foreign branch %r\n' % realpath)
994 994 return None
995 995 return self.revid(revnum, realpath)
996 996
997 997 def reparent(self, module):
998 998 """Reparent the svn transport and return the previous parent."""
999 999 if self.prevmodule == module:
1000 1000 return module
1001 1001 svnurl = self.baseurl + quote(module)
1002 1002 prevmodule = self.prevmodule
1003 1003 if prevmodule is None:
1004 1004 prevmodule = b''
1005 1005 self.ui.debug(b"reparent to %s\n" % svnurl)
1006 1006 svn.ra.reparent(self.ra, svnurl)
1007 1007 self.prevmodule = module
1008 1008 return prevmodule
1009 1009
1010 1010 def expandpaths(self, rev, paths, parents):
1011 1011 changed, removed = set(), set()
1012 1012 copies = {}
1013 1013
1014 1014 new_module, revnum = revsplit(rev)[1:]
1015 1015 if new_module != self.module:
1016 1016 self.module = new_module
1017 1017 self.reparent(self.module)
1018 1018
1019 1019 progress = self.ui.makeprogress(
1020 1020 _(b'scanning paths'), unit=_(b'paths'), total=len(paths)
1021 1021 )
1022 1022 for i, (path, ent) in enumerate(paths):
1023 1023 progress.update(i, item=path)
1024 1024 entrypath = self.getrelpath(path)
1025 1025
1026 1026 kind = self._checkpath(entrypath, revnum)
1027 1027 if kind == svn.core.svn_node_file:
1028 1028 changed.add(self.recode(entrypath))
1029 1029 if not ent.copyfrom_path or not parents:
1030 1030 continue
1031 1031 # Copy sources not in parent revisions cannot be
1032 1032 # represented, ignore their origin for now
1033 1033 pmodule, prevnum = revsplit(parents[0])[1:]
1034 1034 if ent.copyfrom_rev < prevnum:
1035 1035 continue
1036 1036 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
1037 1037 if not copyfrom_path:
1038 1038 continue
1039 1039 self.ui.debug(
1040 1040 b"copied to %s from %s@%d\n"
1041 1041 % (entrypath, copyfrom_path, ent.copyfrom_rev)
1042 1042 )
1043 1043 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
1044 1044 elif kind == 0: # gone, but had better be a deleted *file*
1045 1045 self.ui.debug(b"gone from %d\n" % ent.copyfrom_rev)
1046 1046 pmodule, prevnum = revsplit(parents[0])[1:]
1047 1047 parentpath = pmodule + b"/" + entrypath
1048 1048 fromkind = self._checkpath(entrypath, prevnum, pmodule)
1049 1049
1050 1050 if fromkind == svn.core.svn_node_file:
1051 1051 removed.add(self.recode(entrypath))
1052 1052 elif fromkind == svn.core.svn_node_dir:
1053 1053 oroot = parentpath.strip(b'/')
1054 1054 nroot = path.strip(b'/')
1055 1055 children = self._iterfiles(oroot, prevnum)
1056 1056 for childpath in children:
1057 1057 childpath = childpath.replace(oroot, nroot)
1058 1058 childpath = self.getrelpath(b"/" + childpath, pmodule)
1059 1059 if childpath:
1060 1060 removed.add(self.recode(childpath))
1061 1061 else:
1062 1062 self.ui.debug(
1063 1063 b'unknown path in revision %d: %s\n' % (revnum, path)
1064 1064 )
1065 1065 elif kind == svn.core.svn_node_dir:
1066 1066 if ent.action == b'M':
1067 1067 # If the directory just had a prop change,
1068 1068 # then we shouldn't need to look for its children.
1069 1069 continue
1070 1070 if ent.action == b'R' and parents:
1071 1071 # If a directory is replacing a file, mark the previous
1072 1072 # file as deleted
1073 1073 pmodule, prevnum = revsplit(parents[0])[1:]
1074 1074 pkind = self._checkpath(entrypath, prevnum, pmodule)
1075 1075 if pkind == svn.core.svn_node_file:
1076 1076 removed.add(self.recode(entrypath))
1077 1077 elif pkind == svn.core.svn_node_dir:
1078 1078 # We do not know what files were kept or removed,
1079 1079 # mark them all as changed.
1080 1080 for childpath in self._iterfiles(pmodule, prevnum):
1081 1081 childpath = self.getrelpath(b"/" + childpath)
1082 1082 if childpath:
1083 1083 changed.add(self.recode(childpath))
1084 1084
1085 1085 for childpath in self._iterfiles(path, revnum):
1086 1086 childpath = self.getrelpath(b"/" + childpath)
1087 1087 if childpath:
1088 1088 changed.add(self.recode(childpath))
1089 1089
1090 1090 # Handle directory copies
1091 1091 if not ent.copyfrom_path or not parents:
1092 1092 continue
1093 1093 # Copy sources not in parent revisions cannot be
1094 1094 # represented, ignore their origin for now
1095 1095 pmodule, prevnum = revsplit(parents[0])[1:]
1096 1096 if ent.copyfrom_rev < prevnum:
1097 1097 continue
1098 1098 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
1099 1099 if not copyfrompath:
1100 1100 continue
1101 1101 self.ui.debug(
1102 1102 b"mark %s came from %s:%d\n"
1103 1103 % (path, copyfrompath, ent.copyfrom_rev)
1104 1104 )
1105 1105 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
1106 1106 for childpath in children:
1107 1107 childpath = self.getrelpath(b"/" + childpath, pmodule)
1108 1108 if not childpath:
1109 1109 continue
1110 1110 copytopath = path + childpath[len(copyfrompath) :]
1111 1111 copytopath = self.getrelpath(copytopath)
1112 1112 copies[self.recode(copytopath)] = self.recode(childpath)
1113 1113
1114 1114 progress.complete()
1115 1115 changed.update(removed)
1116 1116 return (list(changed), removed, copies)
1117 1117
1118 1118 def _fetch_revisions(self, from_revnum, to_revnum):
1119 1119 if from_revnum < to_revnum:
1120 1120 from_revnum, to_revnum = to_revnum, from_revnum
1121 1121
1122 1122 self.child_cset = None
1123 1123
1124 1124 def parselogentry(orig_paths, revnum, author, date, message):
1125 1125 """Return the parsed commit object or None, and True if
1126 1126 the revision is a branch root.
1127 1127 """
1128 1128 self.ui.debug(
1129 1129 b"parsing revision %d (%d changes)\n"
1130 1130 % (revnum, len(orig_paths))
1131 1131 )
1132 1132
1133 1133 branched = False
1134 1134 rev = self.revid(revnum)
1135 1135 # branch log might return entries for a parent we already have
1136 1136
1137 1137 if rev in self.commits or revnum < to_revnum:
1138 1138 return None, branched
1139 1139
1140 1140 parents = []
1141 1141 # check whether this revision is the start of a branch or part
1142 1142 # of a branch renaming
1143 1143 orig_paths = sorted(pycompat.iteritems(orig_paths))
1144 1144 root_paths = [
1145 1145 (p, e) for p, e in orig_paths if self.module.startswith(p)
1146 1146 ]
1147 1147 if root_paths:
1148 1148 path, ent = root_paths[-1]
1149 1149 if ent.copyfrom_path:
1150 1150 branched = True
1151 1151 newpath = ent.copyfrom_path + self.module[len(path) :]
1152 1152 # ent.copyfrom_rev may not be the actual last revision
1153 1153 previd = self.latest(newpath, ent.copyfrom_rev)
1154 1154 if previd is not None:
1155 1155 prevmodule, prevnum = revsplit(previd)[1:]
1156 1156 if prevnum >= self.startrev:
1157 1157 parents = [previd]
1158 1158 self.ui.note(
1159 1159 _(b'found parent of branch %s at %d: %s\n')
1160 1160 % (self.module, prevnum, prevmodule)
1161 1161 )
1162 1162 else:
1163 1163 self.ui.debug(b"no copyfrom path, don't know what to do.\n")
1164 1164
1165 1165 paths = []
1166 1166 # filter out unrelated paths
1167 1167 for path, ent in orig_paths:
1168 1168 if self.getrelpath(path) is None:
1169 1169 continue
1170 1170 paths.append((path, ent))
1171 1171
1172 1172 date = parsesvndate(date)
1173 1173 if self.ui.configbool(b'convert', b'localtimezone'):
1174 1174 date = makedatetimestamp(date[0])
1175 1175
1176 1176 if message:
1177 1177 log = self.recode(message)
1178 1178 else:
1179 1179 log = b''
1180 1180
1181 1181 if author:
1182 1182 author = self.recode(author)
1183 1183 else:
1184 1184 author = b''
1185 1185
1186 1186 try:
1187 1187 branch = self.module.split(b"/")[-1]
1188 1188 if branch == self.trunkname:
1189 1189 branch = None
1190 1190 except IndexError:
1191 1191 branch = None
1192 1192
1193 1193 cset = commit(
1194 1194 author=author,
1195 1195 date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
1196 1196 desc=log,
1197 1197 parents=parents,
1198 1198 branch=branch,
1199 1199 rev=rev,
1200 1200 )
1201 1201
1202 1202 self.commits[rev] = cset
1203 1203 # The parents list is *shared* among self.paths and the
1204 1204 # commit object. Both will be updated below.
1205 1205 self.paths[rev] = (paths, cset.parents)
1206 1206 if self.child_cset and not self.child_cset.parents:
1207 1207 self.child_cset.parents[:] = [rev]
1208 1208 self.child_cset = cset
1209 1209 return cset, branched
1210 1210
1211 1211 self.ui.note(
1212 1212 _(b'fetching revision log for "%s" from %d to %d\n')
1213 1213 % (self.module, from_revnum, to_revnum)
1214 1214 )
1215 1215
1216 1216 try:
1217 1217 firstcset = None
1218 1218 lastonbranch = False
1219 1219 stream = self._getlog([self.module], from_revnum, to_revnum)
1220 1220 try:
1221 1221 for entry in stream:
1222 1222 paths, revnum, author, date, message = entry
1223 1223 if revnum < self.startrev:
1224 1224 lastonbranch = True
1225 1225 break
1226 1226 if not paths:
1227 1227 self.ui.debug(b'revision %d has no entries\n' % revnum)
1228 1228 # If we ever leave the loop on an empty
1229 1229 # revision, do not try to get a parent branch
1230 1230 lastonbranch = lastonbranch or revnum == 0
1231 1231 continue
1232 1232 cset, lastonbranch = parselogentry(
1233 1233 paths, revnum, author, date, message
1234 1234 )
1235 1235 if cset:
1236 1236 firstcset = cset
1237 1237 if lastonbranch:
1238 1238 break
1239 1239 finally:
1240 1240 stream.close()
1241 1241
1242 1242 if not lastonbranch and firstcset and not firstcset.parents:
1243 1243 # The first revision of the sequence (the last fetched one)
1244 1244 # has invalid parents if not a branch root. Find the parent
1245 1245 # revision now, if any.
1246 1246 try:
1247 1247 firstrevnum = self.revnum(firstcset.rev)
1248 1248 if firstrevnum > 1:
1249 1249 latest = self.latest(self.module, firstrevnum - 1)
1250 1250 if latest:
1251 1251 firstcset.parents.append(latest)
1252 1252 except SvnPathNotFound:
1253 1253 pass
1254 1254 except svn.core.SubversionException as xxx_todo_changeme:
1255 1255 (inst, num) = xxx_todo_changeme.args
1256 1256 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
1257 1257 raise error.Abort(
1258 1258 _(b'svn: branch has no revision %s') % to_revnum
1259 1259 )
1260 1260 raise
1261 1261
1262 1262 def getfile(self, file, rev):
1263 1263 # TODO: ra.get_file transmits the whole file instead of diffs.
1264 1264 if file in self.removed:
1265 1265 return None, None
1266 1266 try:
1267 1267 new_module, revnum = revsplit(rev)[1:]
1268 1268 if self.module != new_module:
1269 1269 self.module = new_module
1270 1270 self.reparent(self.module)
1271 1271 io = stringio()
1272 1272 info = svn.ra.get_file(self.ra, file, revnum, io)
1273 1273 data = io.getvalue()
1274 1274 # ra.get_file() seems to keep a reference on the input buffer
1275 1275 # preventing collection. Release it explicitly.
1276 1276 io.close()
1277 1277 if isinstance(info, list):
1278 1278 info = info[-1]
1279 1279 mode = (b"svn:executable" in info) and b'x' or b''
1280 1280 mode = (b"svn:special" in info) and b'l' or mode
1281 1281 except svn.core.SubversionException as e:
1282 1282 notfound = (
1283 1283 svn.core.SVN_ERR_FS_NOT_FOUND,
1284 1284 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
1285 1285 )
1286 1286 if e.apr_err in notfound: # File not found
1287 1287 return None, None
1288 1288 raise
1289 1289 if mode == b'l':
1290 1290 link_prefix = b"link "
1291 1291 if data.startswith(link_prefix):
1292 1292 data = data[len(link_prefix) :]
1293 1293 return data, mode
1294 1294
1295 1295 def _iterfiles(self, path, revnum):
1296 1296 """Enumerate all files in path at revnum, recursively."""
1297 1297 path = path.strip(b'/')
1298 1298 pool = svn.core.Pool()
1299 1299 rpath = b'/'.join([self.baseurl, quote(path)]).strip(b'/')
1300 1300 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1301 1301 if path:
1302 1302 path += b'/'
1303 1303 return (
1304 1304 (path + p)
1305 1305 for p, e in pycompat.iteritems(entries)
1306 1306 if e.kind == svn.core.svn_node_file
1307 1307 )
1308 1308
1309 1309 def getrelpath(self, path, module=None):
1310 1310 if module is None:
1311 1311 module = self.module
1312 1312 # Given the repository url of this wc, say
1313 1313 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1314 1314 # extract the "entry" portion (a relative path) from what
1315 1315 # svn log --xml says, i.e.
1316 1316 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1317 1317 # that is to say "tests/PloneTestCase.py"
1318 1318 if path.startswith(module):
1319 1319 relative = path.rstrip(b'/')[len(module) :]
1320 1320 if relative.startswith(b'/'):
1321 1321 return relative[1:]
1322 1322 elif relative == b'':
1323 1323 return relative
1324 1324
1325 1325 # The path is outside our tracked tree...
1326 1326 self.ui.debug(
1327 1327 b'%r is not under %r, ignoring\n'
1328 1328 % (pycompat.bytestr(path), pycompat.bytestr(module))
1329 1329 )
1330 1330 return None
1331 1331
1332 1332 def _checkpath(self, path, revnum, module=None):
1333 1333 if module is not None:
1334 1334 prevmodule = self.reparent(b'')
1335 1335 path = module + b'/' + path
1336 1336 try:
1337 1337 # ra.check_path does not like leading slashes very much, it leads
1338 1338 # to PROPFIND subversion errors
1339 1339 return svn.ra.check_path(self.ra, path.strip(b'/'), revnum)
1340 1340 finally:
1341 1341 if module is not None:
1342 1342 self.reparent(prevmodule)
1343 1343
1344 1344 def _getlog(
1345 1345 self,
1346 1346 paths,
1347 1347 start,
1348 1348 end,
1349 1349 limit=0,
1350 1350 discover_changed_paths=True,
1351 1351 strict_node_history=False,
1352 1352 ):
1353 1353 # Normalize path names, svn >= 1.5 only wants paths relative to
1354 1354 # supplied URL
1355 1355 relpaths = []
1356 1356 for p in paths:
1357 1357 if not p.startswith(b'/'):
1358 1358 p = self.module + b'/' + p
1359 1359 relpaths.append(p.strip(b'/'))
1360 1360 args = [
1361 1361 self.baseurl,
1362 1362 relpaths,
1363 1363 start,
1364 1364 end,
1365 1365 limit,
1366 1366 discover_changed_paths,
1367 1367 strict_node_history,
1368 1368 ]
1369 1369 # developer config: convert.svn.debugsvnlog
1370 1370 if not self.ui.configbool(b'convert', b'svn.debugsvnlog'):
1371 1371 return directlogstream(*args)
1372 1372 arg = encodeargs(args)
1373 1373 hgexe = procutil.hgexecutable()
1374 1374 cmd = b'%s debugsvnlog' % procutil.shellquote(hgexe)
1375 1375 stdin, stdout = procutil.popen2(cmd)
1376 1376 stdin.write(arg)
1377 1377 try:
1378 1378 stdin.close()
1379 1379 except IOError:
1380 1380 raise error.Abort(
1381 1381 _(
1382 1382 b'Mercurial failed to run itself, check'
1383 1383 b' hg executable is in PATH'
1384 1384 )
1385 1385 )
1386 1386 return logstream(stdout)
1387 1387
1388 1388
1389 1389 pre_revprop_change_template = b'''#!/bin/sh
1390 1390
1391 1391 REPOS="$1"
1392 1392 REV="$2"
1393 1393 USER="$3"
1394 1394 PROPNAME="$4"
1395 1395 ACTION="$5"
1396 1396
1397 1397 %(rules)s
1398 1398
1399 1399 echo "Changing prohibited revision property" >&2
1400 1400 exit 1
1401 1401 '''
1402 1402
1403 1403
1404 1404 def gen_pre_revprop_change_hook(prop_actions_allowed):
1405 1405 rules = []
1406 1406 for action, propname in prop_actions_allowed:
1407 1407 rules.append(
1408 1408 (
1409 1409 b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
1410 1410 b'then exit 0; fi'
1411 1411 )
1412 1412 % (action, propname)
1413 1413 )
1414 1414 return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
1415 1415
1416 1416
1417 1417 class svn_sink(converter_sink, commandline):
1418 1418 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1419 1419 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1420 1420
1421 1421 def prerun(self):
1422 1422 if self.wc:
1423 1423 os.chdir(self.wc)
1424 1424
1425 1425 def postrun(self):
1426 1426 if self.wc:
1427 1427 os.chdir(self.cwd)
1428 1428
1429 1429 def join(self, name):
1430 1430 return os.path.join(self.wc, b'.svn', name)
1431 1431
1432 1432 def revmapfile(self):
1433 1433 return self.join(b'hg-shamap')
1434 1434
1435 1435 def authorfile(self):
1436 1436 return self.join(b'hg-authormap')
1437 1437
1438 1438 def __init__(self, ui, repotype, path):
1439 1439
1440 1440 converter_sink.__init__(self, ui, repotype, path)
1441 1441 commandline.__init__(self, ui, b'svn')
1442 1442 self.delete = []
1443 1443 self.setexec = []
1444 1444 self.delexec = []
1445 1445 self.copies = []
1446 1446 self.wc = None
1447 1447 self.cwd = encoding.getcwd()
1448 1448
1449 1449 created = False
1450 1450 if os.path.isfile(os.path.join(path, b'.svn', b'entries')):
1451 1451 self.wc = os.path.realpath(path)
1452 1452 self.run0(b'update')
1453 1453 else:
1454 1454 if not re.search(br'^(file|http|https|svn|svn\+ssh)://', path):
1455 1455 path = os.path.realpath(path)
1456 1456 if os.path.isdir(os.path.dirname(path)):
1457 1457 if not os.path.exists(
1458 1458 os.path.join(path, b'db', b'fs-type')
1459 1459 ):
1460 1460 ui.status(
1461 1461 _(b"initializing svn repository '%s'\n")
1462 1462 % os.path.basename(path)
1463 1463 )
1464 1464 commandline(ui, b'svnadmin').run0(b'create', path)
1465 1465 created = path
1466 1466 path = util.normpath(path)
1467 1467 if not path.startswith(b'/'):
1468 1468 path = b'/' + path
1469 1469 path = b'file://' + path
1470 1470
1471 1471 wcpath = os.path.join(
1472 1472 encoding.getcwd(), os.path.basename(path) + b'-wc'
1473 1473 )
1474 1474 ui.status(
1475 1475 _(b"initializing svn working copy '%s'\n")
1476 1476 % os.path.basename(wcpath)
1477 1477 )
1478 1478 self.run0(b'checkout', path, wcpath)
1479 1479
1480 1480 self.wc = wcpath
1481 1481 self.opener = vfsmod.vfs(self.wc)
1482 1482 self.wopener = vfsmod.vfs(self.wc)
1483 1483 self.childmap = mapfile(ui, self.join(b'hg-childmap'))
1484 1484 if util.checkexec(self.wc):
1485 1485 self.is_exec = util.isexec
1486 1486 else:
1487 1487 self.is_exec = None
1488 1488
1489 1489 if created:
1490 1490 prop_actions_allowed = [
1491 1491 (b'M', b'svn:log'),
1492 1492 (b'A', b'hg:convert-branch'),
1493 1493 (b'A', b'hg:convert-rev'),
1494 1494 ]
1495 1495
1496 1496 if self.ui.configbool(
1497 1497 b'convert', b'svn.dangerous-set-commit-dates'
1498 1498 ):
1499 1499 prop_actions_allowed.append((b'M', b'svn:date'))
1500 1500
1501 1501 hook = os.path.join(created, b'hooks', b'pre-revprop-change')
1502 1502 fp = open(hook, b'wb')
1503 1503 fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
1504 1504 fp.close()
1505 1505 util.setflags(hook, False, True)
1506 1506
1507 1507 output = self.run0(b'info')
1508 1508 self.uuid = self.uuid_re.search(output).group(1).strip()
1509 1509
1510 1510 def wjoin(self, *names):
1511 1511 return os.path.join(self.wc, *names)
1512 1512
1513 1513 @propertycache
1514 1514 def manifest(self):
1515 1515 # As of svn 1.7, the "add" command fails when receiving
1516 1516 # already tracked entries, so we have to track and filter them
1517 1517 # ourselves.
1518 1518 m = set()
1519 1519 output = self.run0(b'ls', recursive=True, xml=True)
1520 1520 doc = xml.dom.minidom.parseString(output)
1521 1521 for e in doc.getElementsByTagName('entry'):
1522 1522 for n in e.childNodes:
1523 1523 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1524 1524 continue
1525 1525 name = ''.join(
1526 1526 c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
1527 1527 )
1528 1528 # Entries are compared with names coming from
1529 1529 # mercurial, so bytes with undefined encoding. Our
1530 1530 # best bet is to assume they are in local
1531 1531 # encoding. They will be passed to command line calls
1532 1532 # later anyway, so they better be.
1533 1533 m.add(encoding.unitolocal(name))
1534 1534 break
1535 1535 return m
1536 1536
1537 1537 def putfile(self, filename, flags, data):
1538 1538 if b'l' in flags:
1539 1539 self.wopener.symlink(data, filename)
1540 1540 else:
1541 1541 try:
1542 1542 if os.path.islink(self.wjoin(filename)):
1543 1543 os.unlink(filename)
1544 1544 except OSError:
1545 1545 pass
1546 1546
1547 1547 if self.is_exec:
1548 1548 # We need to check executability of the file before the change,
1549 1549 # because `vfs.write` is able to reset exec bit.
1550 1550 wasexec = False
1551 1551 if os.path.exists(self.wjoin(filename)):
1552 1552 wasexec = self.is_exec(self.wjoin(filename))
1553 1553
1554 1554 self.wopener.write(filename, data)
1555 1555
1556 1556 if self.is_exec:
1557 1557 if wasexec:
1558 1558 if b'x' not in flags:
1559 1559 self.delexec.append(filename)
1560 1560 else:
1561 1561 if b'x' in flags:
1562 1562 self.setexec.append(filename)
1563 1563 util.setflags(self.wjoin(filename), False, b'x' in flags)
1564 1564
1565 1565 def _copyfile(self, source, dest):
1566 1566 # SVN's copy command pukes if the destination file exists, but
1567 1567 # our copyfile method expects to record a copy that has
1568 1568 # already occurred. Cross the semantic gap.
1569 1569 wdest = self.wjoin(dest)
1570 1570 exists = os.path.lexists(wdest)
1571 1571 if exists:
1572 1572 fd, tempname = pycompat.mkstemp(
1573 1573 prefix=b'hg-copy-', dir=os.path.dirname(wdest)
1574 1574 )
1575 1575 os.close(fd)
1576 1576 os.unlink(tempname)
1577 1577 os.rename(wdest, tempname)
1578 1578 try:
1579 1579 self.run0(b'copy', source, dest)
1580 1580 finally:
1581 1581 self.manifest.add(dest)
1582 1582 if exists:
1583 1583 try:
1584 1584 os.unlink(wdest)
1585 1585 except OSError:
1586 1586 pass
1587 1587 os.rename(tempname, wdest)
1588 1588
1589 1589 def dirs_of(self, files):
1590 1590 dirs = set()
1591 1591 for f in files:
1592 1592 if os.path.isdir(self.wjoin(f)):
1593 1593 dirs.add(f)
1594 1594 i = len(f)
1595 1595 for i in iter(lambda: f.rfind(b'/', 0, i), -1):
1596 1596 dirs.add(f[:i])
1597 1597 return dirs
1598 1598
1599 1599 def add_dirs(self, files):
1600 1600 add_dirs = [
1601 1601 d for d in sorted(self.dirs_of(files)) if d not in self.manifest
1602 1602 ]
1603 1603 if add_dirs:
1604 1604 self.manifest.update(add_dirs)
1605 1605 self.xargs(add_dirs, b'add', non_recursive=True, quiet=True)
1606 1606 return add_dirs
1607 1607
1608 1608 def add_files(self, files):
1609 1609 files = [f for f in files if f not in self.manifest]
1610 1610 if files:
1611 1611 self.manifest.update(files)
1612 1612 self.xargs(files, b'add', quiet=True)
1613 1613 return files
1614 1614
1615 1615 def addchild(self, parent, child):
1616 1616 self.childmap[parent] = child
1617 1617
1618 1618 def revid(self, rev):
1619 1619 return b"svn:%s@%s" % (self.uuid, rev)
1620 1620
1621 1621 def putcommit(
1622 1622 self, files, copies, parents, commit, source, revmap, full, cleanp2
1623 1623 ):
1624 1624 for parent in parents:
1625 1625 try:
1626 1626 return self.revid(self.childmap[parent])
1627 1627 except KeyError:
1628 1628 pass
1629 1629
1630 1630 # Apply changes to working copy
1631 1631 for f, v in files:
1632 1632 data, mode = source.getfile(f, v)
1633 1633 if data is None:
1634 1634 self.delete.append(f)
1635 1635 else:
1636 1636 self.putfile(f, mode, data)
1637 1637 if f in copies:
1638 1638 self.copies.append([copies[f], f])
1639 1639 if full:
1640 1640 self.delete.extend(sorted(self.manifest.difference(files)))
1641 1641 files = [f[0] for f in files]
1642 1642
1643 1643 entries = set(self.delete)
1644 1644 files = frozenset(files)
1645 1645 entries.update(self.add_dirs(files.difference(entries)))
1646 1646 if self.copies:
1647 1647 for s, d in self.copies:
1648 1648 self._copyfile(s, d)
1649 1649 self.copies = []
1650 1650 if self.delete:
1651 1651 self.xargs(self.delete, b'delete')
1652 1652 for f in self.delete:
1653 1653 self.manifest.remove(f)
1654 1654 self.delete = []
1655 1655 entries.update(self.add_files(files.difference(entries)))
1656 1656 if self.delexec:
1657 1657 self.xargs(self.delexec, b'propdel', b'svn:executable')
1658 1658 self.delexec = []
1659 1659 if self.setexec:
1660 1660 self.xargs(self.setexec, b'propset', b'svn:executable', b'*')
1661 1661 self.setexec = []
1662 1662
1663 1663 fd, messagefile = pycompat.mkstemp(prefix=b'hg-convert-')
1664 1664 fp = os.fdopen(fd, 'wb')
1665 1665 fp.write(util.tonativeeol(commit.desc))
1666 1666 fp.close()
1667 1667 try:
1668 1668 output = self.run0(
1669 1669 b'commit',
1670 1670 username=stringutil.shortuser(commit.author),
1671 1671 file=messagefile,
1672 1672 encoding=b'utf-8',
1673 1673 )
1674 1674 try:
1675 1675 rev = self.commit_re.search(output).group(1)
1676 1676 except AttributeError:
1677 1677 if not files:
1678 1678 return parents[0] if parents else b'None'
1679 1679 self.ui.warn(_(b'unexpected svn output:\n'))
1680 1680 self.ui.warn(output)
1681 1681 raise error.Abort(_(b'unable to cope with svn output'))
1682 1682 if commit.rev:
1683 1683 self.run(
1684 1684 b'propset',
1685 1685 b'hg:convert-rev',
1686 1686 commit.rev,
1687 1687 revprop=True,
1688 1688 revision=rev,
1689 1689 )
1690 1690 if commit.branch and commit.branch != b'default':
1691 1691 self.run(
1692 1692 b'propset',
1693 1693 b'hg:convert-branch',
1694 1694 commit.branch,
1695 1695 revprop=True,
1696 1696 revision=rev,
1697 1697 )
1698 1698
1699 1699 if self.ui.configbool(
1700 1700 b'convert', b'svn.dangerous-set-commit-dates'
1701 1701 ):
1702 1702 # Subverson always uses UTC to represent date and time
1703 1703 date = dateutil.parsedate(commit.date)
1704 1704 date = (date[0], 0)
1705 1705
1706 1706 # The only way to set date and time for svn commit is to use propset after commit is done
1707 1707 self.run(
1708 1708 b'propset',
1709 1709 b'svn:date',
1710 1710 formatsvndate(date),
1711 1711 revprop=True,
1712 1712 revision=rev,
1713 1713 )
1714 1714
1715 1715 for parent in parents:
1716 1716 self.addchild(parent, rev)
1717 1717 return self.revid(rev)
1718 1718 finally:
1719 1719 os.unlink(messagefile)
1720 1720
1721 1721 def puttags(self, tags):
1722 1722 self.ui.warn(_(b'writing Subversion tags is not yet implemented\n'))
1723 1723 return None, None
1724 1724
1725 1725 def hascommitfrommap(self, rev):
1726 1726 # We trust that revisions referenced in a map still is present
1727 1727 # TODO: implement something better if necessary and feasible
1728 1728 return True
1729 1729
1730 1730 def hascommitforsplicemap(self, rev):
1731 1731 # This is not correct as one can convert to an existing subversion
1732 1732 # repository and childmap would not list all revisions. Too bad.
1733 1733 if rev in self.childmap:
1734 1734 return True
1735 1735 raise error.Abort(
1736 1736 _(
1737 1737 b'splice map revision %s not found in subversion '
1738 1738 b'child map (revision lookups are not implemented)'
1739 1739 )
1740 1740 % rev
1741 1741 )
@@ -1,2690 +1,2690 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but allow edits before making new commit
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but allow edits before making new commit
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 The summary of a change can be customized as well::
160 160
161 161 [histedit]
162 162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163 163
164 164 The customized summary should be kept short enough that rule lines
165 165 will fit in the configured line length. See above if that requires
166 166 customization.
167 167
168 168 ``hg histedit`` attempts to automatically choose an appropriate base
169 169 revision to use. To change which base revision is used, define a
170 170 revset in your configuration file::
171 171
172 172 [histedit]
173 173 defaultrev = only(.) & draft()
174 174
175 175 By default each edited revision needs to be present in histedit commands.
176 176 To remove revision you need to use ``drop`` operation. You can configure
177 177 the drop to be implicit for missing commits by adding::
178 178
179 179 [histedit]
180 180 dropmissing = True
181 181
182 182 By default, histedit will close the transaction after each action. For
183 183 performance purposes, you can configure histedit to use a single transaction
184 184 across the entire histedit. WARNING: This setting introduces a significant risk
185 185 of losing the work you've done in a histedit if the histedit aborts
186 186 unexpectedly::
187 187
188 188 [histedit]
189 189 singletransaction = True
190 190
191 191 """
192 192
193 193 from __future__ import absolute_import
194 194
195 195 # chistedit dependencies that are not available everywhere
196 196 try:
197 197 import fcntl
198 198 import termios
199 199 except ImportError:
200 200 fcntl = None
201 201 termios = None
202 202
203 203 import functools
204 204 import os
205 import pickle
205 206 import struct
206 207
207 208 from mercurial.i18n import _
208 209 from mercurial.pycompat import (
209 210 getattr,
210 211 open,
211 212 )
212 213 from mercurial.node import (
213 214 bin,
214 215 hex,
215 216 short,
216 217 )
217 218 from mercurial import (
218 219 bundle2,
219 220 cmdutil,
220 221 context,
221 222 copies,
222 223 destutil,
223 224 discovery,
224 225 encoding,
225 226 error,
226 227 exchange,
227 228 extensions,
228 229 hg,
229 230 logcmdutil,
230 231 merge as mergemod,
231 232 mergestate as mergestatemod,
232 233 mergeutil,
233 234 obsolete,
234 235 pycompat,
235 236 registrar,
236 237 repair,
237 238 rewriteutil,
238 239 scmutil,
239 240 state as statemod,
240 241 util,
241 242 )
242 243 from mercurial.utils import (
243 244 dateutil,
244 245 stringutil,
245 246 urlutil,
246 247 )
247 248
248 pickle = util.pickle
249 249 cmdtable = {}
250 250 command = registrar.command(cmdtable)
251 251
252 252 configtable = {}
253 253 configitem = registrar.configitem(configtable)
254 254 configitem(
255 255 b'experimental',
256 256 b'histedit.autoverb',
257 257 default=False,
258 258 )
259 259 configitem(
260 260 b'histedit',
261 261 b'defaultrev',
262 262 default=None,
263 263 )
264 264 configitem(
265 265 b'histedit',
266 266 b'dropmissing',
267 267 default=False,
268 268 )
269 269 configitem(
270 270 b'histedit',
271 271 b'linelen',
272 272 default=80,
273 273 )
274 274 configitem(
275 275 b'histedit',
276 276 b'singletransaction',
277 277 default=False,
278 278 )
279 279 configitem(
280 280 b'ui',
281 281 b'interface.histedit',
282 282 default=None,
283 283 )
284 284 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
285 285 # TODO: Teach the text-based histedit interface to respect this config option
286 286 # before we make it non-experimental.
287 287 configitem(
288 288 b'histedit', b'later-commits-first', default=False, experimental=True
289 289 )
290 290
291 291 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
292 292 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
293 293 # be specifying the version(s) of Mercurial they are tested with, or
294 294 # leave the attribute unspecified.
295 295 testedwith = b'ships-with-hg-core'
296 296
297 297 actiontable = {}
298 298 primaryactions = set()
299 299 secondaryactions = set()
300 300 tertiaryactions = set()
301 301 internalactions = set()
302 302
303 303
304 304 def geteditcomment(ui, first, last):
305 305 """construct the editor comment
306 306 The comment includes::
307 307 - an intro
308 308 - sorted primary commands
309 309 - sorted short commands
310 310 - sorted long commands
311 311 - additional hints
312 312
313 313 Commands are only included once.
314 314 """
315 315 intro = _(
316 316 b"""Edit history between %s and %s
317 317
318 318 Commits are listed from least to most recent
319 319
320 320 You can reorder changesets by reordering the lines
321 321
322 322 Commands:
323 323 """
324 324 )
325 325 actions = []
326 326
327 327 def addverb(v):
328 328 a = actiontable[v]
329 329 lines = a.message.split(b"\n")
330 330 if len(a.verbs):
331 331 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
332 332 actions.append(b" %s = %s" % (v, lines[0]))
333 333 actions.extend([b' %s'] * (len(lines) - 1))
334 334
335 335 for v in (
336 336 sorted(primaryactions)
337 337 + sorted(secondaryactions)
338 338 + sorted(tertiaryactions)
339 339 ):
340 340 addverb(v)
341 341 actions.append(b'')
342 342
343 343 hints = []
344 344 if ui.configbool(b'histedit', b'dropmissing'):
345 345 hints.append(
346 346 b"Deleting a changeset from the list "
347 347 b"will DISCARD it from the edited history!"
348 348 )
349 349
350 350 lines = (intro % (first, last)).split(b'\n') + actions + hints
351 351
352 352 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
353 353
354 354
355 355 class histeditstate(object):
356 356 def __init__(self, repo):
357 357 self.repo = repo
358 358 self.actions = None
359 359 self.keep = None
360 360 self.topmost = None
361 361 self.parentctxnode = None
362 362 self.lock = None
363 363 self.wlock = None
364 364 self.backupfile = None
365 365 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
366 366 self.replacements = []
367 367
368 368 def read(self):
369 369 """Load histedit state from disk and set fields appropriately."""
370 370 if not self.stateobj.exists():
371 371 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
372 372
373 373 data = self._read()
374 374
375 375 self.parentctxnode = data[b'parentctxnode']
376 376 actions = parserules(data[b'rules'], self)
377 377 self.actions = actions
378 378 self.keep = data[b'keep']
379 379 self.topmost = data[b'topmost']
380 380 self.replacements = data[b'replacements']
381 381 self.backupfile = data[b'backupfile']
382 382
383 383 def _read(self):
384 384 fp = self.repo.vfs.read(b'histedit-state')
385 385 if fp.startswith(b'v1\n'):
386 386 data = self._load()
387 387 parentctxnode, rules, keep, topmost, replacements, backupfile = data
388 388 else:
389 389 data = pickle.loads(fp)
390 390 parentctxnode, rules, keep, topmost, replacements = data
391 391 backupfile = None
392 392 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
393 393
394 394 return {
395 395 b'parentctxnode': parentctxnode,
396 396 b"rules": rules,
397 397 b"keep": keep,
398 398 b"topmost": topmost,
399 399 b"replacements": replacements,
400 400 b"backupfile": backupfile,
401 401 }
402 402
403 403 def write(self, tr=None):
404 404 if tr:
405 405 tr.addfilegenerator(
406 406 b'histedit-state',
407 407 (b'histedit-state',),
408 408 self._write,
409 409 location=b'plain',
410 410 )
411 411 else:
412 412 with self.repo.vfs(b"histedit-state", b"w") as f:
413 413 self._write(f)
414 414
415 415 def _write(self, fp):
416 416 fp.write(b'v1\n')
417 417 fp.write(b'%s\n' % hex(self.parentctxnode))
418 418 fp.write(b'%s\n' % hex(self.topmost))
419 419 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
420 420 fp.write(b'%d\n' % len(self.actions))
421 421 for action in self.actions:
422 422 fp.write(b'%s\n' % action.tostate())
423 423 fp.write(b'%d\n' % len(self.replacements))
424 424 for replacement in self.replacements:
425 425 fp.write(
426 426 b'%s%s\n'
427 427 % (
428 428 hex(replacement[0]),
429 429 b''.join(hex(r) for r in replacement[1]),
430 430 )
431 431 )
432 432 backupfile = self.backupfile
433 433 if not backupfile:
434 434 backupfile = b''
435 435 fp.write(b'%s\n' % backupfile)
436 436
437 437 def _load(self):
438 438 fp = self.repo.vfs(b'histedit-state', b'r')
439 439 lines = [l[:-1] for l in fp.readlines()]
440 440
441 441 index = 0
442 442 lines[index] # version number
443 443 index += 1
444 444
445 445 parentctxnode = bin(lines[index])
446 446 index += 1
447 447
448 448 topmost = bin(lines[index])
449 449 index += 1
450 450
451 451 keep = lines[index] == b'True'
452 452 index += 1
453 453
454 454 # Rules
455 455 rules = []
456 456 rulelen = int(lines[index])
457 457 index += 1
458 458 for i in pycompat.xrange(rulelen):
459 459 ruleaction = lines[index]
460 460 index += 1
461 461 rule = lines[index]
462 462 index += 1
463 463 rules.append((ruleaction, rule))
464 464
465 465 # Replacements
466 466 replacements = []
467 467 replacementlen = int(lines[index])
468 468 index += 1
469 469 for i in pycompat.xrange(replacementlen):
470 470 replacement = lines[index]
471 471 original = bin(replacement[:40])
472 472 succ = [
473 473 bin(replacement[i : i + 40])
474 474 for i in range(40, len(replacement), 40)
475 475 ]
476 476 replacements.append((original, succ))
477 477 index += 1
478 478
479 479 backupfile = lines[index]
480 480 index += 1
481 481
482 482 fp.close()
483 483
484 484 return parentctxnode, rules, keep, topmost, replacements, backupfile
485 485
486 486 def clear(self):
487 487 if self.inprogress():
488 488 self.repo.vfs.unlink(b'histedit-state')
489 489
490 490 def inprogress(self):
491 491 return self.repo.vfs.exists(b'histedit-state')
492 492
493 493
494 494 class histeditaction(object):
495 495 def __init__(self, state, node):
496 496 self.state = state
497 497 self.repo = state.repo
498 498 self.node = node
499 499
500 500 @classmethod
501 501 def fromrule(cls, state, rule):
502 502 """Parses the given rule, returning an instance of the histeditaction."""
503 503 ruleid = rule.strip().split(b' ', 1)[0]
504 504 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
505 505 # Check for validation of rule ids and get the rulehash
506 506 try:
507 507 rev = bin(ruleid)
508 508 except TypeError:
509 509 try:
510 510 _ctx = scmutil.revsingle(state.repo, ruleid)
511 511 rulehash = _ctx.hex()
512 512 rev = bin(rulehash)
513 513 except error.RepoLookupError:
514 514 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
515 515 return cls(state, rev)
516 516
517 517 def verify(self, prev, expected, seen):
518 518 """Verifies semantic correctness of the rule"""
519 519 repo = self.repo
520 520 ha = hex(self.node)
521 521 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
522 522 if self.node is None:
523 523 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
524 524 self._verifynodeconstraints(prev, expected, seen)
525 525
526 526 def _verifynodeconstraints(self, prev, expected, seen):
527 527 # by default command need a node in the edited list
528 528 if self.node not in expected:
529 529 raise error.ParseError(
530 530 _(b'%s "%s" changeset was not a candidate')
531 531 % (self.verb, short(self.node)),
532 532 hint=_(b'only use listed changesets'),
533 533 )
534 534 # and only one command per node
535 535 if self.node in seen:
536 536 raise error.ParseError(
537 537 _(b'duplicated command for changeset %s') % short(self.node)
538 538 )
539 539
540 540 def torule(self):
541 541 """build a histedit rule line for an action
542 542
543 543 by default lines are in the form:
544 544 <hash> <rev> <summary>
545 545 """
546 546 ctx = self.repo[self.node]
547 547 ui = self.repo.ui
548 548 # We don't want color codes in the commit message template, so
549 549 # disable the label() template function while we render it.
550 550 with ui.configoverride(
551 551 {(b'templatealias', b'label(l,x)'): b"x"}, b'histedit'
552 552 ):
553 553 summary = cmdutil.rendertemplate(
554 554 ctx, ui.config(b'histedit', b'summary-template')
555 555 )
556 556 # Handle the fact that `''.splitlines() => []`
557 557 summary = summary.splitlines()[0] if summary else b''
558 558 line = b'%s %s %s' % (self.verb, ctx, summary)
559 559 # trim to 75 columns by default so it's not stupidly wide in my editor
560 560 # (the 5 more are left for verb)
561 561 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
562 562 maxlen = max(maxlen, 22) # avoid truncating hash
563 563 return stringutil.ellipsis(line, maxlen)
564 564
565 565 def tostate(self):
566 566 """Print an action in format used by histedit state files
567 567 (the first line is a verb, the remainder is the second)
568 568 """
569 569 return b"%s\n%s" % (self.verb, hex(self.node))
570 570
571 571 def run(self):
572 572 """Runs the action. The default behavior is simply apply the action's
573 573 rulectx onto the current parentctx."""
574 574 self.applychange()
575 575 self.continuedirty()
576 576 return self.continueclean()
577 577
578 578 def applychange(self):
579 579 """Applies the changes from this action's rulectx onto the current
580 580 parentctx, but does not commit them."""
581 581 repo = self.repo
582 582 rulectx = repo[self.node]
583 583 with repo.ui.silent():
584 584 hg.update(repo, self.state.parentctxnode, quietempty=True)
585 585 stats = applychanges(repo.ui, repo, rulectx, {})
586 586 repo.dirstate.setbranch(rulectx.branch())
587 587 if stats.unresolvedcount:
588 588 raise error.InterventionRequired(
589 589 _(b'Fix up the change (%s %s)') % (self.verb, short(self.node)),
590 590 hint=_(b'hg histedit --continue to resume'),
591 591 )
592 592
593 593 def continuedirty(self):
594 594 """Continues the action when changes have been applied to the working
595 595 copy. The default behavior is to commit the dirty changes."""
596 596 repo = self.repo
597 597 rulectx = repo[self.node]
598 598
599 599 editor = self.commiteditor()
600 600 commit = commitfuncfor(repo, rulectx)
601 601 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
602 602 date = dateutil.makedate()
603 603 else:
604 604 date = rulectx.date()
605 605 commit(
606 606 text=rulectx.description(),
607 607 user=rulectx.user(),
608 608 date=date,
609 609 extra=rulectx.extra(),
610 610 editor=editor,
611 611 )
612 612
613 613 def commiteditor(self):
614 614 """The editor to be used to edit the commit message."""
615 615 return False
616 616
617 617 def continueclean(self):
618 618 """Continues the action when the working copy is clean. The default
619 619 behavior is to accept the current commit as the new version of the
620 620 rulectx."""
621 621 ctx = self.repo[b'.']
622 622 if ctx.node() == self.state.parentctxnode:
623 623 self.repo.ui.warn(
624 624 _(b'%s: skipping changeset (no changes)\n') % short(self.node)
625 625 )
626 626 return ctx, [(self.node, tuple())]
627 627 if ctx.node() == self.node:
628 628 # Nothing changed
629 629 return ctx, []
630 630 return ctx, [(self.node, (ctx.node(),))]
631 631
632 632
633 633 def commitfuncfor(repo, src):
634 634 """Build a commit function for the replacement of <src>
635 635
636 636 This function ensure we apply the same treatment to all changesets.
637 637
638 638 - Add a 'histedit_source' entry in extra.
639 639
640 640 Note that fold has its own separated logic because its handling is a bit
641 641 different and not easily factored out of the fold method.
642 642 """
643 643 phasemin = src.phase()
644 644
645 645 def commitfunc(**kwargs):
646 646 overrides = {(b'phases', b'new-commit'): phasemin}
647 647 with repo.ui.configoverride(overrides, b'histedit'):
648 648 extra = kwargs.get('extra', {}).copy()
649 649 extra[b'histedit_source'] = src.hex()
650 650 kwargs['extra'] = extra
651 651 return repo.commit(**kwargs)
652 652
653 653 return commitfunc
654 654
655 655
656 656 def applychanges(ui, repo, ctx, opts):
657 657 """Merge changeset from ctx (only) in the current working directory"""
658 658 if ctx.p1().node() == repo.dirstate.p1():
659 659 # edits are "in place" we do not need to make any merge,
660 660 # just applies changes on parent for editing
661 661 with ui.silent():
662 662 cmdutil.revert(ui, repo, ctx, all=True)
663 663 stats = mergemod.updateresult(0, 0, 0, 0)
664 664 else:
665 665 try:
666 666 # ui.forcemerge is an internal variable, do not document
667 667 repo.ui.setconfig(
668 668 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
669 669 )
670 670 stats = mergemod.graft(
671 671 repo,
672 672 ctx,
673 673 labels=[
674 674 b'already edited',
675 675 b'current change',
676 676 b'parent of current change',
677 677 ],
678 678 )
679 679 finally:
680 680 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
681 681 return stats
682 682
683 683
684 684 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
685 685 """collapse the set of revisions from first to last as new one.
686 686
687 687 Expected commit options are:
688 688 - message
689 689 - date
690 690 - username
691 691 Commit message is edited in all cases.
692 692
693 693 This function works in memory."""
694 694 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
695 695 if not ctxs:
696 696 return None
697 697 for c in ctxs:
698 698 if not c.mutable():
699 699 raise error.ParseError(
700 700 _(b"cannot fold into public change %s") % short(c.node())
701 701 )
702 702 base = firstctx.p1()
703 703
704 704 # commit a new version of the old changeset, including the update
705 705 # collect all files which might be affected
706 706 files = set()
707 707 for ctx in ctxs:
708 708 files.update(ctx.files())
709 709
710 710 # Recompute copies (avoid recording a -> b -> a)
711 711 copied = copies.pathcopies(base, lastctx)
712 712
713 713 # prune files which were reverted by the updates
714 714 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
715 715 # commit version of these files as defined by head
716 716 headmf = lastctx.manifest()
717 717
718 718 def filectxfn(repo, ctx, path):
719 719 if path in headmf:
720 720 fctx = lastctx[path]
721 721 flags = fctx.flags()
722 722 mctx = context.memfilectx(
723 723 repo,
724 724 ctx,
725 725 fctx.path(),
726 726 fctx.data(),
727 727 islink=b'l' in flags,
728 728 isexec=b'x' in flags,
729 729 copysource=copied.get(path),
730 730 )
731 731 return mctx
732 732 return None
733 733
734 734 if commitopts.get(b'message'):
735 735 message = commitopts[b'message']
736 736 else:
737 737 message = firstctx.description()
738 738 user = commitopts.get(b'user')
739 739 date = commitopts.get(b'date')
740 740 extra = commitopts.get(b'extra')
741 741
742 742 parents = (firstctx.p1().node(), firstctx.p2().node())
743 743 editor = None
744 744 if not skipprompt:
745 745 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
746 746 new = context.memctx(
747 747 repo,
748 748 parents=parents,
749 749 text=message,
750 750 files=files,
751 751 filectxfn=filectxfn,
752 752 user=user,
753 753 date=date,
754 754 extra=extra,
755 755 editor=editor,
756 756 )
757 757 return repo.commitctx(new)
758 758
759 759
760 760 def _isdirtywc(repo):
761 761 return repo[None].dirty(missing=True)
762 762
763 763
764 764 def abortdirty():
765 765 raise error.StateError(
766 766 _(b'working copy has pending changes'),
767 767 hint=_(
768 768 b'amend, commit, or revert them and run histedit '
769 769 b'--continue, or abort with histedit --abort'
770 770 ),
771 771 )
772 772
773 773
774 774 def action(verbs, message, priority=False, internal=False):
775 775 def wrap(cls):
776 776 assert not priority or not internal
777 777 verb = verbs[0]
778 778 if priority:
779 779 primaryactions.add(verb)
780 780 elif internal:
781 781 internalactions.add(verb)
782 782 elif len(verbs) > 1:
783 783 secondaryactions.add(verb)
784 784 else:
785 785 tertiaryactions.add(verb)
786 786
787 787 cls.verb = verb
788 788 cls.verbs = verbs
789 789 cls.message = message
790 790 for verb in verbs:
791 791 actiontable[verb] = cls
792 792 return cls
793 793
794 794 return wrap
795 795
796 796
797 797 @action([b'pick', b'p'], _(b'use commit'), priority=True)
798 798 class pick(histeditaction):
799 799 def run(self):
800 800 rulectx = self.repo[self.node]
801 801 if rulectx.p1().node() == self.state.parentctxnode:
802 802 self.repo.ui.debug(b'node %s unchanged\n' % short(self.node))
803 803 return rulectx, []
804 804
805 805 return super(pick, self).run()
806 806
807 807
808 808 @action(
809 809 [b'edit', b'e'],
810 810 _(b'use commit, but allow edits before making new commit'),
811 811 priority=True,
812 812 )
813 813 class edit(histeditaction):
814 814 def run(self):
815 815 repo = self.repo
816 816 rulectx = repo[self.node]
817 817 hg.update(repo, self.state.parentctxnode, quietempty=True)
818 818 applychanges(repo.ui, repo, rulectx, {})
819 819 hint = _(b'to edit %s, `hg histedit --continue` after making changes')
820 820 raise error.InterventionRequired(
821 821 _(b'Editing (%s), commit as needed now to split the change')
822 822 % short(self.node),
823 823 hint=hint % short(self.node),
824 824 )
825 825
826 826 def commiteditor(self):
827 827 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
828 828
829 829
830 830 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
831 831 class fold(histeditaction):
832 832 def verify(self, prev, expected, seen):
833 833 """Verifies semantic correctness of the fold rule"""
834 834 super(fold, self).verify(prev, expected, seen)
835 835 repo = self.repo
836 836 if not prev:
837 837 c = repo[self.node].p1()
838 838 elif not prev.verb in (b'pick', b'base'):
839 839 return
840 840 else:
841 841 c = repo[prev.node]
842 842 if not c.mutable():
843 843 raise error.ParseError(
844 844 _(b"cannot fold into public change %s") % short(c.node())
845 845 )
846 846
847 847 def continuedirty(self):
848 848 repo = self.repo
849 849 rulectx = repo[self.node]
850 850
851 851 commit = commitfuncfor(repo, rulectx)
852 852 commit(
853 853 text=b'fold-temp-revision %s' % short(self.node),
854 854 user=rulectx.user(),
855 855 date=rulectx.date(),
856 856 extra=rulectx.extra(),
857 857 )
858 858
859 859 def continueclean(self):
860 860 repo = self.repo
861 861 ctx = repo[b'.']
862 862 rulectx = repo[self.node]
863 863 parentctxnode = self.state.parentctxnode
864 864 if ctx.node() == parentctxnode:
865 865 repo.ui.warn(_(b'%s: empty changeset\n') % short(self.node))
866 866 return ctx, [(self.node, (parentctxnode,))]
867 867
868 868 parentctx = repo[parentctxnode]
869 869 newcommits = {
870 870 c.node()
871 871 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
872 872 }
873 873 if not newcommits:
874 874 repo.ui.warn(
875 875 _(
876 876 b'%s: cannot fold - working copy is not a '
877 877 b'descendant of previous commit %s\n'
878 878 )
879 879 % (short(self.node), short(parentctxnode))
880 880 )
881 881 return ctx, [(self.node, (ctx.node(),))]
882 882
883 883 middlecommits = newcommits.copy()
884 884 middlecommits.discard(ctx.node())
885 885
886 886 return self.finishfold(
887 887 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
888 888 )
889 889
890 890 def skipprompt(self):
891 891 """Returns true if the rule should skip the message editor.
892 892
893 893 For example, 'fold' wants to show an editor, but 'rollup'
894 894 doesn't want to.
895 895 """
896 896 return False
897 897
898 898 def mergedescs(self):
899 899 """Returns true if the rule should merge messages of multiple changes.
900 900
901 901 This exists mainly so that 'rollup' rules can be a subclass of
902 902 'fold'.
903 903 """
904 904 return True
905 905
906 906 def firstdate(self):
907 907 """Returns true if the rule should preserve the date of the first
908 908 change.
909 909
910 910 This exists mainly so that 'rollup' rules can be a subclass of
911 911 'fold'.
912 912 """
913 913 return False
914 914
915 915 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
916 916 mergemod.update(ctx.p1())
917 917 ### prepare new commit data
918 918 commitopts = {}
919 919 commitopts[b'user'] = ctx.user()
920 920 # commit message
921 921 if not self.mergedescs():
922 922 newmessage = ctx.description()
923 923 else:
924 924 newmessage = (
925 925 b'\n***\n'.join(
926 926 [ctx.description()]
927 927 + [repo[r].description() for r in internalchanges]
928 928 + [oldctx.description()]
929 929 )
930 930 + b'\n'
931 931 )
932 932 commitopts[b'message'] = newmessage
933 933 # date
934 934 if self.firstdate():
935 935 commitopts[b'date'] = ctx.date()
936 936 else:
937 937 commitopts[b'date'] = max(ctx.date(), oldctx.date())
938 938 # if date is to be updated to current
939 939 if ui.configbool(b'rewrite', b'update-timestamp'):
940 940 commitopts[b'date'] = dateutil.makedate()
941 941
942 942 extra = ctx.extra().copy()
943 943 # histedit_source
944 944 # note: ctx is likely a temporary commit but that the best we can do
945 945 # here. This is sufficient to solve issue3681 anyway.
946 946 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
947 947 commitopts[b'extra'] = extra
948 948 phasemin = max(ctx.phase(), oldctx.phase())
949 949 overrides = {(b'phases', b'new-commit'): phasemin}
950 950 with repo.ui.configoverride(overrides, b'histedit'):
951 951 n = collapse(
952 952 repo,
953 953 ctx,
954 954 repo[newnode],
955 955 commitopts,
956 956 skipprompt=self.skipprompt(),
957 957 )
958 958 if n is None:
959 959 return ctx, []
960 960 mergemod.update(repo[n])
961 961 replacements = [
962 962 (oldctx.node(), (newnode,)),
963 963 (ctx.node(), (n,)),
964 964 (newnode, (n,)),
965 965 ]
966 966 for ich in internalchanges:
967 967 replacements.append((ich, (n,)))
968 968 return repo[n], replacements
969 969
970 970
971 971 @action(
972 972 [b'base', b'b'],
973 973 _(b'checkout changeset and apply further changesets from there'),
974 974 )
975 975 class base(histeditaction):
976 976 def run(self):
977 977 if self.repo[b'.'].node() != self.node:
978 978 mergemod.clean_update(self.repo[self.node])
979 979 return self.continueclean()
980 980
981 981 def continuedirty(self):
982 982 abortdirty()
983 983
984 984 def continueclean(self):
985 985 basectx = self.repo[b'.']
986 986 return basectx, []
987 987
988 988 def _verifynodeconstraints(self, prev, expected, seen):
989 989 # base can only be use with a node not in the edited set
990 990 if self.node in expected:
991 991 msg = _(b'%s "%s" changeset was an edited list candidate')
992 992 raise error.ParseError(
993 993 msg % (self.verb, short(self.node)),
994 994 hint=_(b'base must only use unlisted changesets'),
995 995 )
996 996
997 997
998 998 @action(
999 999 [b'_multifold'],
1000 1000 _(
1001 1001 """fold subclass used for when multiple folds happen in a row
1002 1002
1003 1003 We only want to fire the editor for the folded message once when
1004 1004 (say) four changes are folded down into a single change. This is
1005 1005 similar to rollup, but we should preserve both messages so that
1006 1006 when the last fold operation runs we can show the user all the
1007 1007 commit messages in their editor.
1008 1008 """
1009 1009 ),
1010 1010 internal=True,
1011 1011 )
1012 1012 class _multifold(fold):
1013 1013 def skipprompt(self):
1014 1014 return True
1015 1015
1016 1016
1017 1017 @action(
1018 1018 [b"roll", b"r"],
1019 1019 _(b"like fold, but discard this commit's description and date"),
1020 1020 )
1021 1021 class rollup(fold):
1022 1022 def mergedescs(self):
1023 1023 return False
1024 1024
1025 1025 def skipprompt(self):
1026 1026 return True
1027 1027
1028 1028 def firstdate(self):
1029 1029 return True
1030 1030
1031 1031
1032 1032 @action([b"drop", b"d"], _(b'remove commit from history'))
1033 1033 class drop(histeditaction):
1034 1034 def run(self):
1035 1035 parentctx = self.repo[self.state.parentctxnode]
1036 1036 return parentctx, [(self.node, tuple())]
1037 1037
1038 1038
1039 1039 @action(
1040 1040 [b"mess", b"m"],
1041 1041 _(b'edit commit message without changing commit content'),
1042 1042 priority=True,
1043 1043 )
1044 1044 class message(histeditaction):
1045 1045 def commiteditor(self):
1046 1046 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1047 1047
1048 1048
1049 1049 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1050 1050 """utility function to find the first outgoing changeset
1051 1051
1052 1052 Used by initialization code"""
1053 1053 if opts is None:
1054 1054 opts = {}
1055 1055 path = urlutil.get_unique_push_path(b'histedit', repo, ui, remote)
1056 1056 dest = path.pushloc or path.loc
1057 1057
1058 1058 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(dest))
1059 1059
1060 1060 revs, checkout = hg.addbranchrevs(repo, repo, (path.branch, []), None)
1061 1061 other = hg.peer(repo, opts, dest)
1062 1062
1063 1063 if revs:
1064 1064 revs = [repo.lookup(rev) for rev in revs]
1065 1065
1066 1066 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1067 1067 if not outgoing.missing:
1068 1068 raise error.StateError(_(b'no outgoing ancestors'))
1069 1069 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1070 1070 if len(roots) > 1:
1071 1071 msg = _(b'there are ambiguous outgoing revisions')
1072 1072 hint = _(b"see 'hg help histedit' for more detail")
1073 1073 raise error.StateError(msg, hint=hint)
1074 1074 return repo[roots[0]].node()
1075 1075
1076 1076
1077 1077 # Curses Support
1078 1078 try:
1079 1079 import curses
1080 1080 except ImportError:
1081 1081 curses = None
1082 1082
1083 1083 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1084 1084 ACTION_LABELS = {
1085 1085 b'fold': b'^fold',
1086 1086 b'roll': b'^roll',
1087 1087 }
1088 1088
1089 1089 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1090 1090 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1091 1091 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1092 1092
1093 1093 E_QUIT, E_HISTEDIT = 1, 2
1094 1094 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1095 1095 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1096 1096
1097 1097 KEYTABLE = {
1098 1098 b'global': {
1099 1099 b'h': b'next-action',
1100 1100 b'KEY_RIGHT': b'next-action',
1101 1101 b'l': b'prev-action',
1102 1102 b'KEY_LEFT': b'prev-action',
1103 1103 b'q': b'quit',
1104 1104 b'c': b'histedit',
1105 1105 b'C': b'histedit',
1106 1106 b'v': b'showpatch',
1107 1107 b'?': b'help',
1108 1108 },
1109 1109 MODE_RULES: {
1110 1110 b'd': b'action-drop',
1111 1111 b'e': b'action-edit',
1112 1112 b'f': b'action-fold',
1113 1113 b'm': b'action-mess',
1114 1114 b'p': b'action-pick',
1115 1115 b'r': b'action-roll',
1116 1116 b' ': b'select',
1117 1117 b'j': b'down',
1118 1118 b'k': b'up',
1119 1119 b'KEY_DOWN': b'down',
1120 1120 b'KEY_UP': b'up',
1121 1121 b'J': b'move-down',
1122 1122 b'K': b'move-up',
1123 1123 b'KEY_NPAGE': b'move-down',
1124 1124 b'KEY_PPAGE': b'move-up',
1125 1125 b'0': b'goto', # Used for 0..9
1126 1126 },
1127 1127 MODE_PATCH: {
1128 1128 b' ': b'page-down',
1129 1129 b'KEY_NPAGE': b'page-down',
1130 1130 b'KEY_PPAGE': b'page-up',
1131 1131 b'j': b'line-down',
1132 1132 b'k': b'line-up',
1133 1133 b'KEY_DOWN': b'line-down',
1134 1134 b'KEY_UP': b'line-up',
1135 1135 b'J': b'down',
1136 1136 b'K': b'up',
1137 1137 },
1138 1138 MODE_HELP: {},
1139 1139 }
1140 1140
1141 1141
1142 1142 def screen_size():
1143 1143 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1144 1144
1145 1145
1146 1146 class histeditrule(object):
1147 1147 def __init__(self, ui, ctx, pos, action=b'pick'):
1148 1148 self.ui = ui
1149 1149 self.ctx = ctx
1150 1150 self.action = action
1151 1151 self.origpos = pos
1152 1152 self.pos = pos
1153 1153 self.conflicts = []
1154 1154
1155 1155 def __bytes__(self):
1156 1156 # Example display of several histeditrules:
1157 1157 #
1158 1158 # #10 pick 316392:06a16c25c053 add option to skip tests
1159 1159 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1160 1160 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1161 1161 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1162 1162 #
1163 1163 # The carets point to the changeset being folded into ("roll this
1164 1164 # changeset into the changeset above").
1165 1165 return b'%s%s' % (self.prefix, self.desc)
1166 1166
1167 1167 __str__ = encoding.strmethod(__bytes__)
1168 1168
1169 1169 @property
1170 1170 def prefix(self):
1171 1171 # Some actions ('fold' and 'roll') combine a patch with a
1172 1172 # previous one. Add a marker showing which patch they apply
1173 1173 # to.
1174 1174 action = ACTION_LABELS.get(self.action, self.action)
1175 1175
1176 1176 h = self.ctx.hex()[0:12]
1177 1177 r = self.ctx.rev()
1178 1178
1179 1179 return b"#%s %s %d:%s " % (
1180 1180 (b'%d' % self.origpos).ljust(2),
1181 1181 action.ljust(6),
1182 1182 r,
1183 1183 h,
1184 1184 )
1185 1185
1186 1186 @util.propertycache
1187 1187 def desc(self):
1188 1188 summary = cmdutil.rendertemplate(
1189 1189 self.ctx, self.ui.config(b'histedit', b'summary-template')
1190 1190 )
1191 1191 if summary:
1192 1192 return summary
1193 1193 # This is split off from the prefix property so that we can
1194 1194 # separately make the description for 'roll' red (since it
1195 1195 # will get discarded).
1196 1196 return self.ctx.description().splitlines()[0].strip()
1197 1197
1198 1198 def checkconflicts(self, other):
1199 1199 if other.pos > self.pos and other.origpos <= self.origpos:
1200 1200 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1201 1201 self.conflicts.append(other)
1202 1202 return self.conflicts
1203 1203
1204 1204 if other in self.conflicts:
1205 1205 self.conflicts.remove(other)
1206 1206 return self.conflicts
1207 1207
1208 1208
1209 1209 def makecommands(rules):
1210 1210 """Returns a list of commands consumable by histedit --commands based on
1211 1211 our list of rules"""
1212 1212 commands = []
1213 1213 for rules in rules:
1214 1214 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1215 1215 return commands
1216 1216
1217 1217
1218 1218 def addln(win, y, x, line, color=None):
1219 1219 """Add a line to the given window left padding but 100% filled with
1220 1220 whitespace characters, so that the color appears on the whole line"""
1221 1221 maxy, maxx = win.getmaxyx()
1222 1222 length = maxx - 1 - x
1223 1223 line = bytes(line).ljust(length)[:length]
1224 1224 if y < 0:
1225 1225 y = maxy + y
1226 1226 if x < 0:
1227 1227 x = maxx + x
1228 1228 if color:
1229 1229 win.addstr(y, x, line, color)
1230 1230 else:
1231 1231 win.addstr(y, x, line)
1232 1232
1233 1233
1234 1234 def _trunc_head(line, n):
1235 1235 if len(line) <= n:
1236 1236 return line
1237 1237 return b'> ' + line[-(n - 2) :]
1238 1238
1239 1239
1240 1240 def _trunc_tail(line, n):
1241 1241 if len(line) <= n:
1242 1242 return line
1243 1243 return line[: n - 2] + b' >'
1244 1244
1245 1245
1246 1246 class _chistedit_state(object):
1247 1247 def __init__(
1248 1248 self,
1249 1249 repo,
1250 1250 rules,
1251 1251 stdscr,
1252 1252 ):
1253 1253 self.repo = repo
1254 1254 self.rules = rules
1255 1255 self.stdscr = stdscr
1256 1256 self.later_on_top = repo.ui.configbool(
1257 1257 b'histedit', b'later-commits-first'
1258 1258 )
1259 1259 # The current item in display order, initialized to point to the top
1260 1260 # of the screen.
1261 1261 self.pos = 0
1262 1262 self.selected = None
1263 1263 self.mode = (MODE_INIT, MODE_INIT)
1264 1264 self.page_height = None
1265 1265 self.modes = {
1266 1266 MODE_RULES: {
1267 1267 b'line_offset': 0,
1268 1268 },
1269 1269 MODE_PATCH: {
1270 1270 b'line_offset': 0,
1271 1271 },
1272 1272 }
1273 1273
1274 1274 def render_commit(self, win):
1275 1275 """Renders the commit window that shows the log of the current selected
1276 1276 commit"""
1277 1277 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1278 1278
1279 1279 ctx = rule.ctx
1280 1280 win.box()
1281 1281
1282 1282 maxy, maxx = win.getmaxyx()
1283 1283 length = maxx - 3
1284 1284
1285 1285 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1286 1286 win.addstr(1, 1, line[:length])
1287 1287
1288 1288 line = b"user: %s" % ctx.user()
1289 1289 win.addstr(2, 1, line[:length])
1290 1290
1291 1291 bms = self.repo.nodebookmarks(ctx.node())
1292 1292 line = b"bookmark: %s" % b' '.join(bms)
1293 1293 win.addstr(3, 1, line[:length])
1294 1294
1295 1295 line = b"summary: %s" % (ctx.description().splitlines()[0])
1296 1296 win.addstr(4, 1, line[:length])
1297 1297
1298 1298 line = b"files: "
1299 1299 win.addstr(5, 1, line)
1300 1300 fnx = 1 + len(line)
1301 1301 fnmaxx = length - fnx + 1
1302 1302 y = 5
1303 1303 fnmaxn = maxy - (1 + y) - 1
1304 1304 files = ctx.files()
1305 1305 for i, line1 in enumerate(files):
1306 1306 if len(files) > fnmaxn and i == fnmaxn - 1:
1307 1307 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1308 1308 y = y + 1
1309 1309 break
1310 1310 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1311 1311 y = y + 1
1312 1312
1313 1313 conflicts = rule.conflicts
1314 1314 if len(conflicts) > 0:
1315 1315 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1316 1316 conflictstr = b"changed files overlap with %s" % conflictstr
1317 1317 else:
1318 1318 conflictstr = b'no overlap'
1319 1319
1320 1320 win.addstr(y, 1, conflictstr[:length])
1321 1321 win.noutrefresh()
1322 1322
1323 1323 def helplines(self):
1324 1324 if self.mode[0] == MODE_PATCH:
1325 1325 help = b"""\
1326 1326 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1327 1327 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1328 1328 """
1329 1329 else:
1330 1330 help = b"""\
1331 1331 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1332 1332 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1333 1333 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1334 1334 """
1335 1335 if self.later_on_top:
1336 1336 help += b"Newer commits are shown above older commits.\n"
1337 1337 else:
1338 1338 help += b"Older commits are shown above newer commits.\n"
1339 1339 return help.splitlines()
1340 1340
1341 1341 def render_help(self, win):
1342 1342 maxy, maxx = win.getmaxyx()
1343 1343 for y, line in enumerate(self.helplines()):
1344 1344 if y >= maxy:
1345 1345 break
1346 1346 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1347 1347 win.noutrefresh()
1348 1348
1349 1349 def layout(self):
1350 1350 maxy, maxx = self.stdscr.getmaxyx()
1351 1351 helplen = len(self.helplines())
1352 1352 mainlen = maxy - helplen - 12
1353 1353 if mainlen < 1:
1354 1354 raise error.Abort(
1355 1355 _(b"terminal dimensions %d by %d too small for curses histedit")
1356 1356 % (maxy, maxx),
1357 1357 hint=_(
1358 1358 b"enlarge your terminal or use --config ui.interface=text"
1359 1359 ),
1360 1360 )
1361 1361 return {
1362 1362 b'commit': (12, maxx),
1363 1363 b'help': (helplen, maxx),
1364 1364 b'main': (mainlen, maxx),
1365 1365 }
1366 1366
1367 1367 def display_pos_to_rule_pos(self, display_pos):
1368 1368 """Converts a position in display order to rule order.
1369 1369
1370 1370 The `display_pos` is the order from the top in display order, not
1371 1371 considering which items are currently visible on the screen. Thus,
1372 1372 `display_pos=0` is the item at the top (possibly after scrolling to
1373 1373 the top)
1374 1374 """
1375 1375 if self.later_on_top:
1376 1376 return len(self.rules) - 1 - display_pos
1377 1377 else:
1378 1378 return display_pos
1379 1379
1380 1380 def render_rules(self, rulesscr):
1381 1381 start = self.modes[MODE_RULES][b'line_offset']
1382 1382
1383 1383 conflicts = [r.ctx for r in self.rules if r.conflicts]
1384 1384 if len(conflicts) > 0:
1385 1385 line = b"potential conflict in %s" % b','.join(
1386 1386 map(pycompat.bytestr, conflicts)
1387 1387 )
1388 1388 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1389 1389
1390 1390 for display_pos in range(start, len(self.rules)):
1391 1391 y = display_pos - start
1392 1392 if y < 0 or y >= self.page_height:
1393 1393 continue
1394 1394 rule_pos = self.display_pos_to_rule_pos(display_pos)
1395 1395 rule = self.rules[rule_pos]
1396 1396 if len(rule.conflicts) > 0:
1397 1397 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1398 1398 else:
1399 1399 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1400 1400
1401 1401 if display_pos == self.selected:
1402 1402 rollcolor = COLOR_ROLL_SELECTED
1403 1403 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1404 1404 elif display_pos == self.pos:
1405 1405 rollcolor = COLOR_ROLL_CURRENT
1406 1406 addln(
1407 1407 rulesscr,
1408 1408 y,
1409 1409 2,
1410 1410 rule,
1411 1411 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1412 1412 )
1413 1413 else:
1414 1414 rollcolor = COLOR_ROLL
1415 1415 addln(rulesscr, y, 2, rule)
1416 1416
1417 1417 if rule.action == b'roll':
1418 1418 rulesscr.addstr(
1419 1419 y,
1420 1420 2 + len(rule.prefix),
1421 1421 rule.desc,
1422 1422 curses.color_pair(rollcolor),
1423 1423 )
1424 1424
1425 1425 rulesscr.noutrefresh()
1426 1426
1427 1427 def render_string(self, win, output, diffcolors=False):
1428 1428 maxy, maxx = win.getmaxyx()
1429 1429 length = min(maxy - 1, len(output))
1430 1430 for y in range(0, length):
1431 1431 line = output[y]
1432 1432 if diffcolors:
1433 1433 if line and line[0] == b'+':
1434 1434 win.addstr(
1435 1435 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1436 1436 )
1437 1437 elif line and line[0] == b'-':
1438 1438 win.addstr(
1439 1439 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1440 1440 )
1441 1441 elif line.startswith(b'@@ '):
1442 1442 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1443 1443 else:
1444 1444 win.addstr(y, 0, line)
1445 1445 else:
1446 1446 win.addstr(y, 0, line)
1447 1447 win.noutrefresh()
1448 1448
1449 1449 def render_patch(self, win):
1450 1450 start = self.modes[MODE_PATCH][b'line_offset']
1451 1451 content = self.modes[MODE_PATCH][b'patchcontents']
1452 1452 self.render_string(win, content[start:], diffcolors=True)
1453 1453
1454 1454 def event(self, ch):
1455 1455 """Change state based on the current character input
1456 1456
1457 1457 This takes the current state and based on the current character input from
1458 1458 the user we change the state.
1459 1459 """
1460 1460 oldpos = self.pos
1461 1461
1462 1462 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1463 1463 return E_RESIZE
1464 1464
1465 1465 lookup_ch = ch
1466 1466 if ch is not None and b'0' <= ch <= b'9':
1467 1467 lookup_ch = b'0'
1468 1468
1469 1469 curmode, prevmode = self.mode
1470 1470 action = KEYTABLE[curmode].get(
1471 1471 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1472 1472 )
1473 1473 if action is None:
1474 1474 return
1475 1475 if action in (b'down', b'move-down'):
1476 1476 newpos = min(oldpos + 1, len(self.rules) - 1)
1477 1477 self.move_cursor(oldpos, newpos)
1478 1478 if self.selected is not None or action == b'move-down':
1479 1479 self.swap(oldpos, newpos)
1480 1480 elif action in (b'up', b'move-up'):
1481 1481 newpos = max(0, oldpos - 1)
1482 1482 self.move_cursor(oldpos, newpos)
1483 1483 if self.selected is not None or action == b'move-up':
1484 1484 self.swap(oldpos, newpos)
1485 1485 elif action == b'next-action':
1486 1486 self.cycle_action(oldpos, next=True)
1487 1487 elif action == b'prev-action':
1488 1488 self.cycle_action(oldpos, next=False)
1489 1489 elif action == b'select':
1490 1490 self.selected = oldpos if self.selected is None else None
1491 1491 self.make_selection(self.selected)
1492 1492 elif action == b'goto' and int(ch) < len(self.rules) <= 10:
1493 1493 newrule = next((r for r in self.rules if r.origpos == int(ch)))
1494 1494 self.move_cursor(oldpos, newrule.pos)
1495 1495 if self.selected is not None:
1496 1496 self.swap(oldpos, newrule.pos)
1497 1497 elif action.startswith(b'action-'):
1498 1498 self.change_action(oldpos, action[7:])
1499 1499 elif action == b'showpatch':
1500 1500 self.change_mode(MODE_PATCH if curmode != MODE_PATCH else prevmode)
1501 1501 elif action == b'help':
1502 1502 self.change_mode(MODE_HELP if curmode != MODE_HELP else prevmode)
1503 1503 elif action == b'quit':
1504 1504 return E_QUIT
1505 1505 elif action == b'histedit':
1506 1506 return E_HISTEDIT
1507 1507 elif action == b'page-down':
1508 1508 return E_PAGEDOWN
1509 1509 elif action == b'page-up':
1510 1510 return E_PAGEUP
1511 1511 elif action == b'line-down':
1512 1512 return E_LINEDOWN
1513 1513 elif action == b'line-up':
1514 1514 return E_LINEUP
1515 1515
1516 1516 def patch_contents(self):
1517 1517 repo = self.repo
1518 1518 rule = self.rules[self.display_pos_to_rule_pos(self.pos)]
1519 1519 displayer = logcmdutil.changesetdisplayer(
1520 1520 repo.ui,
1521 1521 repo,
1522 1522 {b"patch": True, b"template": b"status"},
1523 1523 buffered=True,
1524 1524 )
1525 1525 overrides = {(b'ui', b'verbose'): True}
1526 1526 with repo.ui.configoverride(overrides, source=b'histedit'):
1527 1527 displayer.show(rule.ctx)
1528 1528 displayer.close()
1529 1529 return displayer.hunk[rule.ctx.rev()].splitlines()
1530 1530
1531 1531 def move_cursor(self, oldpos, newpos):
1532 1532 """Change the rule/changeset that the cursor is pointing to, regardless of
1533 1533 current mode (you can switch between patches from the view patch window)."""
1534 1534 self.pos = newpos
1535 1535
1536 1536 mode, _ = self.mode
1537 1537 if mode == MODE_RULES:
1538 1538 # Scroll through the list by updating the view for MODE_RULES, so that
1539 1539 # even if we are not currently viewing the rules, switching back will
1540 1540 # result in the cursor's rule being visible.
1541 1541 modestate = self.modes[MODE_RULES]
1542 1542 if newpos < modestate[b'line_offset']:
1543 1543 modestate[b'line_offset'] = newpos
1544 1544 elif newpos > modestate[b'line_offset'] + self.page_height - 1:
1545 1545 modestate[b'line_offset'] = newpos - self.page_height + 1
1546 1546
1547 1547 # Reset the patch view region to the top of the new patch.
1548 1548 self.modes[MODE_PATCH][b'line_offset'] = 0
1549 1549
1550 1550 def change_mode(self, mode):
1551 1551 curmode, _ = self.mode
1552 1552 self.mode = (mode, curmode)
1553 1553 if mode == MODE_PATCH:
1554 1554 self.modes[MODE_PATCH][b'patchcontents'] = self.patch_contents()
1555 1555
1556 1556 def make_selection(self, pos):
1557 1557 self.selected = pos
1558 1558
1559 1559 def swap(self, oldpos, newpos):
1560 1560 """Swap two positions and calculate necessary conflicts in
1561 1561 O(|newpos-oldpos|) time"""
1562 1562 old_rule_pos = self.display_pos_to_rule_pos(oldpos)
1563 1563 new_rule_pos = self.display_pos_to_rule_pos(newpos)
1564 1564
1565 1565 rules = self.rules
1566 1566 assert 0 <= old_rule_pos < len(rules) and 0 <= new_rule_pos < len(rules)
1567 1567
1568 1568 rules[old_rule_pos], rules[new_rule_pos] = (
1569 1569 rules[new_rule_pos],
1570 1570 rules[old_rule_pos],
1571 1571 )
1572 1572
1573 1573 # TODO: swap should not know about histeditrule's internals
1574 1574 rules[new_rule_pos].pos = new_rule_pos
1575 1575 rules[old_rule_pos].pos = old_rule_pos
1576 1576
1577 1577 start = min(old_rule_pos, new_rule_pos)
1578 1578 end = max(old_rule_pos, new_rule_pos)
1579 1579 for r in pycompat.xrange(start, end + 1):
1580 1580 rules[new_rule_pos].checkconflicts(rules[r])
1581 1581 rules[old_rule_pos].checkconflicts(rules[r])
1582 1582
1583 1583 if self.selected:
1584 1584 self.make_selection(newpos)
1585 1585
1586 1586 def change_action(self, pos, action):
1587 1587 """Change the action state on the given position to the new action"""
1588 1588 assert 0 <= pos < len(self.rules)
1589 1589 self.rules[pos].action = action
1590 1590
1591 1591 def cycle_action(self, pos, next=False):
1592 1592 """Changes the action state the next or the previous action from
1593 1593 the action list"""
1594 1594 assert 0 <= pos < len(self.rules)
1595 1595 current = self.rules[pos].action
1596 1596
1597 1597 assert current in KEY_LIST
1598 1598
1599 1599 index = KEY_LIST.index(current)
1600 1600 if next:
1601 1601 index += 1
1602 1602 else:
1603 1603 index -= 1
1604 1604 self.change_action(pos, KEY_LIST[index % len(KEY_LIST)])
1605 1605
1606 1606 def change_view(self, delta, unit):
1607 1607 """Change the region of whatever is being viewed (a patch or the list of
1608 1608 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
1609 1609 mode, _ = self.mode
1610 1610 if mode != MODE_PATCH:
1611 1611 return
1612 1612 mode_state = self.modes[mode]
1613 1613 num_lines = len(mode_state[b'patchcontents'])
1614 1614 page_height = self.page_height
1615 1615 unit = page_height if unit == b'page' else 1
1616 1616 num_pages = 1 + (num_lines - 1) // page_height
1617 1617 max_offset = (num_pages - 1) * page_height
1618 1618 newline = mode_state[b'line_offset'] + delta * unit
1619 1619 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1620 1620
1621 1621
1622 1622 def _chisteditmain(repo, rules, stdscr):
1623 1623 try:
1624 1624 curses.use_default_colors()
1625 1625 except curses.error:
1626 1626 pass
1627 1627
1628 1628 # initialize color pattern
1629 1629 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1630 1630 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1631 1631 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1632 1632 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1633 1633 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1634 1634 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1635 1635 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1636 1636 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1637 1637 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1638 1638 curses.init_pair(
1639 1639 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1640 1640 )
1641 1641 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1642 1642
1643 1643 # don't display the cursor
1644 1644 try:
1645 1645 curses.curs_set(0)
1646 1646 except curses.error:
1647 1647 pass
1648 1648
1649 1649 def drawvertwin(size, y, x):
1650 1650 win = curses.newwin(size[0], size[1], y, x)
1651 1651 y += size[0]
1652 1652 return win, y, x
1653 1653
1654 1654 state = _chistedit_state(repo, rules, stdscr)
1655 1655
1656 1656 # eventloop
1657 1657 ch = None
1658 1658 stdscr.clear()
1659 1659 stdscr.refresh()
1660 1660 while True:
1661 1661 oldmode, unused = state.mode
1662 1662 if oldmode == MODE_INIT:
1663 1663 state.change_mode(MODE_RULES)
1664 1664 e = state.event(ch)
1665 1665
1666 1666 if e == E_QUIT:
1667 1667 return False
1668 1668 if e == E_HISTEDIT:
1669 1669 return state.rules
1670 1670 else:
1671 1671 if e == E_RESIZE:
1672 1672 size = screen_size()
1673 1673 if size != stdscr.getmaxyx():
1674 1674 curses.resizeterm(*size)
1675 1675
1676 1676 sizes = state.layout()
1677 1677 curmode, unused = state.mode
1678 1678 if curmode != oldmode:
1679 1679 state.page_height = sizes[b'main'][0]
1680 1680 # Adjust the view to fit the current screen size.
1681 1681 state.move_cursor(state.pos, state.pos)
1682 1682
1683 1683 # Pack the windows against the top, each pane spread across the
1684 1684 # full width of the screen.
1685 1685 y, x = (0, 0)
1686 1686 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1687 1687 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1688 1688 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1689 1689
1690 1690 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1691 1691 if e == E_PAGEDOWN:
1692 1692 state.change_view(+1, b'page')
1693 1693 elif e == E_PAGEUP:
1694 1694 state.change_view(-1, b'page')
1695 1695 elif e == E_LINEDOWN:
1696 1696 state.change_view(+1, b'line')
1697 1697 elif e == E_LINEUP:
1698 1698 state.change_view(-1, b'line')
1699 1699
1700 1700 # start rendering
1701 1701 commitwin.erase()
1702 1702 helpwin.erase()
1703 1703 mainwin.erase()
1704 1704 if curmode == MODE_PATCH:
1705 1705 state.render_patch(mainwin)
1706 1706 elif curmode == MODE_HELP:
1707 1707 state.render_string(mainwin, __doc__.strip().splitlines())
1708 1708 else:
1709 1709 state.render_rules(mainwin)
1710 1710 state.render_commit(commitwin)
1711 1711 state.render_help(helpwin)
1712 1712 curses.doupdate()
1713 1713 # done rendering
1714 1714 ch = encoding.strtolocal(stdscr.getkey())
1715 1715
1716 1716
1717 1717 def _chistedit(ui, repo, freeargs, opts):
1718 1718 """interactively edit changeset history via a curses interface
1719 1719
1720 1720 Provides a ncurses interface to histedit. Press ? in chistedit mode
1721 1721 to see an extensive help. Requires python-curses to be installed."""
1722 1722
1723 1723 if curses is None:
1724 1724 raise error.Abort(_(b"Python curses library required"))
1725 1725
1726 1726 # disable color
1727 1727 ui._colormode = None
1728 1728
1729 1729 try:
1730 1730 keep = opts.get(b'keep')
1731 1731 revs = opts.get(b'rev', [])[:]
1732 1732 cmdutil.checkunfinished(repo)
1733 1733 cmdutil.bailifchanged(repo)
1734 1734
1735 1735 revs.extend(freeargs)
1736 1736 if not revs:
1737 1737 defaultrev = destutil.desthistedit(ui, repo)
1738 1738 if defaultrev is not None:
1739 1739 revs.append(defaultrev)
1740 1740 if len(revs) != 1:
1741 1741 raise error.InputError(
1742 1742 _(b'histedit requires exactly one ancestor revision')
1743 1743 )
1744 1744
1745 1745 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
1746 1746 if len(rr) != 1:
1747 1747 raise error.InputError(
1748 1748 _(
1749 1749 b'The specified revisions must have '
1750 1750 b'exactly one common root'
1751 1751 )
1752 1752 )
1753 1753 root = rr[0].node()
1754 1754
1755 1755 topmost = repo.dirstate.p1()
1756 1756 revs = between(repo, root, topmost, keep)
1757 1757 if not revs:
1758 1758 raise error.InputError(
1759 1759 _(b'%s is not an ancestor of working directory') % short(root)
1760 1760 )
1761 1761
1762 1762 rules = []
1763 1763 for i, r in enumerate(revs):
1764 1764 rules.append(histeditrule(ui, repo[r], i))
1765 1765 with util.with_lc_ctype():
1766 1766 rc = curses.wrapper(functools.partial(_chisteditmain, repo, rules))
1767 1767 curses.echo()
1768 1768 curses.endwin()
1769 1769 if rc is False:
1770 1770 ui.write(_(b"histedit aborted\n"))
1771 1771 return 0
1772 1772 if type(rc) is list:
1773 1773 ui.status(_(b"performing changes\n"))
1774 1774 rules = makecommands(rc)
1775 1775 with repo.vfs(b'chistedit', b'w+') as fp:
1776 1776 for r in rules:
1777 1777 fp.write(r)
1778 1778 opts[b'commands'] = fp.name
1779 1779 return _texthistedit(ui, repo, freeargs, opts)
1780 1780 except KeyboardInterrupt:
1781 1781 pass
1782 1782 return -1
1783 1783
1784 1784
1785 1785 @command(
1786 1786 b'histedit',
1787 1787 [
1788 1788 (
1789 1789 b'',
1790 1790 b'commands',
1791 1791 b'',
1792 1792 _(b'read history edits from the specified file'),
1793 1793 _(b'FILE'),
1794 1794 ),
1795 1795 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1796 1796 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1797 1797 (
1798 1798 b'k',
1799 1799 b'keep',
1800 1800 False,
1801 1801 _(b"don't strip old nodes after edit is complete"),
1802 1802 ),
1803 1803 (b'', b'abort', False, _(b'abort an edit in progress')),
1804 1804 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1805 1805 (
1806 1806 b'f',
1807 1807 b'force',
1808 1808 False,
1809 1809 _(b'force outgoing even for unrelated repositories'),
1810 1810 ),
1811 1811 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1812 1812 ]
1813 1813 + cmdutil.formatteropts,
1814 1814 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1815 1815 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1816 1816 )
1817 1817 def histedit(ui, repo, *freeargs, **opts):
1818 1818 """interactively edit changeset history
1819 1819
1820 1820 This command lets you edit a linear series of changesets (up to
1821 1821 and including the working directory, which should be clean).
1822 1822 You can:
1823 1823
1824 1824 - `pick` to [re]order a changeset
1825 1825
1826 1826 - `drop` to omit changeset
1827 1827
1828 1828 - `mess` to reword the changeset commit message
1829 1829
1830 1830 - `fold` to combine it with the preceding changeset (using the later date)
1831 1831
1832 1832 - `roll` like fold, but discarding this commit's description and date
1833 1833
1834 1834 - `edit` to edit this changeset (preserving date)
1835 1835
1836 1836 - `base` to checkout changeset and apply further changesets from there
1837 1837
1838 1838 There are a number of ways to select the root changeset:
1839 1839
1840 1840 - Specify ANCESTOR directly
1841 1841
1842 1842 - Use --outgoing -- it will be the first linear changeset not
1843 1843 included in destination. (See :hg:`help config.paths.default-push`)
1844 1844
1845 1845 - Otherwise, the value from the "histedit.defaultrev" config option
1846 1846 is used as a revset to select the base revision when ANCESTOR is not
1847 1847 specified. The first revision returned by the revset is used. By
1848 1848 default, this selects the editable history that is unique to the
1849 1849 ancestry of the working directory.
1850 1850
1851 1851 .. container:: verbose
1852 1852
1853 1853 If you use --outgoing, this command will abort if there are ambiguous
1854 1854 outgoing revisions. For example, if there are multiple branches
1855 1855 containing outgoing revisions.
1856 1856
1857 1857 Use "min(outgoing() and ::.)" or similar revset specification
1858 1858 instead of --outgoing to specify edit target revision exactly in
1859 1859 such ambiguous situation. See :hg:`help revsets` for detail about
1860 1860 selecting revisions.
1861 1861
1862 1862 .. container:: verbose
1863 1863
1864 1864 Examples:
1865 1865
1866 1866 - A number of changes have been made.
1867 1867 Revision 3 is no longer needed.
1868 1868
1869 1869 Start history editing from revision 3::
1870 1870
1871 1871 hg histedit -r 3
1872 1872
1873 1873 An editor opens, containing the list of revisions,
1874 1874 with specific actions specified::
1875 1875
1876 1876 pick 5339bf82f0ca 3 Zworgle the foobar
1877 1877 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1878 1878 pick 0a9639fcda9d 5 Morgify the cromulancy
1879 1879
1880 1880 Additional information about the possible actions
1881 1881 to take appears below the list of revisions.
1882 1882
1883 1883 To remove revision 3 from the history,
1884 1884 its action (at the beginning of the relevant line)
1885 1885 is changed to 'drop'::
1886 1886
1887 1887 drop 5339bf82f0ca 3 Zworgle the foobar
1888 1888 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1889 1889 pick 0a9639fcda9d 5 Morgify the cromulancy
1890 1890
1891 1891 - A number of changes have been made.
1892 1892 Revision 2 and 4 need to be swapped.
1893 1893
1894 1894 Start history editing from revision 2::
1895 1895
1896 1896 hg histedit -r 2
1897 1897
1898 1898 An editor opens, containing the list of revisions,
1899 1899 with specific actions specified::
1900 1900
1901 1901 pick 252a1af424ad 2 Blorb a morgwazzle
1902 1902 pick 5339bf82f0ca 3 Zworgle the foobar
1903 1903 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1904 1904
1905 1905 To swap revision 2 and 4, its lines are swapped
1906 1906 in the editor::
1907 1907
1908 1908 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1909 1909 pick 5339bf82f0ca 3 Zworgle the foobar
1910 1910 pick 252a1af424ad 2 Blorb a morgwazzle
1911 1911
1912 1912 Returns 0 on success, 1 if user intervention is required (not only
1913 1913 for intentional "edit" command, but also for resolving unexpected
1914 1914 conflicts).
1915 1915 """
1916 1916 opts = pycompat.byteskwargs(opts)
1917 1917
1918 1918 # kludge: _chistedit only works for starting an edit, not aborting
1919 1919 # or continuing, so fall back to regular _texthistedit for those
1920 1920 # operations.
1921 1921 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1922 1922 return _chistedit(ui, repo, freeargs, opts)
1923 1923 return _texthistedit(ui, repo, freeargs, opts)
1924 1924
1925 1925
1926 1926 def _texthistedit(ui, repo, freeargs, opts):
1927 1927 state = histeditstate(repo)
1928 1928 with repo.wlock() as wlock, repo.lock() as lock:
1929 1929 state.wlock = wlock
1930 1930 state.lock = lock
1931 1931 _histedit(ui, repo, state, freeargs, opts)
1932 1932
1933 1933
1934 1934 goalcontinue = b'continue'
1935 1935 goalabort = b'abort'
1936 1936 goaleditplan = b'edit-plan'
1937 1937 goalnew = b'new'
1938 1938
1939 1939
1940 1940 def _getgoal(opts):
1941 1941 if opts.get(b'continue'):
1942 1942 return goalcontinue
1943 1943 if opts.get(b'abort'):
1944 1944 return goalabort
1945 1945 if opts.get(b'edit_plan'):
1946 1946 return goaleditplan
1947 1947 return goalnew
1948 1948
1949 1949
1950 1950 def _readfile(ui, path):
1951 1951 if path == b'-':
1952 1952 with ui.timeblockedsection(b'histedit'):
1953 1953 return ui.fin.read()
1954 1954 else:
1955 1955 with open(path, b'rb') as f:
1956 1956 return f.read()
1957 1957
1958 1958
1959 1959 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1960 1960 # TODO only abort if we try to histedit mq patches, not just
1961 1961 # blanket if mq patches are applied somewhere
1962 1962 mq = getattr(repo, 'mq', None)
1963 1963 if mq and mq.applied:
1964 1964 raise error.StateError(_(b'source has mq patches applied'))
1965 1965
1966 1966 # basic argument incompatibility processing
1967 1967 outg = opts.get(b'outgoing')
1968 1968 editplan = opts.get(b'edit_plan')
1969 1969 abort = opts.get(b'abort')
1970 1970 force = opts.get(b'force')
1971 1971 if force and not outg:
1972 1972 raise error.InputError(_(b'--force only allowed with --outgoing'))
1973 1973 if goal == b'continue':
1974 1974 if any((outg, abort, revs, freeargs, rules, editplan)):
1975 1975 raise error.InputError(_(b'no arguments allowed with --continue'))
1976 1976 elif goal == b'abort':
1977 1977 if any((outg, revs, freeargs, rules, editplan)):
1978 1978 raise error.InputError(_(b'no arguments allowed with --abort'))
1979 1979 elif goal == b'edit-plan':
1980 1980 if any((outg, revs, freeargs)):
1981 1981 raise error.InputError(
1982 1982 _(b'only --commands argument allowed with --edit-plan')
1983 1983 )
1984 1984 else:
1985 1985 if outg:
1986 1986 if revs:
1987 1987 raise error.InputError(
1988 1988 _(b'no revisions allowed with --outgoing')
1989 1989 )
1990 1990 if len(freeargs) > 1:
1991 1991 raise error.InputError(
1992 1992 _(b'only one repo argument allowed with --outgoing')
1993 1993 )
1994 1994 else:
1995 1995 revs.extend(freeargs)
1996 1996 if len(revs) == 0:
1997 1997 defaultrev = destutil.desthistedit(ui, repo)
1998 1998 if defaultrev is not None:
1999 1999 revs.append(defaultrev)
2000 2000
2001 2001 if len(revs) != 1:
2002 2002 raise error.InputError(
2003 2003 _(b'histedit requires exactly one ancestor revision')
2004 2004 )
2005 2005
2006 2006
2007 2007 def _histedit(ui, repo, state, freeargs, opts):
2008 2008 fm = ui.formatter(b'histedit', opts)
2009 2009 fm.startitem()
2010 2010 goal = _getgoal(opts)
2011 2011 revs = opts.get(b'rev', [])
2012 2012 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2013 2013 rules = opts.get(b'commands', b'')
2014 2014 state.keep = opts.get(b'keep', False)
2015 2015
2016 2016 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
2017 2017
2018 2018 hastags = False
2019 2019 if revs:
2020 2020 revs = logcmdutil.revrange(repo, revs)
2021 2021 ctxs = [repo[rev] for rev in revs]
2022 2022 for ctx in ctxs:
2023 2023 tags = [tag for tag in ctx.tags() if tag != b'tip']
2024 2024 if not hastags:
2025 2025 hastags = len(tags)
2026 2026 if hastags:
2027 2027 if ui.promptchoice(
2028 2028 _(
2029 2029 b'warning: tags associated with the given'
2030 2030 b' changeset will be lost after histedit.\n'
2031 2031 b'do you want to continue (yN)? $$ &Yes $$ &No'
2032 2032 ),
2033 2033 default=1,
2034 2034 ):
2035 2035 raise error.CanceledError(_(b'histedit cancelled\n'))
2036 2036 # rebuild state
2037 2037 if goal == goalcontinue:
2038 2038 state.read()
2039 2039 state = bootstrapcontinue(ui, state, opts)
2040 2040 elif goal == goaleditplan:
2041 2041 _edithisteditplan(ui, repo, state, rules)
2042 2042 return
2043 2043 elif goal == goalabort:
2044 2044 _aborthistedit(ui, repo, state, nobackup=nobackup)
2045 2045 return
2046 2046 else:
2047 2047 # goal == goalnew
2048 2048 _newhistedit(ui, repo, state, revs, freeargs, opts)
2049 2049
2050 2050 _continuehistedit(ui, repo, state)
2051 2051 _finishhistedit(ui, repo, state, fm)
2052 2052 fm.end()
2053 2053
2054 2054
2055 2055 def _continuehistedit(ui, repo, state):
2056 2056 """This function runs after either:
2057 2057 - bootstrapcontinue (if the goal is 'continue')
2058 2058 - _newhistedit (if the goal is 'new')
2059 2059 """
2060 2060 # preprocess rules so that we can hide inner folds from the user
2061 2061 # and only show one editor
2062 2062 actions = state.actions[:]
2063 2063 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2064 2064 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2065 2065 state.actions[idx].__class__ = _multifold
2066 2066
2067 2067 # Force an initial state file write, so the user can run --abort/continue
2068 2068 # even if there's an exception before the first transaction serialize.
2069 2069 state.write()
2070 2070
2071 2071 tr = None
2072 2072 # Don't use singletransaction by default since it rolls the entire
2073 2073 # transaction back if an unexpected exception happens (like a
2074 2074 # pretxncommit hook throws, or the user aborts the commit msg editor).
2075 2075 if ui.configbool(b"histedit", b"singletransaction"):
2076 2076 # Don't use a 'with' for the transaction, since actions may close
2077 2077 # and reopen a transaction. For example, if the action executes an
2078 2078 # external process it may choose to commit the transaction first.
2079 2079 tr = repo.transaction(b'histedit')
2080 2080 progress = ui.makeprogress(
2081 2081 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2082 2082 )
2083 2083 with progress, util.acceptintervention(tr):
2084 2084 while state.actions:
2085 2085 state.write(tr=tr)
2086 2086 actobj = state.actions[0]
2087 2087 progress.increment(item=actobj.torule())
2088 2088 ui.debug(
2089 2089 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2090 2090 )
2091 2091 parentctx, replacement_ = actobj.run()
2092 2092 state.parentctxnode = parentctx.node()
2093 2093 state.replacements.extend(replacement_)
2094 2094 state.actions.pop(0)
2095 2095
2096 2096 state.write()
2097 2097
2098 2098
2099 2099 def _finishhistedit(ui, repo, state, fm):
2100 2100 """This action runs when histedit is finishing its session"""
2101 2101 mergemod.update(repo[state.parentctxnode])
2102 2102
2103 2103 mapping, tmpnodes, created, ntm = processreplacement(state)
2104 2104 if mapping:
2105 2105 for prec, succs in pycompat.iteritems(mapping):
2106 2106 if not succs:
2107 2107 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2108 2108 else:
2109 2109 ui.debug(
2110 2110 b'histedit: %s is replaced by %s\n'
2111 2111 % (short(prec), short(succs[0]))
2112 2112 )
2113 2113 if len(succs) > 1:
2114 2114 m = b'histedit: %s'
2115 2115 for n in succs[1:]:
2116 2116 ui.debug(m % short(n))
2117 2117
2118 2118 if not state.keep:
2119 2119 if mapping:
2120 2120 movetopmostbookmarks(repo, state.topmost, ntm)
2121 2121 # TODO update mq state
2122 2122 else:
2123 2123 mapping = {}
2124 2124
2125 2125 for n in tmpnodes:
2126 2126 if n in repo:
2127 2127 mapping[n] = ()
2128 2128
2129 2129 # remove entries about unknown nodes
2130 2130 has_node = repo.unfiltered().changelog.index.has_node
2131 2131 mapping = {
2132 2132 k: v
2133 2133 for k, v in mapping.items()
2134 2134 if has_node(k) and all(has_node(n) for n in v)
2135 2135 }
2136 2136 scmutil.cleanupnodes(repo, mapping, b'histedit')
2137 2137 hf = fm.hexfunc
2138 2138 fl = fm.formatlist
2139 2139 fd = fm.formatdict
2140 2140 nodechanges = fd(
2141 2141 {
2142 2142 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2143 2143 for oldn, newn in pycompat.iteritems(mapping)
2144 2144 },
2145 2145 key=b"oldnode",
2146 2146 value=b"newnodes",
2147 2147 )
2148 2148 fm.data(nodechanges=nodechanges)
2149 2149
2150 2150 state.clear()
2151 2151 if os.path.exists(repo.sjoin(b'undo')):
2152 2152 os.unlink(repo.sjoin(b'undo'))
2153 2153 if repo.vfs.exists(b'histedit-last-edit.txt'):
2154 2154 repo.vfs.unlink(b'histedit-last-edit.txt')
2155 2155
2156 2156
2157 2157 def _aborthistedit(ui, repo, state, nobackup=False):
2158 2158 try:
2159 2159 state.read()
2160 2160 __, leafs, tmpnodes, __ = processreplacement(state)
2161 2161 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2162 2162
2163 2163 # Recover our old commits if necessary
2164 2164 if not state.topmost in repo and state.backupfile:
2165 2165 backupfile = repo.vfs.join(state.backupfile)
2166 2166 f = hg.openpath(ui, backupfile)
2167 2167 gen = exchange.readbundle(ui, f, backupfile)
2168 2168 with repo.transaction(b'histedit.abort') as tr:
2169 2169 bundle2.applybundle(
2170 2170 repo,
2171 2171 gen,
2172 2172 tr,
2173 2173 source=b'histedit',
2174 2174 url=b'bundle:' + backupfile,
2175 2175 )
2176 2176
2177 2177 os.remove(backupfile)
2178 2178
2179 2179 # check whether we should update away
2180 2180 if repo.unfiltered().revs(
2181 2181 b'parents() and (%n or %ln::)',
2182 2182 state.parentctxnode,
2183 2183 leafs | tmpnodes,
2184 2184 ):
2185 2185 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2186 2186 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2187 2187 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2188 2188 except Exception:
2189 2189 if state.inprogress():
2190 2190 ui.warn(
2191 2191 _(
2192 2192 b'warning: encountered an exception during histedit '
2193 2193 b'--abort; the repository may not have been completely '
2194 2194 b'cleaned up\n'
2195 2195 )
2196 2196 )
2197 2197 raise
2198 2198 finally:
2199 2199 state.clear()
2200 2200
2201 2201
2202 2202 def hgaborthistedit(ui, repo):
2203 2203 state = histeditstate(repo)
2204 2204 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2205 2205 with repo.wlock() as wlock, repo.lock() as lock:
2206 2206 state.wlock = wlock
2207 2207 state.lock = lock
2208 2208 _aborthistedit(ui, repo, state, nobackup=nobackup)
2209 2209
2210 2210
2211 2211 def _edithisteditplan(ui, repo, state, rules):
2212 2212 state.read()
2213 2213 if not rules:
2214 2214 comment = geteditcomment(
2215 2215 ui, short(state.parentctxnode), short(state.topmost)
2216 2216 )
2217 2217 rules = ruleeditor(repo, ui, state.actions, comment)
2218 2218 else:
2219 2219 rules = _readfile(ui, rules)
2220 2220 actions = parserules(rules, state)
2221 2221 ctxs = [repo[act.node] for act in state.actions if act.node]
2222 2222 warnverifyactions(ui, repo, actions, state, ctxs)
2223 2223 state.actions = actions
2224 2224 state.write()
2225 2225
2226 2226
2227 2227 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2228 2228 outg = opts.get(b'outgoing')
2229 2229 rules = opts.get(b'commands', b'')
2230 2230 force = opts.get(b'force')
2231 2231
2232 2232 cmdutil.checkunfinished(repo)
2233 2233 cmdutil.bailifchanged(repo)
2234 2234
2235 2235 topmost = repo.dirstate.p1()
2236 2236 if outg:
2237 2237 if freeargs:
2238 2238 remote = freeargs[0]
2239 2239 else:
2240 2240 remote = None
2241 2241 root = findoutgoing(ui, repo, remote, force, opts)
2242 2242 else:
2243 2243 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
2244 2244 if len(rr) != 1:
2245 2245 raise error.InputError(
2246 2246 _(
2247 2247 b'The specified revisions must have '
2248 2248 b'exactly one common root'
2249 2249 )
2250 2250 )
2251 2251 root = rr[0].node()
2252 2252
2253 2253 revs = between(repo, root, topmost, state.keep)
2254 2254 if not revs:
2255 2255 raise error.InputError(
2256 2256 _(b'%s is not an ancestor of working directory') % short(root)
2257 2257 )
2258 2258
2259 2259 ctxs = [repo[r] for r in revs]
2260 2260
2261 2261 wctx = repo[None]
2262 2262 # Please don't ask me why `ancestors` is this value. I figured it
2263 2263 # out with print-debugging, not by actually understanding what the
2264 2264 # merge code is doing. :(
2265 2265 ancs = [repo[b'.']]
2266 2266 # Sniff-test to make sure we won't collide with untracked files in
2267 2267 # the working directory. If we don't do this, we can get a
2268 2268 # collision after we've started histedit and backing out gets ugly
2269 2269 # for everyone, especially the user.
2270 2270 for c in [ctxs[0].p1()] + ctxs:
2271 2271 try:
2272 2272 mergemod.calculateupdates(
2273 2273 repo,
2274 2274 wctx,
2275 2275 c,
2276 2276 ancs,
2277 2277 # These parameters were determined by print-debugging
2278 2278 # what happens later on inside histedit.
2279 2279 branchmerge=False,
2280 2280 force=False,
2281 2281 acceptremote=False,
2282 2282 followcopies=False,
2283 2283 )
2284 2284 except error.Abort:
2285 2285 raise error.StateError(
2286 2286 _(
2287 2287 b"untracked files in working directory conflict with files in %s"
2288 2288 )
2289 2289 % c
2290 2290 )
2291 2291
2292 2292 if not rules:
2293 2293 comment = geteditcomment(ui, short(root), short(topmost))
2294 2294 actions = [pick(state, r) for r in revs]
2295 2295 rules = ruleeditor(repo, ui, actions, comment)
2296 2296 else:
2297 2297 rules = _readfile(ui, rules)
2298 2298 actions = parserules(rules, state)
2299 2299 warnverifyactions(ui, repo, actions, state, ctxs)
2300 2300
2301 2301 parentctxnode = repo[root].p1().node()
2302 2302
2303 2303 state.parentctxnode = parentctxnode
2304 2304 state.actions = actions
2305 2305 state.topmost = topmost
2306 2306 state.replacements = []
2307 2307
2308 2308 ui.log(
2309 2309 b"histedit",
2310 2310 b"%d actions to histedit\n",
2311 2311 len(actions),
2312 2312 histedit_num_actions=len(actions),
2313 2313 )
2314 2314
2315 2315 # Create a backup so we can always abort completely.
2316 2316 backupfile = None
2317 2317 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2318 2318 backupfile = repair.backupbundle(
2319 2319 repo, [parentctxnode], [topmost], root, b'histedit'
2320 2320 )
2321 2321 state.backupfile = backupfile
2322 2322
2323 2323
2324 2324 def _getsummary(ctx):
2325 2325 # a common pattern is to extract the summary but default to the empty
2326 2326 # string
2327 2327 summary = ctx.description() or b''
2328 2328 if summary:
2329 2329 summary = summary.splitlines()[0]
2330 2330 return summary
2331 2331
2332 2332
2333 2333 def bootstrapcontinue(ui, state, opts):
2334 2334 repo = state.repo
2335 2335
2336 2336 ms = mergestatemod.mergestate.read(repo)
2337 2337 mergeutil.checkunresolved(ms)
2338 2338
2339 2339 if state.actions:
2340 2340 actobj = state.actions.pop(0)
2341 2341
2342 2342 if _isdirtywc(repo):
2343 2343 actobj.continuedirty()
2344 2344 if _isdirtywc(repo):
2345 2345 abortdirty()
2346 2346
2347 2347 parentctx, replacements = actobj.continueclean()
2348 2348
2349 2349 state.parentctxnode = parentctx.node()
2350 2350 state.replacements.extend(replacements)
2351 2351
2352 2352 return state
2353 2353
2354 2354
2355 2355 def between(repo, old, new, keep):
2356 2356 """select and validate the set of revision to edit
2357 2357
2358 2358 When keep is false, the specified set can't have children."""
2359 2359 revs = repo.revs(b'%n::%n', old, new)
2360 2360 if revs and not keep:
2361 2361 rewriteutil.precheck(repo, revs, b'edit')
2362 2362 if repo.revs(b'(%ld) and merge()', revs):
2363 2363 raise error.StateError(
2364 2364 _(b'cannot edit history that contains merges')
2365 2365 )
2366 2366 return pycompat.maplist(repo.changelog.node, revs)
2367 2367
2368 2368
2369 2369 def ruleeditor(repo, ui, actions, editcomment=b""):
2370 2370 """open an editor to edit rules
2371 2371
2372 2372 rules are in the format [ [act, ctx], ...] like in state.rules
2373 2373 """
2374 2374 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2375 2375 newact = util.sortdict()
2376 2376 for act in actions:
2377 2377 ctx = repo[act.node]
2378 2378 summary = _getsummary(ctx)
2379 2379 fword = summary.split(b' ', 1)[0].lower()
2380 2380 added = False
2381 2381
2382 2382 # if it doesn't end with the special character '!' just skip this
2383 2383 if fword.endswith(b'!'):
2384 2384 fword = fword[:-1]
2385 2385 if fword in primaryactions | secondaryactions | tertiaryactions:
2386 2386 act.verb = fword
2387 2387 # get the target summary
2388 2388 tsum = summary[len(fword) + 1 :].lstrip()
2389 2389 # safe but slow: reverse iterate over the actions so we
2390 2390 # don't clash on two commits having the same summary
2391 2391 for na, l in reversed(list(pycompat.iteritems(newact))):
2392 2392 actx = repo[na.node]
2393 2393 asum = _getsummary(actx)
2394 2394 if asum == tsum:
2395 2395 added = True
2396 2396 l.append(act)
2397 2397 break
2398 2398
2399 2399 if not added:
2400 2400 newact[act] = []
2401 2401
2402 2402 # copy over and flatten the new list
2403 2403 actions = []
2404 2404 for na, l in pycompat.iteritems(newact):
2405 2405 actions.append(na)
2406 2406 actions += l
2407 2407
2408 2408 rules = b'\n'.join([act.torule() for act in actions])
2409 2409 rules += b'\n\n'
2410 2410 rules += editcomment
2411 2411 rules = ui.edit(
2412 2412 rules,
2413 2413 ui.username(),
2414 2414 {b'prefix': b'histedit'},
2415 2415 repopath=repo.path,
2416 2416 action=b'histedit',
2417 2417 )
2418 2418
2419 2419 # Save edit rules in .hg/histedit-last-edit.txt in case
2420 2420 # the user needs to ask for help after something
2421 2421 # surprising happens.
2422 2422 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2423 2423 f.write(rules)
2424 2424
2425 2425 return rules
2426 2426
2427 2427
2428 2428 def parserules(rules, state):
2429 2429 """Read the histedit rules string and return list of action objects"""
2430 2430 rules = [
2431 2431 l
2432 2432 for l in (r.strip() for r in rules.splitlines())
2433 2433 if l and not l.startswith(b'#')
2434 2434 ]
2435 2435 actions = []
2436 2436 for r in rules:
2437 2437 if b' ' not in r:
2438 2438 raise error.ParseError(_(b'malformed line "%s"') % r)
2439 2439 verb, rest = r.split(b' ', 1)
2440 2440
2441 2441 if verb not in actiontable:
2442 2442 raise error.ParseError(_(b'unknown action "%s"') % verb)
2443 2443
2444 2444 action = actiontable[verb].fromrule(state, rest)
2445 2445 actions.append(action)
2446 2446 return actions
2447 2447
2448 2448
2449 2449 def warnverifyactions(ui, repo, actions, state, ctxs):
2450 2450 try:
2451 2451 verifyactions(actions, state, ctxs)
2452 2452 except error.ParseError:
2453 2453 if repo.vfs.exists(b'histedit-last-edit.txt'):
2454 2454 ui.warn(
2455 2455 _(
2456 2456 b'warning: histedit rules saved '
2457 2457 b'to: .hg/histedit-last-edit.txt\n'
2458 2458 )
2459 2459 )
2460 2460 raise
2461 2461
2462 2462
2463 2463 def verifyactions(actions, state, ctxs):
2464 2464 """Verify that there exists exactly one action per given changeset and
2465 2465 other constraints.
2466 2466
2467 2467 Will abort if there are to many or too few rules, a malformed rule,
2468 2468 or a rule on a changeset outside of the user-given range.
2469 2469 """
2470 2470 expected = {c.node() for c in ctxs}
2471 2471 seen = set()
2472 2472 prev = None
2473 2473
2474 2474 if actions and actions[0].verb in [b'roll', b'fold']:
2475 2475 raise error.ParseError(
2476 2476 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2477 2477 )
2478 2478
2479 2479 for action in actions:
2480 2480 action.verify(prev, expected, seen)
2481 2481 prev = action
2482 2482 if action.node is not None:
2483 2483 seen.add(action.node)
2484 2484 missing = sorted(expected - seen) # sort to stabilize output
2485 2485
2486 2486 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2487 2487 if len(actions) == 0:
2488 2488 raise error.ParseError(
2489 2489 _(b'no rules provided'),
2490 2490 hint=_(b'use strip extension to remove commits'),
2491 2491 )
2492 2492
2493 2493 drops = [drop(state, n) for n in missing]
2494 2494 # put the in the beginning so they execute immediately and
2495 2495 # don't show in the edit-plan in the future
2496 2496 actions[:0] = drops
2497 2497 elif missing:
2498 2498 raise error.ParseError(
2499 2499 _(b'missing rules for changeset %s') % short(missing[0]),
2500 2500 hint=_(
2501 2501 b'use "drop %s" to discard, see also: '
2502 2502 b"'hg help -e histedit.config'"
2503 2503 )
2504 2504 % short(missing[0]),
2505 2505 )
2506 2506
2507 2507
2508 2508 def adjustreplacementsfrommarkers(repo, oldreplacements):
2509 2509 """Adjust replacements from obsolescence markers
2510 2510
2511 2511 Replacements structure is originally generated based on
2512 2512 histedit's state and does not account for changes that are
2513 2513 not recorded there. This function fixes that by adding
2514 2514 data read from obsolescence markers"""
2515 2515 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2516 2516 return oldreplacements
2517 2517
2518 2518 unfi = repo.unfiltered()
2519 2519 get_rev = unfi.changelog.index.get_rev
2520 2520 obsstore = repo.obsstore
2521 2521 newreplacements = list(oldreplacements)
2522 2522 oldsuccs = [r[1] for r in oldreplacements]
2523 2523 # successors that have already been added to succstocheck once
2524 2524 seensuccs = set().union(
2525 2525 *oldsuccs
2526 2526 ) # create a set from an iterable of tuples
2527 2527 succstocheck = list(seensuccs)
2528 2528 while succstocheck:
2529 2529 n = succstocheck.pop()
2530 2530 missing = get_rev(n) is None
2531 2531 markers = obsstore.successors.get(n, ())
2532 2532 if missing and not markers:
2533 2533 # dead end, mark it as such
2534 2534 newreplacements.append((n, ()))
2535 2535 for marker in markers:
2536 2536 nsuccs = marker[1]
2537 2537 newreplacements.append((n, nsuccs))
2538 2538 for nsucc in nsuccs:
2539 2539 if nsucc not in seensuccs:
2540 2540 seensuccs.add(nsucc)
2541 2541 succstocheck.append(nsucc)
2542 2542
2543 2543 return newreplacements
2544 2544
2545 2545
2546 2546 def processreplacement(state):
2547 2547 """process the list of replacements to return
2548 2548
2549 2549 1) the final mapping between original and created nodes
2550 2550 2) the list of temporary node created by histedit
2551 2551 3) the list of new commit created by histedit"""
2552 2552 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2553 2553 allsuccs = set()
2554 2554 replaced = set()
2555 2555 fullmapping = {}
2556 2556 # initialize basic set
2557 2557 # fullmapping records all operations recorded in replacement
2558 2558 for rep in replacements:
2559 2559 allsuccs.update(rep[1])
2560 2560 replaced.add(rep[0])
2561 2561 fullmapping.setdefault(rep[0], set()).update(rep[1])
2562 2562 new = allsuccs - replaced
2563 2563 tmpnodes = allsuccs & replaced
2564 2564 # Reduce content fullmapping into direct relation between original nodes
2565 2565 # and final node created during history edition
2566 2566 # Dropped changeset are replaced by an empty list
2567 2567 toproceed = set(fullmapping)
2568 2568 final = {}
2569 2569 while toproceed:
2570 2570 for x in list(toproceed):
2571 2571 succs = fullmapping[x]
2572 2572 for s in list(succs):
2573 2573 if s in toproceed:
2574 2574 # non final node with unknown closure
2575 2575 # We can't process this now
2576 2576 break
2577 2577 elif s in final:
2578 2578 # non final node, replace with closure
2579 2579 succs.remove(s)
2580 2580 succs.update(final[s])
2581 2581 else:
2582 2582 final[x] = succs
2583 2583 toproceed.remove(x)
2584 2584 # remove tmpnodes from final mapping
2585 2585 for n in tmpnodes:
2586 2586 del final[n]
2587 2587 # we expect all changes involved in final to exist in the repo
2588 2588 # turn `final` into list (topologically sorted)
2589 2589 get_rev = state.repo.changelog.index.get_rev
2590 2590 for prec, succs in final.items():
2591 2591 final[prec] = sorted(succs, key=get_rev)
2592 2592
2593 2593 # computed topmost element (necessary for bookmark)
2594 2594 if new:
2595 2595 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2596 2596 elif not final:
2597 2597 # Nothing rewritten at all. we won't need `newtopmost`
2598 2598 # It is the same as `oldtopmost` and `processreplacement` know it
2599 2599 newtopmost = None
2600 2600 else:
2601 2601 # every body died. The newtopmost is the parent of the root.
2602 2602 r = state.repo.changelog.rev
2603 2603 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2604 2604
2605 2605 return final, tmpnodes, new, newtopmost
2606 2606
2607 2607
2608 2608 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2609 2609 """Move bookmark from oldtopmost to newly created topmost
2610 2610
2611 2611 This is arguably a feature and we may only want that for the active
2612 2612 bookmark. But the behavior is kept compatible with the old version for now.
2613 2613 """
2614 2614 if not oldtopmost or not newtopmost:
2615 2615 return
2616 2616 oldbmarks = repo.nodebookmarks(oldtopmost)
2617 2617 if oldbmarks:
2618 2618 with repo.lock(), repo.transaction(b'histedit') as tr:
2619 2619 marks = repo._bookmarks
2620 2620 changes = []
2621 2621 for name in oldbmarks:
2622 2622 changes.append((name, newtopmost))
2623 2623 marks.applychanges(repo, tr, changes)
2624 2624
2625 2625
2626 2626 def cleanupnode(ui, repo, nodes, nobackup=False):
2627 2627 """strip a group of nodes from the repository
2628 2628
2629 2629 The set of node to strip may contains unknown nodes."""
2630 2630 with repo.lock():
2631 2631 # do not let filtering get in the way of the cleanse
2632 2632 # we should probably get rid of obsolescence marker created during the
2633 2633 # histedit, but we currently do not have such information.
2634 2634 repo = repo.unfiltered()
2635 2635 # Find all nodes that need to be stripped
2636 2636 # (we use %lr instead of %ln to silently ignore unknown items)
2637 2637 has_node = repo.changelog.index.has_node
2638 2638 nodes = sorted(n for n in nodes if has_node(n))
2639 2639 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2640 2640 if roots:
2641 2641 backup = not nobackup
2642 2642 repair.strip(ui, repo, roots, backup=backup)
2643 2643
2644 2644
2645 2645 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2646 2646 if isinstance(nodelist, bytes):
2647 2647 nodelist = [nodelist]
2648 2648 state = histeditstate(repo)
2649 2649 if state.inprogress():
2650 2650 state.read()
2651 2651 histedit_nodes = {
2652 2652 action.node for action in state.actions if action.node
2653 2653 }
2654 2654 common_nodes = histedit_nodes & set(nodelist)
2655 2655 if common_nodes:
2656 2656 raise error.Abort(
2657 2657 _(b"histedit in progress, can't strip %s")
2658 2658 % b', '.join(short(x) for x in common_nodes)
2659 2659 )
2660 2660 return orig(ui, repo, nodelist, *args, **kwargs)
2661 2661
2662 2662
2663 2663 extensions.wrapfunction(repair, b'strip', stripwrapper)
2664 2664
2665 2665
2666 2666 def summaryhook(ui, repo):
2667 2667 state = histeditstate(repo)
2668 2668 if not state.inprogress():
2669 2669 return
2670 2670 state.read()
2671 2671 if state.actions:
2672 2672 # i18n: column positioning for "hg summary"
2673 2673 ui.write(
2674 2674 _(b'hist: %s (histedit --continue)\n')
2675 2675 % (
2676 2676 ui.label(_(b'%d remaining'), b'histedit.remaining')
2677 2677 % len(state.actions)
2678 2678 )
2679 2679 )
2680 2680
2681 2681
2682 2682 def extsetup(ui):
2683 2683 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2684 2684 statemod.addunfinished(
2685 2685 b'histedit',
2686 2686 fname=b'histedit-state',
2687 2687 allowcommit=True,
2688 2688 continueflag=True,
2689 2689 abortfunc=hgaborthistedit,
2690 2690 )
@@ -1,873 +1,872 b''
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import contextlib
111 111 import itertools
112 112 import os
113 import pickle
113 114
114 115 from .i18n import _
115 116 from .node import (
116 117 hex,
117 118 short,
118 119 )
119 120 from .thirdparty import attr
120 121
121 122 from . import (
122 123 error,
123 124 pycompat,
124 125 templatefilters,
125 126 templatekw,
126 127 templater,
127 128 templateutil,
128 129 util,
129 130 )
130 131 from .utils import (
131 132 cborutil,
132 133 dateutil,
133 134 stringutil,
134 135 )
135 136
136 pickle = util.pickle
137
138 137
139 138 def isprintable(obj):
140 139 """Check if the given object can be directly passed in to formatter's
141 140 write() and data() functions
142 141
143 142 Returns False if the object is unsupported or must be pre-processed by
144 143 formatdate(), formatdict(), or formatlist().
145 144 """
146 145 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
147 146
148 147
149 148 class _nullconverter(object):
150 149 '''convert non-primitive data types to be processed by formatter'''
151 150
152 151 # set to True if context object should be stored as item
153 152 storecontext = False
154 153
155 154 @staticmethod
156 155 def wrapnested(data, tmpl, sep):
157 156 '''wrap nested data by appropriate type'''
158 157 return data
159 158
160 159 @staticmethod
161 160 def formatdate(date, fmt):
162 161 '''convert date tuple to appropriate format'''
163 162 # timestamp can be float, but the canonical form should be int
164 163 ts, tz = date
165 164 return (int(ts), tz)
166 165
167 166 @staticmethod
168 167 def formatdict(data, key, value, fmt, sep):
169 168 '''convert dict or key-value pairs to appropriate dict format'''
170 169 # use plain dict instead of util.sortdict so that data can be
171 170 # serialized as a builtin dict in pickle output
172 171 return dict(data)
173 172
174 173 @staticmethod
175 174 def formatlist(data, name, fmt, sep):
176 175 '''convert iterable to appropriate list format'''
177 176 return list(data)
178 177
179 178
180 179 class baseformatter(object):
181 180
182 181 # set to True if the formater output a strict format that does not support
183 182 # arbitrary output in the stream.
184 183 strict_format = False
185 184
186 185 def __init__(self, ui, topic, opts, converter):
187 186 self._ui = ui
188 187 self._topic = topic
189 188 self._opts = opts
190 189 self._converter = converter
191 190 self._item = None
192 191 # function to convert node to string suitable for this output
193 192 self.hexfunc = hex
194 193
195 194 def __enter__(self):
196 195 return self
197 196
198 197 def __exit__(self, exctype, excvalue, traceback):
199 198 if exctype is None:
200 199 self.end()
201 200
202 201 def _showitem(self):
203 202 '''show a formatted item once all data is collected'''
204 203
205 204 def startitem(self):
206 205 '''begin an item in the format list'''
207 206 if self._item is not None:
208 207 self._showitem()
209 208 self._item = {}
210 209
211 210 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
212 211 '''convert date tuple to appropriate format'''
213 212 return self._converter.formatdate(date, fmt)
214 213
215 214 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
216 215 '''convert dict or key-value pairs to appropriate dict format'''
217 216 return self._converter.formatdict(data, key, value, fmt, sep)
218 217
219 218 def formatlist(self, data, name, fmt=None, sep=b' '):
220 219 '''convert iterable to appropriate list format'''
221 220 # name is mandatory argument for now, but it could be optional if
222 221 # we have default template keyword, e.g. {item}
223 222 return self._converter.formatlist(data, name, fmt, sep)
224 223
225 224 def context(self, **ctxs):
226 225 '''insert context objects to be used to render template keywords'''
227 226 ctxs = pycompat.byteskwargs(ctxs)
228 227 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
229 228 if self._converter.storecontext:
230 229 # populate missing resources in fctx -> ctx -> repo order
231 230 if b'fctx' in ctxs and b'ctx' not in ctxs:
232 231 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
233 232 if b'ctx' in ctxs and b'repo' not in ctxs:
234 233 ctxs[b'repo'] = ctxs[b'ctx'].repo()
235 234 self._item.update(ctxs)
236 235
237 236 def datahint(self):
238 237 '''set of field names to be referenced'''
239 238 return set()
240 239
241 240 def data(self, **data):
242 241 '''insert data into item that's not shown in default output'''
243 242 data = pycompat.byteskwargs(data)
244 243 self._item.update(data)
245 244
246 245 def write(self, fields, deftext, *fielddata, **opts):
247 246 '''do default text output while assigning data to item'''
248 247 fieldkeys = fields.split()
249 248 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
250 249 self._item.update(zip(fieldkeys, fielddata))
251 250
252 251 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
253 252 '''do conditional write (primarily for plain formatter)'''
254 253 fieldkeys = fields.split()
255 254 assert len(fieldkeys) == len(fielddata)
256 255 self._item.update(zip(fieldkeys, fielddata))
257 256
258 257 def plain(self, text, **opts):
259 258 '''show raw text for non-templated mode'''
260 259
261 260 def isplain(self):
262 261 '''check for plain formatter usage'''
263 262 return False
264 263
265 264 def nested(self, field, tmpl=None, sep=b''):
266 265 '''sub formatter to store nested data in the specified field'''
267 266 data = []
268 267 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
269 268 return _nestedformatter(self._ui, self._converter, data)
270 269
271 270 def end(self):
272 271 '''end output for the formatter'''
273 272 if self._item is not None:
274 273 self._showitem()
275 274
276 275
277 276 def nullformatter(ui, topic, opts):
278 277 '''formatter that prints nothing'''
279 278 return baseformatter(ui, topic, opts, converter=_nullconverter)
280 279
281 280
282 281 class _nestedformatter(baseformatter):
283 282 '''build sub items and store them in the parent formatter'''
284 283
285 284 def __init__(self, ui, converter, data):
286 285 baseformatter.__init__(
287 286 self, ui, topic=b'', opts={}, converter=converter
288 287 )
289 288 self._data = data
290 289
291 290 def _showitem(self):
292 291 self._data.append(self._item)
293 292
294 293
295 294 def _iteritems(data):
296 295 '''iterate key-value pairs in stable order'''
297 296 if isinstance(data, dict):
298 297 return sorted(pycompat.iteritems(data))
299 298 return data
300 299
301 300
302 301 class _plainconverter(object):
303 302 '''convert non-primitive data types to text'''
304 303
305 304 storecontext = False
306 305
307 306 @staticmethod
308 307 def wrapnested(data, tmpl, sep):
309 308 raise error.ProgrammingError(b'plainformatter should never be nested')
310 309
311 310 @staticmethod
312 311 def formatdate(date, fmt):
313 312 '''stringify date tuple in the given format'''
314 313 return dateutil.datestr(date, fmt)
315 314
316 315 @staticmethod
317 316 def formatdict(data, key, value, fmt, sep):
318 317 '''stringify key-value pairs separated by sep'''
319 318 prefmt = pycompat.identity
320 319 if fmt is None:
321 320 fmt = b'%s=%s'
322 321 prefmt = pycompat.bytestr
323 322 return sep.join(
324 323 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
325 324 )
326 325
327 326 @staticmethod
328 327 def formatlist(data, name, fmt, sep):
329 328 '''stringify iterable separated by sep'''
330 329 prefmt = pycompat.identity
331 330 if fmt is None:
332 331 fmt = b'%s'
333 332 prefmt = pycompat.bytestr
334 333 return sep.join(fmt % prefmt(e) for e in data)
335 334
336 335
337 336 class plainformatter(baseformatter):
338 337 '''the default text output scheme'''
339 338
340 339 def __init__(self, ui, out, topic, opts):
341 340 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
342 341 if ui.debugflag:
343 342 self.hexfunc = hex
344 343 else:
345 344 self.hexfunc = short
346 345 if ui is out:
347 346 self._write = ui.write
348 347 else:
349 348 self._write = lambda s, **opts: out.write(s)
350 349
351 350 def startitem(self):
352 351 pass
353 352
354 353 def data(self, **data):
355 354 pass
356 355
357 356 def write(self, fields, deftext, *fielddata, **opts):
358 357 self._write(deftext % fielddata, **opts)
359 358
360 359 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
361 360 '''do conditional write'''
362 361 if cond:
363 362 self._write(deftext % fielddata, **opts)
364 363
365 364 def plain(self, text, **opts):
366 365 self._write(text, **opts)
367 366
368 367 def isplain(self):
369 368 return True
370 369
371 370 def nested(self, field, tmpl=None, sep=b''):
372 371 # nested data will be directly written to ui
373 372 return self
374 373
375 374 def end(self):
376 375 pass
377 376
378 377
379 378 class debugformatter(baseformatter):
380 379 def __init__(self, ui, out, topic, opts):
381 380 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
382 381 self._out = out
383 382 self._out.write(b"%s = [\n" % self._topic)
384 383
385 384 def _showitem(self):
386 385 self._out.write(
387 386 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
388 387 )
389 388
390 389 def end(self):
391 390 baseformatter.end(self)
392 391 self._out.write(b"]\n")
393 392
394 393
395 394 class pickleformatter(baseformatter):
396 395 def __init__(self, ui, out, topic, opts):
397 396 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
398 397 self._out = out
399 398 self._data = []
400 399
401 400 def _showitem(self):
402 401 self._data.append(self._item)
403 402
404 403 def end(self):
405 404 baseformatter.end(self)
406 405 self._out.write(pickle.dumps(self._data))
407 406
408 407
409 408 class cborformatter(baseformatter):
410 409 '''serialize items as an indefinite-length CBOR array'''
411 410
412 411 def __init__(self, ui, out, topic, opts):
413 412 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
414 413 self._out = out
415 414 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
416 415
417 416 def _showitem(self):
418 417 self._out.write(b''.join(cborutil.streamencode(self._item)))
419 418
420 419 def end(self):
421 420 baseformatter.end(self)
422 421 self._out.write(cborutil.BREAK)
423 422
424 423
425 424 class jsonformatter(baseformatter):
426 425
427 426 strict_format = True
428 427
429 428 def __init__(self, ui, out, topic, opts):
430 429 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
431 430 self._out = out
432 431 self._out.write(b"[")
433 432 self._first = True
434 433
435 434 def _showitem(self):
436 435 if self._first:
437 436 self._first = False
438 437 else:
439 438 self._out.write(b",")
440 439
441 440 self._out.write(b"\n {\n")
442 441 first = True
443 442 for k, v in sorted(self._item.items()):
444 443 if first:
445 444 first = False
446 445 else:
447 446 self._out.write(b",\n")
448 447 u = templatefilters.json(v, paranoid=False)
449 448 self._out.write(b' "%s": %s' % (k, u))
450 449 self._out.write(b"\n }")
451 450
452 451 def end(self):
453 452 baseformatter.end(self)
454 453 self._out.write(b"\n]\n")
455 454
456 455
457 456 class _templateconverter(object):
458 457 '''convert non-primitive data types to be processed by templater'''
459 458
460 459 storecontext = True
461 460
462 461 @staticmethod
463 462 def wrapnested(data, tmpl, sep):
464 463 '''wrap nested data by templatable type'''
465 464 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
466 465
467 466 @staticmethod
468 467 def formatdate(date, fmt):
469 468 '''return date tuple'''
470 469 return templateutil.date(date)
471 470
472 471 @staticmethod
473 472 def formatdict(data, key, value, fmt, sep):
474 473 '''build object that can be evaluated as either plain string or dict'''
475 474 data = util.sortdict(_iteritems(data))
476 475
477 476 def f():
478 477 yield _plainconverter.formatdict(data, key, value, fmt, sep)
479 478
480 479 return templateutil.hybriddict(
481 480 data, key=key, value=value, fmt=fmt, gen=f
482 481 )
483 482
484 483 @staticmethod
485 484 def formatlist(data, name, fmt, sep):
486 485 '''build object that can be evaluated as either plain string or list'''
487 486 data = list(data)
488 487
489 488 def f():
490 489 yield _plainconverter.formatlist(data, name, fmt, sep)
491 490
492 491 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
493 492
494 493
495 494 class templateformatter(baseformatter):
496 495 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
497 496 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
498 497 self._out = out
499 498 self._tref = spec.ref
500 499 self._t = loadtemplater(
501 500 ui,
502 501 spec,
503 502 defaults=templatekw.keywords,
504 503 resources=templateresources(ui),
505 504 cache=templatekw.defaulttempl,
506 505 )
507 506 if overridetemplates:
508 507 self._t.cache.update(overridetemplates)
509 508 self._parts = templatepartsmap(
510 509 spec, self._t, [b'docheader', b'docfooter', b'separator']
511 510 )
512 511 self._counter = itertools.count()
513 512 self._renderitem(b'docheader', {})
514 513
515 514 def _showitem(self):
516 515 item = self._item.copy()
517 516 item[b'index'] = index = next(self._counter)
518 517 if index > 0:
519 518 self._renderitem(b'separator', {})
520 519 self._renderitem(self._tref, item)
521 520
522 521 def _renderitem(self, part, item):
523 522 if part not in self._parts:
524 523 return
525 524 ref = self._parts[part]
526 525 # None can't be put in the mapping dict since it means <unset>
527 526 for k, v in item.items():
528 527 if v is None:
529 528 item[k] = templateutil.wrappedvalue(v)
530 529 self._out.write(self._t.render(ref, item))
531 530
532 531 @util.propertycache
533 532 def _symbolsused(self):
534 533 return self._t.symbolsused(self._tref)
535 534
536 535 def datahint(self):
537 536 '''set of field names to be referenced from the template'''
538 537 return self._symbolsused[0]
539 538
540 539 def end(self):
541 540 baseformatter.end(self)
542 541 self._renderitem(b'docfooter', {})
543 542
544 543
545 544 @attr.s(frozen=True)
546 545 class templatespec(object):
547 546 ref = attr.ib()
548 547 tmpl = attr.ib()
549 548 mapfile = attr.ib()
550 549 refargs = attr.ib(default=None)
551 550 fp = attr.ib(default=None)
552 551
553 552
554 553 def empty_templatespec():
555 554 return templatespec(None, None, None)
556 555
557 556
558 557 def reference_templatespec(ref, refargs=None):
559 558 return templatespec(ref, None, None, refargs)
560 559
561 560
562 561 def literal_templatespec(tmpl):
563 562 if pycompat.ispy3:
564 563 assert not isinstance(tmpl, str), b'tmpl must not be a str'
565 564 return templatespec(b'', tmpl, None)
566 565
567 566
568 567 def mapfile_templatespec(topic, mapfile, fp=None):
569 568 return templatespec(topic, None, mapfile, fp=fp)
570 569
571 570
572 571 def lookuptemplate(ui, topic, tmpl):
573 572 """Find the template matching the given -T/--template spec 'tmpl'
574 573
575 574 'tmpl' can be any of the following:
576 575
577 576 - a literal template (e.g. '{rev}')
578 577 - a reference to built-in template (i.e. formatter)
579 578 - a map-file name or path (e.g. 'changelog')
580 579 - a reference to [templates] in config file
581 580 - a path to raw template file
582 581
583 582 A map file defines a stand-alone template environment. If a map file
584 583 selected, all templates defined in the file will be loaded, and the
585 584 template matching the given topic will be rendered. Aliases won't be
586 585 loaded from user config, but from the map file.
587 586
588 587 If no map file selected, all templates in [templates] section will be
589 588 available as well as aliases in [templatealias].
590 589 """
591 590
592 591 if not tmpl:
593 592 return empty_templatespec()
594 593
595 594 # looks like a literal template?
596 595 if b'{' in tmpl:
597 596 return literal_templatespec(tmpl)
598 597
599 598 # a reference to built-in (formatter) template
600 599 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
601 600 return reference_templatespec(tmpl)
602 601
603 602 # a function-style reference to built-in template
604 603 func, fsep, ftail = tmpl.partition(b'(')
605 604 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
606 605 templater.parseexpr(tmpl) # make sure syntax errors are confined
607 606 return reference_templatespec(func, refargs=ftail[:-1])
608 607
609 608 # perhaps a stock style?
610 609 if not os.path.split(tmpl)[0]:
611 610 (mapname, fp) = templater.try_open_template(
612 611 b'map-cmdline.' + tmpl
613 612 ) or templater.try_open_template(tmpl)
614 613 if mapname:
615 614 return mapfile_templatespec(topic, mapname, fp)
616 615
617 616 # perhaps it's a reference to [templates]
618 617 if ui.config(b'templates', tmpl):
619 618 return reference_templatespec(tmpl)
620 619
621 620 if tmpl == b'list':
622 621 ui.write(_(b"available styles: %s\n") % templater.stylelist())
623 622 raise error.Abort(_(b"specify a template"))
624 623
625 624 # perhaps it's a path to a map or a template
626 625 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
627 626 # is it a mapfile for a style?
628 627 if os.path.basename(tmpl).startswith(b"map-"):
629 628 return mapfile_templatespec(topic, os.path.realpath(tmpl))
630 629 with util.posixfile(tmpl, b'rb') as f:
631 630 tmpl = f.read()
632 631 return literal_templatespec(tmpl)
633 632
634 633 # constant string?
635 634 return literal_templatespec(tmpl)
636 635
637 636
638 637 def templatepartsmap(spec, t, partnames):
639 638 """Create a mapping of {part: ref}"""
640 639 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
641 640 if spec.mapfile:
642 641 partsmap.update((p, p) for p in partnames if p in t)
643 642 elif spec.ref:
644 643 for part in partnames:
645 644 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
646 645 if ref in t:
647 646 partsmap[part] = ref
648 647 return partsmap
649 648
650 649
651 650 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
652 651 """Create a templater from either a literal template or loading from
653 652 a map file"""
654 653 assert not (spec.tmpl and spec.mapfile)
655 654 if spec.mapfile:
656 655 return templater.templater.frommapfile(
657 656 spec.mapfile,
658 657 spec.fp,
659 658 defaults=defaults,
660 659 resources=resources,
661 660 cache=cache,
662 661 )
663 662 return maketemplater(
664 663 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
665 664 )
666 665
667 666
668 667 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
669 668 """Create a templater from a string template 'tmpl'"""
670 669 aliases = ui.configitems(b'templatealias')
671 670 t = templater.templater(
672 671 defaults=defaults, resources=resources, cache=cache, aliases=aliases
673 672 )
674 673 t.cache.update(
675 674 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
676 675 )
677 676 if tmpl:
678 677 t.cache[b''] = tmpl
679 678 return t
680 679
681 680
682 681 # marker to denote a resource to be loaded on demand based on mapping values
683 682 # (e.g. (ctx, path) -> fctx)
684 683 _placeholder = object()
685 684
686 685
687 686 class templateresources(templater.resourcemapper):
688 687 """Resource mapper designed for the default templatekw and function"""
689 688
690 689 def __init__(self, ui, repo=None):
691 690 self._resmap = {
692 691 b'cache': {}, # for templatekw/funcs to store reusable data
693 692 b'repo': repo,
694 693 b'ui': ui,
695 694 }
696 695
697 696 def availablekeys(self, mapping):
698 697 return {
699 698 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
700 699 }
701 700
702 701 def knownkeys(self):
703 702 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
704 703
705 704 def lookup(self, mapping, key):
706 705 if key not in self.knownkeys():
707 706 return None
708 707 v = self._getsome(mapping, key)
709 708 if v is _placeholder:
710 709 v = mapping[key] = self._loadermap[key](self, mapping)
711 710 return v
712 711
713 712 def populatemap(self, context, origmapping, newmapping):
714 713 mapping = {}
715 714 if self._hasnodespec(newmapping):
716 715 mapping[b'revcache'] = {} # per-ctx cache
717 716 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
718 717 orignode = templateutil.runsymbol(context, origmapping, b'node')
719 718 mapping[b'originalnode'] = orignode
720 719 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
721 720 # its existence to be reported by availablekeys()
722 721 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
723 722 mapping[b'ctx'] = _placeholder
724 723 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
725 724 mapping[b'fctx'] = _placeholder
726 725 return mapping
727 726
728 727 def _getsome(self, mapping, key):
729 728 v = mapping.get(key)
730 729 if v is not None:
731 730 return v
732 731 return self._resmap.get(key)
733 732
734 733 def _hasliteral(self, mapping, key):
735 734 """Test if a literal value is set or unset in the given mapping"""
736 735 return key in mapping and not callable(mapping[key])
737 736
738 737 def _getliteral(self, mapping, key):
739 738 """Return value of the given name if it is a literal"""
740 739 v = mapping.get(key)
741 740 if callable(v):
742 741 return None
743 742 return v
744 743
745 744 def _hasnodespec(self, mapping):
746 745 """Test if context revision is set or unset in the given mapping"""
747 746 return b'node' in mapping or b'ctx' in mapping
748 747
749 748 def _loadctx(self, mapping):
750 749 repo = self._getsome(mapping, b'repo')
751 750 node = self._getliteral(mapping, b'node')
752 751 if repo is None or node is None:
753 752 return
754 753 try:
755 754 return repo[node]
756 755 except error.RepoLookupError:
757 756 return None # maybe hidden/non-existent node
758 757
759 758 def _loadfctx(self, mapping):
760 759 ctx = self._getsome(mapping, b'ctx')
761 760 path = self._getliteral(mapping, b'path')
762 761 if ctx is None or path is None:
763 762 return None
764 763 try:
765 764 return ctx[path]
766 765 except error.LookupError:
767 766 return None # maybe removed file?
768 767
769 768 _loadermap = {
770 769 b'ctx': _loadctx,
771 770 b'fctx': _loadfctx,
772 771 }
773 772
774 773
775 774 def _internaltemplateformatter(
776 775 ui,
777 776 out,
778 777 topic,
779 778 opts,
780 779 spec,
781 780 tmpl,
782 781 docheader=b'',
783 782 docfooter=b'',
784 783 separator=b'',
785 784 ):
786 785 """Build template formatter that handles customizable built-in templates
787 786 such as -Tjson(...)"""
788 787 templates = {spec.ref: tmpl}
789 788 if docheader:
790 789 templates[b'%s:docheader' % spec.ref] = docheader
791 790 if docfooter:
792 791 templates[b'%s:docfooter' % spec.ref] = docfooter
793 792 if separator:
794 793 templates[b'%s:separator' % spec.ref] = separator
795 794 return templateformatter(
796 795 ui, out, topic, opts, spec, overridetemplates=templates
797 796 )
798 797
799 798
800 799 def formatter(ui, out, topic, opts):
801 800 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
802 801 if spec.ref == b"cbor" and spec.refargs is not None:
803 802 return _internaltemplateformatter(
804 803 ui,
805 804 out,
806 805 topic,
807 806 opts,
808 807 spec,
809 808 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
810 809 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
811 810 docfooter=cborutil.BREAK,
812 811 )
813 812 elif spec.ref == b"cbor":
814 813 return cborformatter(ui, out, topic, opts)
815 814 elif spec.ref == b"json" and spec.refargs is not None:
816 815 return _internaltemplateformatter(
817 816 ui,
818 817 out,
819 818 topic,
820 819 opts,
821 820 spec,
822 821 tmpl=b'{dict(%s)|json}' % spec.refargs,
823 822 docheader=b'[\n ',
824 823 docfooter=b'\n]\n',
825 824 separator=b',\n ',
826 825 )
827 826 elif spec.ref == b"json":
828 827 return jsonformatter(ui, out, topic, opts)
829 828 elif spec.ref == b"pickle":
830 829 assert spec.refargs is None, r'function-style not supported'
831 830 return pickleformatter(ui, out, topic, opts)
832 831 elif spec.ref == b"debug":
833 832 assert spec.refargs is None, r'function-style not supported'
834 833 return debugformatter(ui, out, topic, opts)
835 834 elif spec.ref or spec.tmpl or spec.mapfile:
836 835 assert spec.refargs is None, r'function-style not supported'
837 836 return templateformatter(ui, out, topic, opts, spec)
838 837 # developer config: ui.formatdebug
839 838 elif ui.configbool(b'ui', b'formatdebug'):
840 839 return debugformatter(ui, out, topic, opts)
841 840 # deprecated config: ui.formatjson
842 841 elif ui.configbool(b'ui', b'formatjson'):
843 842 return jsonformatter(ui, out, topic, opts)
844 843 return plainformatter(ui, out, topic, opts)
845 844
846 845
847 846 @contextlib.contextmanager
848 847 def openformatter(ui, filename, topic, opts):
849 848 """Create a formatter that writes outputs to the specified file
850 849
851 850 Must be invoked using the 'with' statement.
852 851 """
853 852 with util.posixfile(filename, b'wb') as out:
854 853 with formatter(ui, out, topic, opts) as fm:
855 854 yield fm
856 855
857 856
858 857 @contextlib.contextmanager
859 858 def _neverending(fm):
860 859 yield fm
861 860
862 861
863 862 def maybereopen(fm, filename):
864 863 """Create a formatter backed by file if filename specified, else return
865 864 the given formatter
866 865
867 866 Must be invoked using the 'with' statement. This will never call fm.end()
868 867 of the given formatter.
869 868 """
870 869 if filename:
871 870 return openformatter(fm._ui, filename, fm._topic, fm._opts)
872 871 else:
873 872 return _neverending(fm)
@@ -1,3361 +1,3360 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import, print_function
17 17
18 18 import abc
19 19 import collections
20 20 import contextlib
21 21 import errno
22 22 import gc
23 23 import hashlib
24 24 import itertools
25 25 import locale
26 26 import mmap
27 27 import os
28 28 import platform as pyplatform
29 29 import re as remod
30 30 import shutil
31 31 import stat
32 32 import sys
33 33 import time
34 34 import traceback
35 35 import warnings
36 36
37 37 from .node import hex
38 38 from .thirdparty import attr
39 39 from .pycompat import (
40 40 delattr,
41 41 getattr,
42 42 open,
43 43 setattr,
44 44 )
45 45 from .node import hex
46 46 from hgdemandimport import tracing
47 47 from . import (
48 48 encoding,
49 49 error,
50 50 i18n,
51 51 policy,
52 52 pycompat,
53 53 urllibcompat,
54 54 )
55 55 from .utils import (
56 56 compression,
57 57 hashutil,
58 58 procutil,
59 59 stringutil,
60 60 )
61 61
62 62 if pycompat.TYPE_CHECKING:
63 63 from typing import (
64 64 Iterator,
65 65 List,
66 66 Optional,
67 67 Tuple,
68 68 )
69 69
70 70
71 71 base85 = policy.importmod('base85')
72 72 osutil = policy.importmod('osutil')
73 73
74 74 b85decode = base85.b85decode
75 75 b85encode = base85.b85encode
76 76
77 77 cookielib = pycompat.cookielib
78 78 httplib = pycompat.httplib
79 pickle = pycompat.pickle
80 79 safehasattr = pycompat.safehasattr
81 80 socketserver = pycompat.socketserver
82 81 bytesio = pycompat.bytesio
83 82 # TODO deprecate stringio name, as it is a lie on Python 3.
84 83 stringio = bytesio
85 84 xmlrpclib = pycompat.xmlrpclib
86 85
87 86 httpserver = urllibcompat.httpserver
88 87 urlerr = urllibcompat.urlerr
89 88 urlreq = urllibcompat.urlreq
90 89
91 90 # workaround for win32mbcs
92 91 _filenamebytestr = pycompat.bytestr
93 92
94 93 if pycompat.iswindows:
95 94 from . import windows as platform
96 95 else:
97 96 from . import posix as platform
98 97
99 98 _ = i18n._
100 99
101 100 abspath = platform.abspath
102 101 bindunixsocket = platform.bindunixsocket
103 102 cachestat = platform.cachestat
104 103 checkexec = platform.checkexec
105 104 checklink = platform.checklink
106 105 copymode = platform.copymode
107 106 expandglobs = platform.expandglobs
108 107 getfsmountpoint = platform.getfsmountpoint
109 108 getfstype = platform.getfstype
110 109 get_password = platform.get_password
111 110 groupmembers = platform.groupmembers
112 111 groupname = platform.groupname
113 112 isexec = platform.isexec
114 113 isowner = platform.isowner
115 114 listdir = osutil.listdir
116 115 localpath = platform.localpath
117 116 lookupreg = platform.lookupreg
118 117 makedir = platform.makedir
119 118 nlinks = platform.nlinks
120 119 normpath = platform.normpath
121 120 normcase = platform.normcase
122 121 normcasespec = platform.normcasespec
123 122 normcasefallback = platform.normcasefallback
124 123 openhardlinks = platform.openhardlinks
125 124 oslink = platform.oslink
126 125 parsepatchoutput = platform.parsepatchoutput
127 126 pconvert = platform.pconvert
128 127 poll = platform.poll
129 128 posixfile = platform.posixfile
130 129 readlink = platform.readlink
131 130 rename = platform.rename
132 131 removedirs = platform.removedirs
133 132 samedevice = platform.samedevice
134 133 samefile = platform.samefile
135 134 samestat = platform.samestat
136 135 setflags = platform.setflags
137 136 split = platform.split
138 137 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
139 138 statisexec = platform.statisexec
140 139 statislink = platform.statislink
141 140 umask = platform.umask
142 141 unlink = platform.unlink
143 142 username = platform.username
144 143
145 144
146 145 def setumask(val):
147 146 # type: (int) -> None
148 147 '''updates the umask. used by chg server'''
149 148 if pycompat.iswindows:
150 149 return
151 150 os.umask(val)
152 151 global umask
153 152 platform.umask = umask = val & 0o777
154 153
155 154
156 155 # small compat layer
157 156 compengines = compression.compengines
158 157 SERVERROLE = compression.SERVERROLE
159 158 CLIENTROLE = compression.CLIENTROLE
160 159
161 160 try:
162 161 recvfds = osutil.recvfds
163 162 except AttributeError:
164 163 pass
165 164
166 165 # Python compatibility
167 166
168 167 _notset = object()
169 168
170 169
171 170 def bitsfrom(container):
172 171 bits = 0
173 172 for bit in container:
174 173 bits |= bit
175 174 return bits
176 175
177 176
178 177 # python 2.6 still have deprecation warning enabled by default. We do not want
179 178 # to display anything to standard user so detect if we are running test and
180 179 # only use python deprecation warning in this case.
181 180 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
182 181 if _dowarn:
183 182 # explicitly unfilter our warning for python 2.7
184 183 #
185 184 # The option of setting PYTHONWARNINGS in the test runner was investigated.
186 185 # However, module name set through PYTHONWARNINGS was exactly matched, so
187 186 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
188 187 # makes the whole PYTHONWARNINGS thing useless for our usecase.
189 188 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
190 189 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
191 190 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
192 191 if _dowarn and pycompat.ispy3:
193 192 # silence warning emitted by passing user string to re.sub()
194 193 warnings.filterwarnings(
195 194 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
196 195 )
197 196 warnings.filterwarnings(
198 197 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
199 198 )
200 199 # TODO: reinvent imp.is_frozen()
201 200 warnings.filterwarnings(
202 201 'ignore',
203 202 'the imp module is deprecated',
204 203 DeprecationWarning,
205 204 'mercurial',
206 205 )
207 206
208 207
209 208 def nouideprecwarn(msg, version, stacklevel=1):
210 209 """Issue an python native deprecation warning
211 210
212 211 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
213 212 """
214 213 if _dowarn:
215 214 msg += (
216 215 b"\n(compatibility will be dropped after Mercurial-%s,"
217 216 b" update your code.)"
218 217 ) % version
219 218 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
220 219 # on python 3 with chg, we will need to explicitly flush the output
221 220 sys.stderr.flush()
222 221
223 222
224 223 DIGESTS = {
225 224 b'md5': hashlib.md5,
226 225 b'sha1': hashutil.sha1,
227 226 b'sha512': hashlib.sha512,
228 227 }
229 228 # List of digest types from strongest to weakest
230 229 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
231 230
232 231 for k in DIGESTS_BY_STRENGTH:
233 232 assert k in DIGESTS
234 233
235 234
236 235 class digester(object):
237 236 """helper to compute digests.
238 237
239 238 This helper can be used to compute one or more digests given their name.
240 239
241 240 >>> d = digester([b'md5', b'sha1'])
242 241 >>> d.update(b'foo')
243 242 >>> [k for k in sorted(d)]
244 243 ['md5', 'sha1']
245 244 >>> d[b'md5']
246 245 'acbd18db4cc2f85cedef654fccc4a4d8'
247 246 >>> d[b'sha1']
248 247 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
249 248 >>> digester.preferred([b'md5', b'sha1'])
250 249 'sha1'
251 250 """
252 251
253 252 def __init__(self, digests, s=b''):
254 253 self._hashes = {}
255 254 for k in digests:
256 255 if k not in DIGESTS:
257 256 raise error.Abort(_(b'unknown digest type: %s') % k)
258 257 self._hashes[k] = DIGESTS[k]()
259 258 if s:
260 259 self.update(s)
261 260
262 261 def update(self, data):
263 262 for h in self._hashes.values():
264 263 h.update(data)
265 264
266 265 def __getitem__(self, key):
267 266 if key not in DIGESTS:
268 267 raise error.Abort(_(b'unknown digest type: %s') % k)
269 268 return hex(self._hashes[key].digest())
270 269
271 270 def __iter__(self):
272 271 return iter(self._hashes)
273 272
274 273 @staticmethod
275 274 def preferred(supported):
276 275 """returns the strongest digest type in both supported and DIGESTS."""
277 276
278 277 for k in DIGESTS_BY_STRENGTH:
279 278 if k in supported:
280 279 return k
281 280 return None
282 281
283 282
284 283 class digestchecker(object):
285 284 """file handle wrapper that additionally checks content against a given
286 285 size and digests.
287 286
288 287 d = digestchecker(fh, size, {'md5': '...'})
289 288
290 289 When multiple digests are given, all of them are validated.
291 290 """
292 291
293 292 def __init__(self, fh, size, digests):
294 293 self._fh = fh
295 294 self._size = size
296 295 self._got = 0
297 296 self._digests = dict(digests)
298 297 self._digester = digester(self._digests.keys())
299 298
300 299 def read(self, length=-1):
301 300 content = self._fh.read(length)
302 301 self._digester.update(content)
303 302 self._got += len(content)
304 303 return content
305 304
306 305 def validate(self):
307 306 if self._size != self._got:
308 307 raise error.Abort(
309 308 _(b'size mismatch: expected %d, got %d')
310 309 % (self._size, self._got)
311 310 )
312 311 for k, v in self._digests.items():
313 312 if v != self._digester[k]:
314 313 # i18n: first parameter is a digest name
315 314 raise error.Abort(
316 315 _(b'%s mismatch: expected %s, got %s')
317 316 % (k, v, self._digester[k])
318 317 )
319 318
320 319
321 320 try:
322 321 buffer = buffer # pytype: disable=name-error
323 322 except NameError:
324 323
325 324 def buffer(sliceable, offset=0, length=None):
326 325 if length is not None:
327 326 return memoryview(sliceable)[offset : offset + length]
328 327 return memoryview(sliceable)[offset:]
329 328
330 329
331 330 _chunksize = 4096
332 331
333 332
334 333 class bufferedinputpipe(object):
335 334 """a manually buffered input pipe
336 335
337 336 Python will not let us use buffered IO and lazy reading with 'polling' at
338 337 the same time. We cannot probe the buffer state and select will not detect
339 338 that data are ready to read if they are already buffered.
340 339
341 340 This class let us work around that by implementing its own buffering
342 341 (allowing efficient readline) while offering a way to know if the buffer is
343 342 empty from the output (allowing collaboration of the buffer with polling).
344 343
345 344 This class lives in the 'util' module because it makes use of the 'os'
346 345 module from the python stdlib.
347 346 """
348 347
349 348 def __new__(cls, fh):
350 349 # If we receive a fileobjectproxy, we need to use a variation of this
351 350 # class that notifies observers about activity.
352 351 if isinstance(fh, fileobjectproxy):
353 352 cls = observedbufferedinputpipe
354 353
355 354 return super(bufferedinputpipe, cls).__new__(cls)
356 355
357 356 def __init__(self, input):
358 357 self._input = input
359 358 self._buffer = []
360 359 self._eof = False
361 360 self._lenbuf = 0
362 361
363 362 @property
364 363 def hasbuffer(self):
365 364 """True is any data is currently buffered
366 365
367 366 This will be used externally a pre-step for polling IO. If there is
368 367 already data then no polling should be set in place."""
369 368 return bool(self._buffer)
370 369
371 370 @property
372 371 def closed(self):
373 372 return self._input.closed
374 373
375 374 def fileno(self):
376 375 return self._input.fileno()
377 376
378 377 def close(self):
379 378 return self._input.close()
380 379
381 380 def read(self, size):
382 381 while (not self._eof) and (self._lenbuf < size):
383 382 self._fillbuffer()
384 383 return self._frombuffer(size)
385 384
386 385 def unbufferedread(self, size):
387 386 if not self._eof and self._lenbuf == 0:
388 387 self._fillbuffer(max(size, _chunksize))
389 388 return self._frombuffer(min(self._lenbuf, size))
390 389
391 390 def readline(self, *args, **kwargs):
392 391 if len(self._buffer) > 1:
393 392 # this should not happen because both read and readline end with a
394 393 # _frombuffer call that collapse it.
395 394 self._buffer = [b''.join(self._buffer)]
396 395 self._lenbuf = len(self._buffer[0])
397 396 lfi = -1
398 397 if self._buffer:
399 398 lfi = self._buffer[-1].find(b'\n')
400 399 while (not self._eof) and lfi < 0:
401 400 self._fillbuffer()
402 401 if self._buffer:
403 402 lfi = self._buffer[-1].find(b'\n')
404 403 size = lfi + 1
405 404 if lfi < 0: # end of file
406 405 size = self._lenbuf
407 406 elif len(self._buffer) > 1:
408 407 # we need to take previous chunks into account
409 408 size += self._lenbuf - len(self._buffer[-1])
410 409 return self._frombuffer(size)
411 410
412 411 def _frombuffer(self, size):
413 412 """return at most 'size' data from the buffer
414 413
415 414 The data are removed from the buffer."""
416 415 if size == 0 or not self._buffer:
417 416 return b''
418 417 buf = self._buffer[0]
419 418 if len(self._buffer) > 1:
420 419 buf = b''.join(self._buffer)
421 420
422 421 data = buf[:size]
423 422 buf = buf[len(data) :]
424 423 if buf:
425 424 self._buffer = [buf]
426 425 self._lenbuf = len(buf)
427 426 else:
428 427 self._buffer = []
429 428 self._lenbuf = 0
430 429 return data
431 430
432 431 def _fillbuffer(self, size=_chunksize):
433 432 """read data to the buffer"""
434 433 data = os.read(self._input.fileno(), size)
435 434 if not data:
436 435 self._eof = True
437 436 else:
438 437 self._lenbuf += len(data)
439 438 self._buffer.append(data)
440 439
441 440 return data
442 441
443 442
444 443 def mmapread(fp, size=None):
445 444 if size == 0:
446 445 # size of 0 to mmap.mmap() means "all data"
447 446 # rather than "zero bytes", so special case that.
448 447 return b''
449 448 elif size is None:
450 449 size = 0
451 450 fd = getattr(fp, 'fileno', lambda: fp)()
452 451 try:
453 452 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
454 453 except ValueError:
455 454 # Empty files cannot be mmapped, but mmapread should still work. Check
456 455 # if the file is empty, and if so, return an empty buffer.
457 456 if os.fstat(fd).st_size == 0:
458 457 return b''
459 458 raise
460 459
461 460
462 461 class fileobjectproxy(object):
463 462 """A proxy around file objects that tells a watcher when events occur.
464 463
465 464 This type is intended to only be used for testing purposes. Think hard
466 465 before using it in important code.
467 466 """
468 467
469 468 __slots__ = (
470 469 '_orig',
471 470 '_observer',
472 471 )
473 472
474 473 def __init__(self, fh, observer):
475 474 object.__setattr__(self, '_orig', fh)
476 475 object.__setattr__(self, '_observer', observer)
477 476
478 477 def __getattribute__(self, name):
479 478 ours = {
480 479 '_observer',
481 480 # IOBase
482 481 'close',
483 482 # closed if a property
484 483 'fileno',
485 484 'flush',
486 485 'isatty',
487 486 'readable',
488 487 'readline',
489 488 'readlines',
490 489 'seek',
491 490 'seekable',
492 491 'tell',
493 492 'truncate',
494 493 'writable',
495 494 'writelines',
496 495 # RawIOBase
497 496 'read',
498 497 'readall',
499 498 'readinto',
500 499 'write',
501 500 # BufferedIOBase
502 501 # raw is a property
503 502 'detach',
504 503 # read defined above
505 504 'read1',
506 505 # readinto defined above
507 506 # write defined above
508 507 }
509 508
510 509 # We only observe some methods.
511 510 if name in ours:
512 511 return object.__getattribute__(self, name)
513 512
514 513 return getattr(object.__getattribute__(self, '_orig'), name)
515 514
516 515 def __nonzero__(self):
517 516 return bool(object.__getattribute__(self, '_orig'))
518 517
519 518 __bool__ = __nonzero__
520 519
521 520 def __delattr__(self, name):
522 521 return delattr(object.__getattribute__(self, '_orig'), name)
523 522
524 523 def __setattr__(self, name, value):
525 524 return setattr(object.__getattribute__(self, '_orig'), name, value)
526 525
527 526 def __iter__(self):
528 527 return object.__getattribute__(self, '_orig').__iter__()
529 528
530 529 def _observedcall(self, name, *args, **kwargs):
531 530 # Call the original object.
532 531 orig = object.__getattribute__(self, '_orig')
533 532 res = getattr(orig, name)(*args, **kwargs)
534 533
535 534 # Call a method on the observer of the same name with arguments
536 535 # so it can react, log, etc.
537 536 observer = object.__getattribute__(self, '_observer')
538 537 fn = getattr(observer, name, None)
539 538 if fn:
540 539 fn(res, *args, **kwargs)
541 540
542 541 return res
543 542
544 543 def close(self, *args, **kwargs):
545 544 return object.__getattribute__(self, '_observedcall')(
546 545 'close', *args, **kwargs
547 546 )
548 547
549 548 def fileno(self, *args, **kwargs):
550 549 return object.__getattribute__(self, '_observedcall')(
551 550 'fileno', *args, **kwargs
552 551 )
553 552
554 553 def flush(self, *args, **kwargs):
555 554 return object.__getattribute__(self, '_observedcall')(
556 555 'flush', *args, **kwargs
557 556 )
558 557
559 558 def isatty(self, *args, **kwargs):
560 559 return object.__getattribute__(self, '_observedcall')(
561 560 'isatty', *args, **kwargs
562 561 )
563 562
564 563 def readable(self, *args, **kwargs):
565 564 return object.__getattribute__(self, '_observedcall')(
566 565 'readable', *args, **kwargs
567 566 )
568 567
569 568 def readline(self, *args, **kwargs):
570 569 return object.__getattribute__(self, '_observedcall')(
571 570 'readline', *args, **kwargs
572 571 )
573 572
574 573 def readlines(self, *args, **kwargs):
575 574 return object.__getattribute__(self, '_observedcall')(
576 575 'readlines', *args, **kwargs
577 576 )
578 577
579 578 def seek(self, *args, **kwargs):
580 579 return object.__getattribute__(self, '_observedcall')(
581 580 'seek', *args, **kwargs
582 581 )
583 582
584 583 def seekable(self, *args, **kwargs):
585 584 return object.__getattribute__(self, '_observedcall')(
586 585 'seekable', *args, **kwargs
587 586 )
588 587
589 588 def tell(self, *args, **kwargs):
590 589 return object.__getattribute__(self, '_observedcall')(
591 590 'tell', *args, **kwargs
592 591 )
593 592
594 593 def truncate(self, *args, **kwargs):
595 594 return object.__getattribute__(self, '_observedcall')(
596 595 'truncate', *args, **kwargs
597 596 )
598 597
599 598 def writable(self, *args, **kwargs):
600 599 return object.__getattribute__(self, '_observedcall')(
601 600 'writable', *args, **kwargs
602 601 )
603 602
604 603 def writelines(self, *args, **kwargs):
605 604 return object.__getattribute__(self, '_observedcall')(
606 605 'writelines', *args, **kwargs
607 606 )
608 607
609 608 def read(self, *args, **kwargs):
610 609 return object.__getattribute__(self, '_observedcall')(
611 610 'read', *args, **kwargs
612 611 )
613 612
614 613 def readall(self, *args, **kwargs):
615 614 return object.__getattribute__(self, '_observedcall')(
616 615 'readall', *args, **kwargs
617 616 )
618 617
619 618 def readinto(self, *args, **kwargs):
620 619 return object.__getattribute__(self, '_observedcall')(
621 620 'readinto', *args, **kwargs
622 621 )
623 622
624 623 def write(self, *args, **kwargs):
625 624 return object.__getattribute__(self, '_observedcall')(
626 625 'write', *args, **kwargs
627 626 )
628 627
629 628 def detach(self, *args, **kwargs):
630 629 return object.__getattribute__(self, '_observedcall')(
631 630 'detach', *args, **kwargs
632 631 )
633 632
634 633 def read1(self, *args, **kwargs):
635 634 return object.__getattribute__(self, '_observedcall')(
636 635 'read1', *args, **kwargs
637 636 )
638 637
639 638
640 639 class observedbufferedinputpipe(bufferedinputpipe):
641 640 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
642 641
643 642 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
644 643 bypass ``fileobjectproxy``. Because of this, we need to make
645 644 ``bufferedinputpipe`` aware of these operations.
646 645
647 646 This variation of ``bufferedinputpipe`` can notify observers about
648 647 ``os.read()`` events. It also re-publishes other events, such as
649 648 ``read()`` and ``readline()``.
650 649 """
651 650
652 651 def _fillbuffer(self):
653 652 res = super(observedbufferedinputpipe, self)._fillbuffer()
654 653
655 654 fn = getattr(self._input._observer, 'osread', None)
656 655 if fn:
657 656 fn(res, _chunksize)
658 657
659 658 return res
660 659
661 660 # We use different observer methods because the operation isn't
662 661 # performed on the actual file object but on us.
663 662 def read(self, size):
664 663 res = super(observedbufferedinputpipe, self).read(size)
665 664
666 665 fn = getattr(self._input._observer, 'bufferedread', None)
667 666 if fn:
668 667 fn(res, size)
669 668
670 669 return res
671 670
672 671 def readline(self, *args, **kwargs):
673 672 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
674 673
675 674 fn = getattr(self._input._observer, 'bufferedreadline', None)
676 675 if fn:
677 676 fn(res)
678 677
679 678 return res
680 679
681 680
682 681 PROXIED_SOCKET_METHODS = {
683 682 'makefile',
684 683 'recv',
685 684 'recvfrom',
686 685 'recvfrom_into',
687 686 'recv_into',
688 687 'send',
689 688 'sendall',
690 689 'sendto',
691 690 'setblocking',
692 691 'settimeout',
693 692 'gettimeout',
694 693 'setsockopt',
695 694 }
696 695
697 696
698 697 class socketproxy(object):
699 698 """A proxy around a socket that tells a watcher when events occur.
700 699
701 700 This is like ``fileobjectproxy`` except for sockets.
702 701
703 702 This type is intended to only be used for testing purposes. Think hard
704 703 before using it in important code.
705 704 """
706 705
707 706 __slots__ = (
708 707 '_orig',
709 708 '_observer',
710 709 )
711 710
712 711 def __init__(self, sock, observer):
713 712 object.__setattr__(self, '_orig', sock)
714 713 object.__setattr__(self, '_observer', observer)
715 714
716 715 def __getattribute__(self, name):
717 716 if name in PROXIED_SOCKET_METHODS:
718 717 return object.__getattribute__(self, name)
719 718
720 719 return getattr(object.__getattribute__(self, '_orig'), name)
721 720
722 721 def __delattr__(self, name):
723 722 return delattr(object.__getattribute__(self, '_orig'), name)
724 723
725 724 def __setattr__(self, name, value):
726 725 return setattr(object.__getattribute__(self, '_orig'), name, value)
727 726
728 727 def __nonzero__(self):
729 728 return bool(object.__getattribute__(self, '_orig'))
730 729
731 730 __bool__ = __nonzero__
732 731
733 732 def _observedcall(self, name, *args, **kwargs):
734 733 # Call the original object.
735 734 orig = object.__getattribute__(self, '_orig')
736 735 res = getattr(orig, name)(*args, **kwargs)
737 736
738 737 # Call a method on the observer of the same name with arguments
739 738 # so it can react, log, etc.
740 739 observer = object.__getattribute__(self, '_observer')
741 740 fn = getattr(observer, name, None)
742 741 if fn:
743 742 fn(res, *args, **kwargs)
744 743
745 744 return res
746 745
747 746 def makefile(self, *args, **kwargs):
748 747 res = object.__getattribute__(self, '_observedcall')(
749 748 'makefile', *args, **kwargs
750 749 )
751 750
752 751 # The file object may be used for I/O. So we turn it into a
753 752 # proxy using our observer.
754 753 observer = object.__getattribute__(self, '_observer')
755 754 return makeloggingfileobject(
756 755 observer.fh,
757 756 res,
758 757 observer.name,
759 758 reads=observer.reads,
760 759 writes=observer.writes,
761 760 logdata=observer.logdata,
762 761 logdataapis=observer.logdataapis,
763 762 )
764 763
765 764 def recv(self, *args, **kwargs):
766 765 return object.__getattribute__(self, '_observedcall')(
767 766 'recv', *args, **kwargs
768 767 )
769 768
770 769 def recvfrom(self, *args, **kwargs):
771 770 return object.__getattribute__(self, '_observedcall')(
772 771 'recvfrom', *args, **kwargs
773 772 )
774 773
775 774 def recvfrom_into(self, *args, **kwargs):
776 775 return object.__getattribute__(self, '_observedcall')(
777 776 'recvfrom_into', *args, **kwargs
778 777 )
779 778
780 779 def recv_into(self, *args, **kwargs):
781 780 return object.__getattribute__(self, '_observedcall')(
782 781 'recv_info', *args, **kwargs
783 782 )
784 783
785 784 def send(self, *args, **kwargs):
786 785 return object.__getattribute__(self, '_observedcall')(
787 786 'send', *args, **kwargs
788 787 )
789 788
790 789 def sendall(self, *args, **kwargs):
791 790 return object.__getattribute__(self, '_observedcall')(
792 791 'sendall', *args, **kwargs
793 792 )
794 793
795 794 def sendto(self, *args, **kwargs):
796 795 return object.__getattribute__(self, '_observedcall')(
797 796 'sendto', *args, **kwargs
798 797 )
799 798
800 799 def setblocking(self, *args, **kwargs):
801 800 return object.__getattribute__(self, '_observedcall')(
802 801 'setblocking', *args, **kwargs
803 802 )
804 803
805 804 def settimeout(self, *args, **kwargs):
806 805 return object.__getattribute__(self, '_observedcall')(
807 806 'settimeout', *args, **kwargs
808 807 )
809 808
810 809 def gettimeout(self, *args, **kwargs):
811 810 return object.__getattribute__(self, '_observedcall')(
812 811 'gettimeout', *args, **kwargs
813 812 )
814 813
815 814 def setsockopt(self, *args, **kwargs):
816 815 return object.__getattribute__(self, '_observedcall')(
817 816 'setsockopt', *args, **kwargs
818 817 )
819 818
820 819
821 820 class baseproxyobserver(object):
822 821 def __init__(self, fh, name, logdata, logdataapis):
823 822 self.fh = fh
824 823 self.name = name
825 824 self.logdata = logdata
826 825 self.logdataapis = logdataapis
827 826
828 827 def _writedata(self, data):
829 828 if not self.logdata:
830 829 if self.logdataapis:
831 830 self.fh.write(b'\n')
832 831 self.fh.flush()
833 832 return
834 833
835 834 # Simple case writes all data on a single line.
836 835 if b'\n' not in data:
837 836 if self.logdataapis:
838 837 self.fh.write(b': %s\n' % stringutil.escapestr(data))
839 838 else:
840 839 self.fh.write(
841 840 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
842 841 )
843 842 self.fh.flush()
844 843 return
845 844
846 845 # Data with newlines is written to multiple lines.
847 846 if self.logdataapis:
848 847 self.fh.write(b':\n')
849 848
850 849 lines = data.splitlines(True)
851 850 for line in lines:
852 851 self.fh.write(
853 852 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
854 853 )
855 854 self.fh.flush()
856 855
857 856
858 857 class fileobjectobserver(baseproxyobserver):
859 858 """Logs file object activity."""
860 859
861 860 def __init__(
862 861 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
863 862 ):
864 863 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
865 864 self.reads = reads
866 865 self.writes = writes
867 866
868 867 def read(self, res, size=-1):
869 868 if not self.reads:
870 869 return
871 870 # Python 3 can return None from reads at EOF instead of empty strings.
872 871 if res is None:
873 872 res = b''
874 873
875 874 if size == -1 and res == b'':
876 875 # Suppress pointless read(-1) calls that return
877 876 # nothing. These happen _a lot_ on Python 3, and there
878 877 # doesn't seem to be a better workaround to have matching
879 878 # Python 2 and 3 behavior. :(
880 879 return
881 880
882 881 if self.logdataapis:
883 882 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
884 883
885 884 self._writedata(res)
886 885
887 886 def readline(self, res, limit=-1):
888 887 if not self.reads:
889 888 return
890 889
891 890 if self.logdataapis:
892 891 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
893 892
894 893 self._writedata(res)
895 894
896 895 def readinto(self, res, dest):
897 896 if not self.reads:
898 897 return
899 898
900 899 if self.logdataapis:
901 900 self.fh.write(
902 901 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
903 902 )
904 903
905 904 data = dest[0:res] if res is not None else b''
906 905
907 906 # _writedata() uses "in" operator and is confused by memoryview because
908 907 # characters are ints on Python 3.
909 908 if isinstance(data, memoryview):
910 909 data = data.tobytes()
911 910
912 911 self._writedata(data)
913 912
914 913 def write(self, res, data):
915 914 if not self.writes:
916 915 return
917 916
918 917 # Python 2 returns None from some write() calls. Python 3 (reasonably)
919 918 # returns the integer bytes written.
920 919 if res is None and data:
921 920 res = len(data)
922 921
923 922 if self.logdataapis:
924 923 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
925 924
926 925 self._writedata(data)
927 926
928 927 def flush(self, res):
929 928 if not self.writes:
930 929 return
931 930
932 931 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
933 932
934 933 # For observedbufferedinputpipe.
935 934 def bufferedread(self, res, size):
936 935 if not self.reads:
937 936 return
938 937
939 938 if self.logdataapis:
940 939 self.fh.write(
941 940 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
942 941 )
943 942
944 943 self._writedata(res)
945 944
946 945 def bufferedreadline(self, res):
947 946 if not self.reads:
948 947 return
949 948
950 949 if self.logdataapis:
951 950 self.fh.write(
952 951 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
953 952 )
954 953
955 954 self._writedata(res)
956 955
957 956
958 957 def makeloggingfileobject(
959 958 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
960 959 ):
961 960 """Turn a file object into a logging file object."""
962 961
963 962 observer = fileobjectobserver(
964 963 logh,
965 964 name,
966 965 reads=reads,
967 966 writes=writes,
968 967 logdata=logdata,
969 968 logdataapis=logdataapis,
970 969 )
971 970 return fileobjectproxy(fh, observer)
972 971
973 972
974 973 class socketobserver(baseproxyobserver):
975 974 """Logs socket activity."""
976 975
977 976 def __init__(
978 977 self,
979 978 fh,
980 979 name,
981 980 reads=True,
982 981 writes=True,
983 982 states=True,
984 983 logdata=False,
985 984 logdataapis=True,
986 985 ):
987 986 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
988 987 self.reads = reads
989 988 self.writes = writes
990 989 self.states = states
991 990
992 991 def makefile(self, res, mode=None, bufsize=None):
993 992 if not self.states:
994 993 return
995 994
996 995 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
997 996
998 997 def recv(self, res, size, flags=0):
999 998 if not self.reads:
1000 999 return
1001 1000
1002 1001 if self.logdataapis:
1003 1002 self.fh.write(
1004 1003 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
1005 1004 )
1006 1005 self._writedata(res)
1007 1006
1008 1007 def recvfrom(self, res, size, flags=0):
1009 1008 if not self.reads:
1010 1009 return
1011 1010
1012 1011 if self.logdataapis:
1013 1012 self.fh.write(
1014 1013 b'%s> recvfrom(%d, %d) -> %d'
1015 1014 % (self.name, size, flags, len(res[0]))
1016 1015 )
1017 1016
1018 1017 self._writedata(res[0])
1019 1018
1020 1019 def recvfrom_into(self, res, buf, size, flags=0):
1021 1020 if not self.reads:
1022 1021 return
1023 1022
1024 1023 if self.logdataapis:
1025 1024 self.fh.write(
1026 1025 b'%s> recvfrom_into(%d, %d) -> %d'
1027 1026 % (self.name, size, flags, res[0])
1028 1027 )
1029 1028
1030 1029 self._writedata(buf[0 : res[0]])
1031 1030
1032 1031 def recv_into(self, res, buf, size=0, flags=0):
1033 1032 if not self.reads:
1034 1033 return
1035 1034
1036 1035 if self.logdataapis:
1037 1036 self.fh.write(
1038 1037 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1039 1038 )
1040 1039
1041 1040 self._writedata(buf[0:res])
1042 1041
1043 1042 def send(self, res, data, flags=0):
1044 1043 if not self.writes:
1045 1044 return
1046 1045
1047 1046 self.fh.write(
1048 1047 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1049 1048 )
1050 1049 self._writedata(data)
1051 1050
1052 1051 def sendall(self, res, data, flags=0):
1053 1052 if not self.writes:
1054 1053 return
1055 1054
1056 1055 if self.logdataapis:
1057 1056 # Returns None on success. So don't bother reporting return value.
1058 1057 self.fh.write(
1059 1058 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1060 1059 )
1061 1060
1062 1061 self._writedata(data)
1063 1062
1064 1063 def sendto(self, res, data, flagsoraddress, address=None):
1065 1064 if not self.writes:
1066 1065 return
1067 1066
1068 1067 if address:
1069 1068 flags = flagsoraddress
1070 1069 else:
1071 1070 flags = 0
1072 1071
1073 1072 if self.logdataapis:
1074 1073 self.fh.write(
1075 1074 b'%s> sendto(%d, %d, %r) -> %d'
1076 1075 % (self.name, len(data), flags, address, res)
1077 1076 )
1078 1077
1079 1078 self._writedata(data)
1080 1079
1081 1080 def setblocking(self, res, flag):
1082 1081 if not self.states:
1083 1082 return
1084 1083
1085 1084 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1086 1085
1087 1086 def settimeout(self, res, value):
1088 1087 if not self.states:
1089 1088 return
1090 1089
1091 1090 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1092 1091
1093 1092 def gettimeout(self, res):
1094 1093 if not self.states:
1095 1094 return
1096 1095
1097 1096 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1098 1097
1099 1098 def setsockopt(self, res, level, optname, value):
1100 1099 if not self.states:
1101 1100 return
1102 1101
1103 1102 self.fh.write(
1104 1103 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1105 1104 % (self.name, level, optname, value, res)
1106 1105 )
1107 1106
1108 1107
1109 1108 def makeloggingsocket(
1110 1109 logh,
1111 1110 fh,
1112 1111 name,
1113 1112 reads=True,
1114 1113 writes=True,
1115 1114 states=True,
1116 1115 logdata=False,
1117 1116 logdataapis=True,
1118 1117 ):
1119 1118 """Turn a socket into a logging socket."""
1120 1119
1121 1120 observer = socketobserver(
1122 1121 logh,
1123 1122 name,
1124 1123 reads=reads,
1125 1124 writes=writes,
1126 1125 states=states,
1127 1126 logdata=logdata,
1128 1127 logdataapis=logdataapis,
1129 1128 )
1130 1129 return socketproxy(fh, observer)
1131 1130
1132 1131
1133 1132 def version():
1134 1133 """Return version information if available."""
1135 1134 try:
1136 1135 from . import __version__
1137 1136
1138 1137 return __version__.version
1139 1138 except ImportError:
1140 1139 return b'unknown'
1141 1140
1142 1141
1143 1142 def versiontuple(v=None, n=4):
1144 1143 """Parses a Mercurial version string into an N-tuple.
1145 1144
1146 1145 The version string to be parsed is specified with the ``v`` argument.
1147 1146 If it isn't defined, the current Mercurial version string will be parsed.
1148 1147
1149 1148 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1150 1149 returned values:
1151 1150
1152 1151 >>> v = b'3.6.1+190-df9b73d2d444'
1153 1152 >>> versiontuple(v, 2)
1154 1153 (3, 6)
1155 1154 >>> versiontuple(v, 3)
1156 1155 (3, 6, 1)
1157 1156 >>> versiontuple(v, 4)
1158 1157 (3, 6, 1, '190-df9b73d2d444')
1159 1158
1160 1159 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1161 1160 (3, 6, 1, '190-df9b73d2d444+20151118')
1162 1161
1163 1162 >>> v = b'3.6'
1164 1163 >>> versiontuple(v, 2)
1165 1164 (3, 6)
1166 1165 >>> versiontuple(v, 3)
1167 1166 (3, 6, None)
1168 1167 >>> versiontuple(v, 4)
1169 1168 (3, 6, None, None)
1170 1169
1171 1170 >>> v = b'3.9-rc'
1172 1171 >>> versiontuple(v, 2)
1173 1172 (3, 9)
1174 1173 >>> versiontuple(v, 3)
1175 1174 (3, 9, None)
1176 1175 >>> versiontuple(v, 4)
1177 1176 (3, 9, None, 'rc')
1178 1177
1179 1178 >>> v = b'3.9-rc+2-02a8fea4289b'
1180 1179 >>> versiontuple(v, 2)
1181 1180 (3, 9)
1182 1181 >>> versiontuple(v, 3)
1183 1182 (3, 9, None)
1184 1183 >>> versiontuple(v, 4)
1185 1184 (3, 9, None, 'rc+2-02a8fea4289b')
1186 1185
1187 1186 >>> versiontuple(b'4.6rc0')
1188 1187 (4, 6, None, 'rc0')
1189 1188 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1190 1189 (4, 6, None, 'rc0+12-425d55e54f98')
1191 1190 >>> versiontuple(b'.1.2.3')
1192 1191 (None, None, None, '.1.2.3')
1193 1192 >>> versiontuple(b'12.34..5')
1194 1193 (12, 34, None, '..5')
1195 1194 >>> versiontuple(b'1.2.3.4.5.6')
1196 1195 (1, 2, 3, '.4.5.6')
1197 1196 """
1198 1197 if not v:
1199 1198 v = version()
1200 1199 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1201 1200 if not m:
1202 1201 vparts, extra = b'', v
1203 1202 elif m.group(2):
1204 1203 vparts, extra = m.groups()
1205 1204 else:
1206 1205 vparts, extra = m.group(1), None
1207 1206
1208 1207 assert vparts is not None # help pytype
1209 1208
1210 1209 vints = []
1211 1210 for i in vparts.split(b'.'):
1212 1211 try:
1213 1212 vints.append(int(i))
1214 1213 except ValueError:
1215 1214 break
1216 1215 # (3, 6) -> (3, 6, None)
1217 1216 while len(vints) < 3:
1218 1217 vints.append(None)
1219 1218
1220 1219 if n == 2:
1221 1220 return (vints[0], vints[1])
1222 1221 if n == 3:
1223 1222 return (vints[0], vints[1], vints[2])
1224 1223 if n == 4:
1225 1224 return (vints[0], vints[1], vints[2], extra)
1226 1225
1227 1226 raise error.ProgrammingError(b"invalid version part request: %d" % n)
1228 1227
1229 1228
1230 1229 def cachefunc(func):
1231 1230 '''cache the result of function calls'''
1232 1231 # XXX doesn't handle keywords args
1233 1232 if func.__code__.co_argcount == 0:
1234 1233 listcache = []
1235 1234
1236 1235 def f():
1237 1236 if len(listcache) == 0:
1238 1237 listcache.append(func())
1239 1238 return listcache[0]
1240 1239
1241 1240 return f
1242 1241 cache = {}
1243 1242 if func.__code__.co_argcount == 1:
1244 1243 # we gain a small amount of time because
1245 1244 # we don't need to pack/unpack the list
1246 1245 def f(arg):
1247 1246 if arg not in cache:
1248 1247 cache[arg] = func(arg)
1249 1248 return cache[arg]
1250 1249
1251 1250 else:
1252 1251
1253 1252 def f(*args):
1254 1253 if args not in cache:
1255 1254 cache[args] = func(*args)
1256 1255 return cache[args]
1257 1256
1258 1257 return f
1259 1258
1260 1259
1261 1260 class cow(object):
1262 1261 """helper class to make copy-on-write easier
1263 1262
1264 1263 Call preparewrite before doing any writes.
1265 1264 """
1266 1265
1267 1266 def preparewrite(self):
1268 1267 """call this before writes, return self or a copied new object"""
1269 1268 if getattr(self, '_copied', 0):
1270 1269 self._copied -= 1
1271 1270 # Function cow.__init__ expects 1 arg(s), got 2 [wrong-arg-count]
1272 1271 return self.__class__(self) # pytype: disable=wrong-arg-count
1273 1272 return self
1274 1273
1275 1274 def copy(self):
1276 1275 """always do a cheap copy"""
1277 1276 self._copied = getattr(self, '_copied', 0) + 1
1278 1277 return self
1279 1278
1280 1279
1281 1280 class sortdict(collections.OrderedDict):
1282 1281 """a simple sorted dictionary
1283 1282
1284 1283 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1285 1284 >>> d2 = d1.copy()
1286 1285 >>> d2
1287 1286 sortdict([('a', 0), ('b', 1)])
1288 1287 >>> d2.update([(b'a', 2)])
1289 1288 >>> list(d2.keys()) # should still be in last-set order
1290 1289 ['b', 'a']
1291 1290 >>> d1.insert(1, b'a.5', 0.5)
1292 1291 >>> d1
1293 1292 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1294 1293 """
1295 1294
1296 1295 def __setitem__(self, key, value):
1297 1296 if key in self:
1298 1297 del self[key]
1299 1298 super(sortdict, self).__setitem__(key, value)
1300 1299
1301 1300 if pycompat.ispypy:
1302 1301 # __setitem__() isn't called as of PyPy 5.8.0
1303 1302 def update(self, src, **f):
1304 1303 if isinstance(src, dict):
1305 1304 src = pycompat.iteritems(src)
1306 1305 for k, v in src:
1307 1306 self[k] = v
1308 1307 for k in f:
1309 1308 self[k] = f[k]
1310 1309
1311 1310 def insert(self, position, key, value):
1312 1311 for (i, (k, v)) in enumerate(list(self.items())):
1313 1312 if i == position:
1314 1313 self[key] = value
1315 1314 if i >= position:
1316 1315 del self[k]
1317 1316 self[k] = v
1318 1317
1319 1318
1320 1319 class cowdict(cow, dict):
1321 1320 """copy-on-write dict
1322 1321
1323 1322 Be sure to call d = d.preparewrite() before writing to d.
1324 1323
1325 1324 >>> a = cowdict()
1326 1325 >>> a is a.preparewrite()
1327 1326 True
1328 1327 >>> b = a.copy()
1329 1328 >>> b is a
1330 1329 True
1331 1330 >>> c = b.copy()
1332 1331 >>> c is a
1333 1332 True
1334 1333 >>> a = a.preparewrite()
1335 1334 >>> b is a
1336 1335 False
1337 1336 >>> a is a.preparewrite()
1338 1337 True
1339 1338 >>> c = c.preparewrite()
1340 1339 >>> b is c
1341 1340 False
1342 1341 >>> b is b.preparewrite()
1343 1342 True
1344 1343 """
1345 1344
1346 1345
1347 1346 class cowsortdict(cow, sortdict):
1348 1347 """copy-on-write sortdict
1349 1348
1350 1349 Be sure to call d = d.preparewrite() before writing to d.
1351 1350 """
1352 1351
1353 1352
1354 1353 class transactional(object): # pytype: disable=ignored-metaclass
1355 1354 """Base class for making a transactional type into a context manager."""
1356 1355
1357 1356 __metaclass__ = abc.ABCMeta
1358 1357
1359 1358 @abc.abstractmethod
1360 1359 def close(self):
1361 1360 """Successfully closes the transaction."""
1362 1361
1363 1362 @abc.abstractmethod
1364 1363 def release(self):
1365 1364 """Marks the end of the transaction.
1366 1365
1367 1366 If the transaction has not been closed, it will be aborted.
1368 1367 """
1369 1368
1370 1369 def __enter__(self):
1371 1370 return self
1372 1371
1373 1372 def __exit__(self, exc_type, exc_val, exc_tb):
1374 1373 try:
1375 1374 if exc_type is None:
1376 1375 self.close()
1377 1376 finally:
1378 1377 self.release()
1379 1378
1380 1379
1381 1380 @contextlib.contextmanager
1382 1381 def acceptintervention(tr=None):
1383 1382 """A context manager that closes the transaction on InterventionRequired
1384 1383
1385 1384 If no transaction was provided, this simply runs the body and returns
1386 1385 """
1387 1386 if not tr:
1388 1387 yield
1389 1388 return
1390 1389 try:
1391 1390 yield
1392 1391 tr.close()
1393 1392 except error.InterventionRequired:
1394 1393 tr.close()
1395 1394 raise
1396 1395 finally:
1397 1396 tr.release()
1398 1397
1399 1398
1400 1399 @contextlib.contextmanager
1401 1400 def nullcontextmanager(enter_result=None):
1402 1401 yield enter_result
1403 1402
1404 1403
1405 1404 class _lrucachenode(object):
1406 1405 """A node in a doubly linked list.
1407 1406
1408 1407 Holds a reference to nodes on either side as well as a key-value
1409 1408 pair for the dictionary entry.
1410 1409 """
1411 1410
1412 1411 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1413 1412
1414 1413 def __init__(self):
1415 1414 self.next = self
1416 1415 self.prev = self
1417 1416
1418 1417 self.key = _notset
1419 1418 self.value = None
1420 1419 self.cost = 0
1421 1420
1422 1421 def markempty(self):
1423 1422 """Mark the node as emptied."""
1424 1423 self.key = _notset
1425 1424 self.value = None
1426 1425 self.cost = 0
1427 1426
1428 1427
1429 1428 class lrucachedict(object):
1430 1429 """Dict that caches most recent accesses and sets.
1431 1430
1432 1431 The dict consists of an actual backing dict - indexed by original
1433 1432 key - and a doubly linked circular list defining the order of entries in
1434 1433 the cache.
1435 1434
1436 1435 The head node is the newest entry in the cache. If the cache is full,
1437 1436 we recycle head.prev and make it the new head. Cache accesses result in
1438 1437 the node being moved to before the existing head and being marked as the
1439 1438 new head node.
1440 1439
1441 1440 Items in the cache can be inserted with an optional "cost" value. This is
1442 1441 simply an integer that is specified by the caller. The cache can be queried
1443 1442 for the total cost of all items presently in the cache.
1444 1443
1445 1444 The cache can also define a maximum cost. If a cache insertion would
1446 1445 cause the total cost of the cache to go beyond the maximum cost limit,
1447 1446 nodes will be evicted to make room for the new code. This can be used
1448 1447 to e.g. set a max memory limit and associate an estimated bytes size
1449 1448 cost to each item in the cache. By default, no maximum cost is enforced.
1450 1449 """
1451 1450
1452 1451 def __init__(self, max, maxcost=0):
1453 1452 self._cache = {}
1454 1453
1455 1454 self._head = _lrucachenode()
1456 1455 self._size = 1
1457 1456 self.capacity = max
1458 1457 self.totalcost = 0
1459 1458 self.maxcost = maxcost
1460 1459
1461 1460 def __len__(self):
1462 1461 return len(self._cache)
1463 1462
1464 1463 def __contains__(self, k):
1465 1464 return k in self._cache
1466 1465
1467 1466 def __iter__(self):
1468 1467 # We don't have to iterate in cache order, but why not.
1469 1468 n = self._head
1470 1469 for i in range(len(self._cache)):
1471 1470 yield n.key
1472 1471 n = n.next
1473 1472
1474 1473 def __getitem__(self, k):
1475 1474 node = self._cache[k]
1476 1475 self._movetohead(node)
1477 1476 return node.value
1478 1477
1479 1478 def insert(self, k, v, cost=0):
1480 1479 """Insert a new item in the cache with optional cost value."""
1481 1480 node = self._cache.get(k)
1482 1481 # Replace existing value and mark as newest.
1483 1482 if node is not None:
1484 1483 self.totalcost -= node.cost
1485 1484 node.value = v
1486 1485 node.cost = cost
1487 1486 self.totalcost += cost
1488 1487 self._movetohead(node)
1489 1488
1490 1489 if self.maxcost:
1491 1490 self._enforcecostlimit()
1492 1491
1493 1492 return
1494 1493
1495 1494 if self._size < self.capacity:
1496 1495 node = self._addcapacity()
1497 1496 else:
1498 1497 # Grab the last/oldest item.
1499 1498 node = self._head.prev
1500 1499
1501 1500 # At capacity. Kill the old entry.
1502 1501 if node.key is not _notset:
1503 1502 self.totalcost -= node.cost
1504 1503 del self._cache[node.key]
1505 1504
1506 1505 node.key = k
1507 1506 node.value = v
1508 1507 node.cost = cost
1509 1508 self.totalcost += cost
1510 1509 self._cache[k] = node
1511 1510 # And mark it as newest entry. No need to adjust order since it
1512 1511 # is already self._head.prev.
1513 1512 self._head = node
1514 1513
1515 1514 if self.maxcost:
1516 1515 self._enforcecostlimit()
1517 1516
1518 1517 def __setitem__(self, k, v):
1519 1518 self.insert(k, v)
1520 1519
1521 1520 def __delitem__(self, k):
1522 1521 self.pop(k)
1523 1522
1524 1523 def pop(self, k, default=_notset):
1525 1524 try:
1526 1525 node = self._cache.pop(k)
1527 1526 except KeyError:
1528 1527 if default is _notset:
1529 1528 raise
1530 1529 return default
1531 1530
1532 1531 assert node is not None # help pytype
1533 1532 value = node.value
1534 1533 self.totalcost -= node.cost
1535 1534 node.markempty()
1536 1535
1537 1536 # Temporarily mark as newest item before re-adjusting head to make
1538 1537 # this node the oldest item.
1539 1538 self._movetohead(node)
1540 1539 self._head = node.next
1541 1540
1542 1541 return value
1543 1542
1544 1543 # Additional dict methods.
1545 1544
1546 1545 def get(self, k, default=None):
1547 1546 try:
1548 1547 return self.__getitem__(k)
1549 1548 except KeyError:
1550 1549 return default
1551 1550
1552 1551 def peek(self, k, default=_notset):
1553 1552 """Get the specified item without moving it to the head
1554 1553
1555 1554 Unlike get(), this doesn't mutate the internal state. But be aware
1556 1555 that it doesn't mean peek() is thread safe.
1557 1556 """
1558 1557 try:
1559 1558 node = self._cache[k]
1560 1559 assert node is not None # help pytype
1561 1560 return node.value
1562 1561 except KeyError:
1563 1562 if default is _notset:
1564 1563 raise
1565 1564 return default
1566 1565
1567 1566 def clear(self):
1568 1567 n = self._head
1569 1568 while n.key is not _notset:
1570 1569 self.totalcost -= n.cost
1571 1570 n.markempty()
1572 1571 n = n.next
1573 1572
1574 1573 self._cache.clear()
1575 1574
1576 1575 def copy(self, capacity=None, maxcost=0):
1577 1576 """Create a new cache as a copy of the current one.
1578 1577
1579 1578 By default, the new cache has the same capacity as the existing one.
1580 1579 But, the cache capacity can be changed as part of performing the
1581 1580 copy.
1582 1581
1583 1582 Items in the copy have an insertion/access order matching this
1584 1583 instance.
1585 1584 """
1586 1585
1587 1586 capacity = capacity or self.capacity
1588 1587 maxcost = maxcost or self.maxcost
1589 1588 result = lrucachedict(capacity, maxcost=maxcost)
1590 1589
1591 1590 # We copy entries by iterating in oldest-to-newest order so the copy
1592 1591 # has the correct ordering.
1593 1592
1594 1593 # Find the first non-empty entry.
1595 1594 n = self._head.prev
1596 1595 while n.key is _notset and n is not self._head:
1597 1596 n = n.prev
1598 1597
1599 1598 # We could potentially skip the first N items when decreasing capacity.
1600 1599 # But let's keep it simple unless it is a performance problem.
1601 1600 for i in range(len(self._cache)):
1602 1601 result.insert(n.key, n.value, cost=n.cost)
1603 1602 n = n.prev
1604 1603
1605 1604 return result
1606 1605
1607 1606 def popoldest(self):
1608 1607 """Remove the oldest item from the cache.
1609 1608
1610 1609 Returns the (key, value) describing the removed cache entry.
1611 1610 """
1612 1611 if not self._cache:
1613 1612 return
1614 1613
1615 1614 # Walk the linked list backwards starting at tail node until we hit
1616 1615 # a non-empty node.
1617 1616 n = self._head.prev
1618 1617
1619 1618 assert n is not None # help pytype
1620 1619
1621 1620 while n.key is _notset:
1622 1621 n = n.prev
1623 1622
1624 1623 assert n is not None # help pytype
1625 1624
1626 1625 key, value = n.key, n.value
1627 1626
1628 1627 # And remove it from the cache and mark it as empty.
1629 1628 del self._cache[n.key]
1630 1629 self.totalcost -= n.cost
1631 1630 n.markempty()
1632 1631
1633 1632 return key, value
1634 1633
1635 1634 def _movetohead(self, node):
1636 1635 """Mark a node as the newest, making it the new head.
1637 1636
1638 1637 When a node is accessed, it becomes the freshest entry in the LRU
1639 1638 list, which is denoted by self._head.
1640 1639
1641 1640 Visually, let's make ``N`` the new head node (* denotes head):
1642 1641
1643 1642 previous/oldest <-> head <-> next/next newest
1644 1643
1645 1644 ----<->--- A* ---<->-----
1646 1645 | |
1647 1646 E <-> D <-> N <-> C <-> B
1648 1647
1649 1648 To:
1650 1649
1651 1650 ----<->--- N* ---<->-----
1652 1651 | |
1653 1652 E <-> D <-> C <-> B <-> A
1654 1653
1655 1654 This requires the following moves:
1656 1655
1657 1656 C.next = D (node.prev.next = node.next)
1658 1657 D.prev = C (node.next.prev = node.prev)
1659 1658 E.next = N (head.prev.next = node)
1660 1659 N.prev = E (node.prev = head.prev)
1661 1660 N.next = A (node.next = head)
1662 1661 A.prev = N (head.prev = node)
1663 1662 """
1664 1663 head = self._head
1665 1664 # C.next = D
1666 1665 node.prev.next = node.next
1667 1666 # D.prev = C
1668 1667 node.next.prev = node.prev
1669 1668 # N.prev = E
1670 1669 node.prev = head.prev
1671 1670 # N.next = A
1672 1671 # It is tempting to do just "head" here, however if node is
1673 1672 # adjacent to head, this will do bad things.
1674 1673 node.next = head.prev.next
1675 1674 # E.next = N
1676 1675 node.next.prev = node
1677 1676 # A.prev = N
1678 1677 node.prev.next = node
1679 1678
1680 1679 self._head = node
1681 1680
1682 1681 def _addcapacity(self):
1683 1682 """Add a node to the circular linked list.
1684 1683
1685 1684 The new node is inserted before the head node.
1686 1685 """
1687 1686 head = self._head
1688 1687 node = _lrucachenode()
1689 1688 head.prev.next = node
1690 1689 node.prev = head.prev
1691 1690 node.next = head
1692 1691 head.prev = node
1693 1692 self._size += 1
1694 1693 return node
1695 1694
1696 1695 def _enforcecostlimit(self):
1697 1696 # This should run after an insertion. It should only be called if total
1698 1697 # cost limits are being enforced.
1699 1698 # The most recently inserted node is never evicted.
1700 1699 if len(self) <= 1 or self.totalcost <= self.maxcost:
1701 1700 return
1702 1701
1703 1702 # This is logically equivalent to calling popoldest() until we
1704 1703 # free up enough cost. We don't do that since popoldest() needs
1705 1704 # to walk the linked list and doing this in a loop would be
1706 1705 # quadratic. So we find the first non-empty node and then
1707 1706 # walk nodes until we free up enough capacity.
1708 1707 #
1709 1708 # If we only removed the minimum number of nodes to free enough
1710 1709 # cost at insert time, chances are high that the next insert would
1711 1710 # also require pruning. This would effectively constitute quadratic
1712 1711 # behavior for insert-heavy workloads. To mitigate this, we set a
1713 1712 # target cost that is a percentage of the max cost. This will tend
1714 1713 # to free more nodes when the high water mark is reached, which
1715 1714 # lowers the chances of needing to prune on the subsequent insert.
1716 1715 targetcost = int(self.maxcost * 0.75)
1717 1716
1718 1717 n = self._head.prev
1719 1718 while n.key is _notset:
1720 1719 n = n.prev
1721 1720
1722 1721 while len(self) > 1 and self.totalcost > targetcost:
1723 1722 del self._cache[n.key]
1724 1723 self.totalcost -= n.cost
1725 1724 n.markempty()
1726 1725 n = n.prev
1727 1726
1728 1727
1729 1728 def lrucachefunc(func):
1730 1729 '''cache most recent results of function calls'''
1731 1730 cache = {}
1732 1731 order = collections.deque()
1733 1732 if func.__code__.co_argcount == 1:
1734 1733
1735 1734 def f(arg):
1736 1735 if arg not in cache:
1737 1736 if len(cache) > 20:
1738 1737 del cache[order.popleft()]
1739 1738 cache[arg] = func(arg)
1740 1739 else:
1741 1740 order.remove(arg)
1742 1741 order.append(arg)
1743 1742 return cache[arg]
1744 1743
1745 1744 else:
1746 1745
1747 1746 def f(*args):
1748 1747 if args not in cache:
1749 1748 if len(cache) > 20:
1750 1749 del cache[order.popleft()]
1751 1750 cache[args] = func(*args)
1752 1751 else:
1753 1752 order.remove(args)
1754 1753 order.append(args)
1755 1754 return cache[args]
1756 1755
1757 1756 return f
1758 1757
1759 1758
1760 1759 class propertycache(object):
1761 1760 def __init__(self, func):
1762 1761 self.func = func
1763 1762 self.name = func.__name__
1764 1763
1765 1764 def __get__(self, obj, type=None):
1766 1765 result = self.func(obj)
1767 1766 self.cachevalue(obj, result)
1768 1767 return result
1769 1768
1770 1769 def cachevalue(self, obj, value):
1771 1770 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1772 1771 obj.__dict__[self.name] = value
1773 1772
1774 1773
1775 1774 def clearcachedproperty(obj, prop):
1776 1775 '''clear a cached property value, if one has been set'''
1777 1776 prop = pycompat.sysstr(prop)
1778 1777 if prop in obj.__dict__:
1779 1778 del obj.__dict__[prop]
1780 1779
1781 1780
1782 1781 def increasingchunks(source, min=1024, max=65536):
1783 1782 """return no less than min bytes per chunk while data remains,
1784 1783 doubling min after each chunk until it reaches max"""
1785 1784
1786 1785 def log2(x):
1787 1786 if not x:
1788 1787 return 0
1789 1788 i = 0
1790 1789 while x:
1791 1790 x >>= 1
1792 1791 i += 1
1793 1792 return i - 1
1794 1793
1795 1794 buf = []
1796 1795 blen = 0
1797 1796 for chunk in source:
1798 1797 buf.append(chunk)
1799 1798 blen += len(chunk)
1800 1799 if blen >= min:
1801 1800 if min < max:
1802 1801 min = min << 1
1803 1802 nmin = 1 << log2(blen)
1804 1803 if nmin > min:
1805 1804 min = nmin
1806 1805 if min > max:
1807 1806 min = max
1808 1807 yield b''.join(buf)
1809 1808 blen = 0
1810 1809 buf = []
1811 1810 if buf:
1812 1811 yield b''.join(buf)
1813 1812
1814 1813
1815 1814 def always(fn):
1816 1815 return True
1817 1816
1818 1817
1819 1818 def never(fn):
1820 1819 return False
1821 1820
1822 1821
1823 1822 def nogc(func):
1824 1823 """disable garbage collector
1825 1824
1826 1825 Python's garbage collector triggers a GC each time a certain number of
1827 1826 container objects (the number being defined by gc.get_threshold()) are
1828 1827 allocated even when marked not to be tracked by the collector. Tracking has
1829 1828 no effect on when GCs are triggered, only on what objects the GC looks
1830 1829 into. As a workaround, disable GC while building complex (huge)
1831 1830 containers.
1832 1831
1833 1832 This garbage collector issue have been fixed in 2.7. But it still affect
1834 1833 CPython's performance.
1835 1834 """
1836 1835
1837 1836 def wrapper(*args, **kwargs):
1838 1837 gcenabled = gc.isenabled()
1839 1838 gc.disable()
1840 1839 try:
1841 1840 return func(*args, **kwargs)
1842 1841 finally:
1843 1842 if gcenabled:
1844 1843 gc.enable()
1845 1844
1846 1845 return wrapper
1847 1846
1848 1847
1849 1848 if pycompat.ispypy:
1850 1849 # PyPy runs slower with gc disabled
1851 1850 nogc = lambda x: x
1852 1851
1853 1852
1854 1853 def pathto(root, n1, n2):
1855 1854 # type: (bytes, bytes, bytes) -> bytes
1856 1855 """return the relative path from one place to another.
1857 1856 root should use os.sep to separate directories
1858 1857 n1 should use os.sep to separate directories
1859 1858 n2 should use "/" to separate directories
1860 1859 returns an os.sep-separated path.
1861 1860
1862 1861 If n1 is a relative path, it's assumed it's
1863 1862 relative to root.
1864 1863 n2 should always be relative to root.
1865 1864 """
1866 1865 if not n1:
1867 1866 return localpath(n2)
1868 1867 if os.path.isabs(n1):
1869 1868 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1870 1869 return os.path.join(root, localpath(n2))
1871 1870 n2 = b'/'.join((pconvert(root), n2))
1872 1871 a, b = splitpath(n1), n2.split(b'/')
1873 1872 a.reverse()
1874 1873 b.reverse()
1875 1874 while a and b and a[-1] == b[-1]:
1876 1875 a.pop()
1877 1876 b.pop()
1878 1877 b.reverse()
1879 1878 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1880 1879
1881 1880
1882 1881 def checksignature(func, depth=1):
1883 1882 '''wrap a function with code to check for calling errors'''
1884 1883
1885 1884 def check(*args, **kwargs):
1886 1885 try:
1887 1886 return func(*args, **kwargs)
1888 1887 except TypeError:
1889 1888 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1890 1889 raise error.SignatureError
1891 1890 raise
1892 1891
1893 1892 return check
1894 1893
1895 1894
1896 1895 # a whilelist of known filesystems where hardlink works reliably
1897 1896 _hardlinkfswhitelist = {
1898 1897 b'apfs',
1899 1898 b'btrfs',
1900 1899 b'ext2',
1901 1900 b'ext3',
1902 1901 b'ext4',
1903 1902 b'hfs',
1904 1903 b'jfs',
1905 1904 b'NTFS',
1906 1905 b'reiserfs',
1907 1906 b'tmpfs',
1908 1907 b'ufs',
1909 1908 b'xfs',
1910 1909 b'zfs',
1911 1910 }
1912 1911
1913 1912
1914 1913 def copyfile(
1915 1914 src,
1916 1915 dest,
1917 1916 hardlink=False,
1918 1917 copystat=False,
1919 1918 checkambig=False,
1920 1919 nb_bytes=None,
1921 1920 no_hardlink_cb=None,
1922 1921 check_fs_hardlink=True,
1923 1922 ):
1924 1923 """copy a file, preserving mode and optionally other stat info like
1925 1924 atime/mtime
1926 1925
1927 1926 checkambig argument is used with filestat, and is useful only if
1928 1927 destination file is guarded by any lock (e.g. repo.lock or
1929 1928 repo.wlock).
1930 1929
1931 1930 copystat and checkambig should be exclusive.
1932 1931
1933 1932 nb_bytes: if set only copy the first `nb_bytes` of the source file.
1934 1933 """
1935 1934 assert not (copystat and checkambig)
1936 1935 oldstat = None
1937 1936 if os.path.lexists(dest):
1938 1937 if checkambig:
1939 1938 oldstat = checkambig and filestat.frompath(dest)
1940 1939 unlink(dest)
1941 1940 if hardlink and check_fs_hardlink:
1942 1941 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1943 1942 # unless we are confident that dest is on a whitelisted filesystem.
1944 1943 try:
1945 1944 fstype = getfstype(os.path.dirname(dest))
1946 1945 except OSError:
1947 1946 fstype = None
1948 1947 if fstype not in _hardlinkfswhitelist:
1949 1948 if no_hardlink_cb is not None:
1950 1949 no_hardlink_cb()
1951 1950 hardlink = False
1952 1951 if hardlink:
1953 1952 try:
1954 1953 oslink(src, dest)
1955 1954 if nb_bytes is not None:
1956 1955 m = "the `nb_bytes` argument is incompatible with `hardlink`"
1957 1956 raise error.ProgrammingError(m)
1958 1957 return
1959 1958 except (IOError, OSError) as exc:
1960 1959 if exc.errno != errno.EEXIST and no_hardlink_cb is not None:
1961 1960 no_hardlink_cb()
1962 1961 # fall back to normal copy
1963 1962 if os.path.islink(src):
1964 1963 os.symlink(os.readlink(src), dest)
1965 1964 # copytime is ignored for symlinks, but in general copytime isn't needed
1966 1965 # for them anyway
1967 1966 if nb_bytes is not None:
1968 1967 m = "cannot use `nb_bytes` on a symlink"
1969 1968 raise error.ProgrammingError(m)
1970 1969 else:
1971 1970 try:
1972 1971 shutil.copyfile(src, dest)
1973 1972 if copystat:
1974 1973 # copystat also copies mode
1975 1974 shutil.copystat(src, dest)
1976 1975 else:
1977 1976 shutil.copymode(src, dest)
1978 1977 if oldstat and oldstat.stat:
1979 1978 newstat = filestat.frompath(dest)
1980 1979 if newstat.isambig(oldstat):
1981 1980 # stat of copied file is ambiguous to original one
1982 1981 advanced = (
1983 1982 oldstat.stat[stat.ST_MTIME] + 1
1984 1983 ) & 0x7FFFFFFF
1985 1984 os.utime(dest, (advanced, advanced))
1986 1985 # We could do something smarter using `copy_file_range` call or similar
1987 1986 if nb_bytes is not None:
1988 1987 with open(dest, mode='r+') as f:
1989 1988 f.truncate(nb_bytes)
1990 1989 except shutil.Error as inst:
1991 1990 raise error.Abort(stringutil.forcebytestr(inst))
1992 1991
1993 1992
1994 1993 def copyfiles(src, dst, hardlink=None, progress=None):
1995 1994 """Copy a directory tree using hardlinks if possible."""
1996 1995 num = 0
1997 1996
1998 1997 def settopic():
1999 1998 if progress:
2000 1999 progress.topic = _(b'linking') if hardlink else _(b'copying')
2001 2000
2002 2001 if os.path.isdir(src):
2003 2002 if hardlink is None:
2004 2003 hardlink = (
2005 2004 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
2006 2005 )
2007 2006 settopic()
2008 2007 os.mkdir(dst)
2009 2008 for name, kind in listdir(src):
2010 2009 srcname = os.path.join(src, name)
2011 2010 dstname = os.path.join(dst, name)
2012 2011 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
2013 2012 num += n
2014 2013 else:
2015 2014 if hardlink is None:
2016 2015 hardlink = (
2017 2016 os.stat(os.path.dirname(src)).st_dev
2018 2017 == os.stat(os.path.dirname(dst)).st_dev
2019 2018 )
2020 2019 settopic()
2021 2020
2022 2021 if hardlink:
2023 2022 try:
2024 2023 oslink(src, dst)
2025 2024 except (IOError, OSError) as exc:
2026 2025 if exc.errno != errno.EEXIST:
2027 2026 hardlink = False
2028 2027 # XXX maybe try to relink if the file exist ?
2029 2028 shutil.copy(src, dst)
2030 2029 else:
2031 2030 shutil.copy(src, dst)
2032 2031 num += 1
2033 2032 if progress:
2034 2033 progress.increment()
2035 2034
2036 2035 return hardlink, num
2037 2036
2038 2037
2039 2038 _winreservednames = {
2040 2039 b'con',
2041 2040 b'prn',
2042 2041 b'aux',
2043 2042 b'nul',
2044 2043 b'com1',
2045 2044 b'com2',
2046 2045 b'com3',
2047 2046 b'com4',
2048 2047 b'com5',
2049 2048 b'com6',
2050 2049 b'com7',
2051 2050 b'com8',
2052 2051 b'com9',
2053 2052 b'lpt1',
2054 2053 b'lpt2',
2055 2054 b'lpt3',
2056 2055 b'lpt4',
2057 2056 b'lpt5',
2058 2057 b'lpt6',
2059 2058 b'lpt7',
2060 2059 b'lpt8',
2061 2060 b'lpt9',
2062 2061 }
2063 2062 _winreservedchars = b':*?"<>|'
2064 2063
2065 2064
2066 2065 def checkwinfilename(path):
2067 2066 # type: (bytes) -> Optional[bytes]
2068 2067 r"""Check that the base-relative path is a valid filename on Windows.
2069 2068 Returns None if the path is ok, or a UI string describing the problem.
2070 2069
2071 2070 >>> checkwinfilename(b"just/a/normal/path")
2072 2071 >>> checkwinfilename(b"foo/bar/con.xml")
2073 2072 "filename contains 'con', which is reserved on Windows"
2074 2073 >>> checkwinfilename(b"foo/con.xml/bar")
2075 2074 "filename contains 'con', which is reserved on Windows"
2076 2075 >>> checkwinfilename(b"foo/bar/xml.con")
2077 2076 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2078 2077 "filename contains 'AUX', which is reserved on Windows"
2079 2078 >>> checkwinfilename(b"foo/bar/bla:.txt")
2080 2079 "filename contains ':', which is reserved on Windows"
2081 2080 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2082 2081 "filename contains '\\x07', which is invalid on Windows"
2083 2082 >>> checkwinfilename(b"foo/bar/bla ")
2084 2083 "filename ends with ' ', which is not allowed on Windows"
2085 2084 >>> checkwinfilename(b"../bar")
2086 2085 >>> checkwinfilename(b"foo\\")
2087 2086 "filename ends with '\\', which is invalid on Windows"
2088 2087 >>> checkwinfilename(b"foo\\/bar")
2089 2088 "directory name ends with '\\', which is invalid on Windows"
2090 2089 """
2091 2090 if path.endswith(b'\\'):
2092 2091 return _(b"filename ends with '\\', which is invalid on Windows")
2093 2092 if b'\\/' in path:
2094 2093 return _(b"directory name ends with '\\', which is invalid on Windows")
2095 2094 for n in path.replace(b'\\', b'/').split(b'/'):
2096 2095 if not n:
2097 2096 continue
2098 2097 for c in _filenamebytestr(n):
2099 2098 if c in _winreservedchars:
2100 2099 return (
2101 2100 _(
2102 2101 b"filename contains '%s', which is reserved "
2103 2102 b"on Windows"
2104 2103 )
2105 2104 % c
2106 2105 )
2107 2106 if ord(c) <= 31:
2108 2107 return _(
2109 2108 b"filename contains '%s', which is invalid on Windows"
2110 2109 ) % stringutil.escapestr(c)
2111 2110 base = n.split(b'.')[0]
2112 2111 if base and base.lower() in _winreservednames:
2113 2112 return (
2114 2113 _(b"filename contains '%s', which is reserved on Windows")
2115 2114 % base
2116 2115 )
2117 2116 t = n[-1:]
2118 2117 if t in b'. ' and n not in b'..':
2119 2118 return (
2120 2119 _(
2121 2120 b"filename ends with '%s', which is not allowed "
2122 2121 b"on Windows"
2123 2122 )
2124 2123 % t
2125 2124 )
2126 2125
2127 2126
2128 2127 timer = getattr(time, "perf_counter", None)
2129 2128
2130 2129 if pycompat.iswindows:
2131 2130 checkosfilename = checkwinfilename
2132 2131 if not timer:
2133 2132 timer = time.clock
2134 2133 else:
2135 2134 # mercurial.windows doesn't have platform.checkosfilename
2136 2135 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2137 2136 if not timer:
2138 2137 timer = time.time
2139 2138
2140 2139
2141 2140 def makelock(info, pathname):
2142 2141 """Create a lock file atomically if possible
2143 2142
2144 2143 This may leave a stale lock file if symlink isn't supported and signal
2145 2144 interrupt is enabled.
2146 2145 """
2147 2146 try:
2148 2147 return os.symlink(info, pathname)
2149 2148 except OSError as why:
2150 2149 if why.errno == errno.EEXIST:
2151 2150 raise
2152 2151 except AttributeError: # no symlink in os
2153 2152 pass
2154 2153
2155 2154 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2156 2155 ld = os.open(pathname, flags)
2157 2156 os.write(ld, info)
2158 2157 os.close(ld)
2159 2158
2160 2159
2161 2160 def readlock(pathname):
2162 2161 # type: (bytes) -> bytes
2163 2162 try:
2164 2163 return readlink(pathname)
2165 2164 except OSError as why:
2166 2165 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2167 2166 raise
2168 2167 except AttributeError: # no symlink in os
2169 2168 pass
2170 2169 with posixfile(pathname, b'rb') as fp:
2171 2170 return fp.read()
2172 2171
2173 2172
2174 2173 def fstat(fp):
2175 2174 '''stat file object that may not have fileno method.'''
2176 2175 try:
2177 2176 return os.fstat(fp.fileno())
2178 2177 except AttributeError:
2179 2178 return os.stat(fp.name)
2180 2179
2181 2180
2182 2181 # File system features
2183 2182
2184 2183
2185 2184 def fscasesensitive(path):
2186 2185 # type: (bytes) -> bool
2187 2186 """
2188 2187 Return true if the given path is on a case-sensitive filesystem
2189 2188
2190 2189 Requires a path (like /foo/.hg) ending with a foldable final
2191 2190 directory component.
2192 2191 """
2193 2192 s1 = os.lstat(path)
2194 2193 d, b = os.path.split(path)
2195 2194 b2 = b.upper()
2196 2195 if b == b2:
2197 2196 b2 = b.lower()
2198 2197 if b == b2:
2199 2198 return True # no evidence against case sensitivity
2200 2199 p2 = os.path.join(d, b2)
2201 2200 try:
2202 2201 s2 = os.lstat(p2)
2203 2202 if s2 == s1:
2204 2203 return False
2205 2204 return True
2206 2205 except OSError:
2207 2206 return True
2208 2207
2209 2208
2210 2209 _re2_input = lambda x: x
2211 2210 try:
2212 2211 import re2 # pytype: disable=import-error
2213 2212
2214 2213 _re2 = None
2215 2214 except ImportError:
2216 2215 _re2 = False
2217 2216
2218 2217
2219 2218 class _re(object):
2220 2219 def _checkre2(self):
2221 2220 global _re2
2222 2221 global _re2_input
2223 2222
2224 2223 check_pattern = br'\[([^\[]+)\]'
2225 2224 check_input = b'[ui]'
2226 2225 try:
2227 2226 # check if match works, see issue3964
2228 2227 _re2 = bool(re2.match(check_pattern, check_input))
2229 2228 except ImportError:
2230 2229 _re2 = False
2231 2230 except TypeError:
2232 2231 # the `pyre-2` project provides a re2 module that accept bytes
2233 2232 # the `fb-re2` project provides a re2 module that acccept sysstr
2234 2233 check_pattern = pycompat.sysstr(check_pattern)
2235 2234 check_input = pycompat.sysstr(check_input)
2236 2235 _re2 = bool(re2.match(check_pattern, check_input))
2237 2236 _re2_input = pycompat.sysstr
2238 2237
2239 2238 def compile(self, pat, flags=0):
2240 2239 """Compile a regular expression, using re2 if possible
2241 2240
2242 2241 For best performance, use only re2-compatible regexp features. The
2243 2242 only flags from the re module that are re2-compatible are
2244 2243 IGNORECASE and MULTILINE."""
2245 2244 if _re2 is None:
2246 2245 self._checkre2()
2247 2246 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2248 2247 if flags & remod.IGNORECASE:
2249 2248 pat = b'(?i)' + pat
2250 2249 if flags & remod.MULTILINE:
2251 2250 pat = b'(?m)' + pat
2252 2251 try:
2253 2252 return re2.compile(_re2_input(pat))
2254 2253 except re2.error:
2255 2254 pass
2256 2255 return remod.compile(pat, flags)
2257 2256
2258 2257 @propertycache
2259 2258 def escape(self):
2260 2259 """Return the version of escape corresponding to self.compile.
2261 2260
2262 2261 This is imperfect because whether re2 or re is used for a particular
2263 2262 function depends on the flags, etc, but it's the best we can do.
2264 2263 """
2265 2264 global _re2
2266 2265 if _re2 is None:
2267 2266 self._checkre2()
2268 2267 if _re2:
2269 2268 return re2.escape
2270 2269 else:
2271 2270 return remod.escape
2272 2271
2273 2272
2274 2273 re = _re()
2275 2274
2276 2275 _fspathcache = {}
2277 2276
2278 2277
2279 2278 def fspath(name, root):
2280 2279 # type: (bytes, bytes) -> bytes
2281 2280 """Get name in the case stored in the filesystem
2282 2281
2283 2282 The name should be relative to root, and be normcase-ed for efficiency.
2284 2283
2285 2284 Note that this function is unnecessary, and should not be
2286 2285 called, for case-sensitive filesystems (simply because it's expensive).
2287 2286
2288 2287 The root should be normcase-ed, too.
2289 2288 """
2290 2289
2291 2290 def _makefspathcacheentry(dir):
2292 2291 return {normcase(n): n for n in os.listdir(dir)}
2293 2292
2294 2293 seps = pycompat.ossep
2295 2294 if pycompat.osaltsep:
2296 2295 seps = seps + pycompat.osaltsep
2297 2296 # Protect backslashes. This gets silly very quickly.
2298 2297 seps.replace(b'\\', b'\\\\')
2299 2298 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2300 2299 dir = os.path.normpath(root)
2301 2300 result = []
2302 2301 for part, sep in pattern.findall(name):
2303 2302 if sep:
2304 2303 result.append(sep)
2305 2304 continue
2306 2305
2307 2306 if dir not in _fspathcache:
2308 2307 _fspathcache[dir] = _makefspathcacheentry(dir)
2309 2308 contents = _fspathcache[dir]
2310 2309
2311 2310 found = contents.get(part)
2312 2311 if not found:
2313 2312 # retry "once per directory" per "dirstate.walk" which
2314 2313 # may take place for each patches of "hg qpush", for example
2315 2314 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2316 2315 found = contents.get(part)
2317 2316
2318 2317 result.append(found or part)
2319 2318 dir = os.path.join(dir, part)
2320 2319
2321 2320 return b''.join(result)
2322 2321
2323 2322
2324 2323 def checknlink(testfile):
2325 2324 # type: (bytes) -> bool
2326 2325 '''check whether hardlink count reporting works properly'''
2327 2326
2328 2327 # testfile may be open, so we need a separate file for checking to
2329 2328 # work around issue2543 (or testfile may get lost on Samba shares)
2330 2329 f1, f2, fp = None, None, None
2331 2330 try:
2332 2331 fd, f1 = pycompat.mkstemp(
2333 2332 prefix=b'.%s-' % os.path.basename(testfile),
2334 2333 suffix=b'1~',
2335 2334 dir=os.path.dirname(testfile),
2336 2335 )
2337 2336 os.close(fd)
2338 2337 f2 = b'%s2~' % f1[:-2]
2339 2338
2340 2339 oslink(f1, f2)
2341 2340 # nlinks() may behave differently for files on Windows shares if
2342 2341 # the file is open.
2343 2342 fp = posixfile(f2)
2344 2343 return nlinks(f2) > 1
2345 2344 except OSError:
2346 2345 return False
2347 2346 finally:
2348 2347 if fp is not None:
2349 2348 fp.close()
2350 2349 for f in (f1, f2):
2351 2350 try:
2352 2351 if f is not None:
2353 2352 os.unlink(f)
2354 2353 except OSError:
2355 2354 pass
2356 2355
2357 2356
2358 2357 def endswithsep(path):
2359 2358 # type: (bytes) -> bool
2360 2359 '''Check path ends with os.sep or os.altsep.'''
2361 2360 return bool( # help pytype
2362 2361 path.endswith(pycompat.ossep)
2363 2362 or pycompat.osaltsep
2364 2363 and path.endswith(pycompat.osaltsep)
2365 2364 )
2366 2365
2367 2366
2368 2367 def splitpath(path):
2369 2368 # type: (bytes) -> List[bytes]
2370 2369 """Split path by os.sep.
2371 2370 Note that this function does not use os.altsep because this is
2372 2371 an alternative of simple "xxx.split(os.sep)".
2373 2372 It is recommended to use os.path.normpath() before using this
2374 2373 function if need."""
2375 2374 return path.split(pycompat.ossep)
2376 2375
2377 2376
2378 2377 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2379 2378 """Create a temporary file with the same contents from name
2380 2379
2381 2380 The permission bits are copied from the original file.
2382 2381
2383 2382 If the temporary file is going to be truncated immediately, you
2384 2383 can use emptyok=True as an optimization.
2385 2384
2386 2385 Returns the name of the temporary file.
2387 2386 """
2388 2387 d, fn = os.path.split(name)
2389 2388 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2390 2389 os.close(fd)
2391 2390 # Temporary files are created with mode 0600, which is usually not
2392 2391 # what we want. If the original file already exists, just copy
2393 2392 # its mode. Otherwise, manually obey umask.
2394 2393 copymode(name, temp, createmode, enforcewritable)
2395 2394
2396 2395 if emptyok:
2397 2396 return temp
2398 2397 try:
2399 2398 try:
2400 2399 ifp = posixfile(name, b"rb")
2401 2400 except IOError as inst:
2402 2401 if inst.errno == errno.ENOENT:
2403 2402 return temp
2404 2403 if not getattr(inst, 'filename', None):
2405 2404 inst.filename = name
2406 2405 raise
2407 2406 ofp = posixfile(temp, b"wb")
2408 2407 for chunk in filechunkiter(ifp):
2409 2408 ofp.write(chunk)
2410 2409 ifp.close()
2411 2410 ofp.close()
2412 2411 except: # re-raises
2413 2412 try:
2414 2413 os.unlink(temp)
2415 2414 except OSError:
2416 2415 pass
2417 2416 raise
2418 2417 return temp
2419 2418
2420 2419
2421 2420 class filestat(object):
2422 2421 """help to exactly detect change of a file
2423 2422
2424 2423 'stat' attribute is result of 'os.stat()' if specified 'path'
2425 2424 exists. Otherwise, it is None. This can avoid preparative
2426 2425 'exists()' examination on client side of this class.
2427 2426 """
2428 2427
2429 2428 def __init__(self, stat):
2430 2429 self.stat = stat
2431 2430
2432 2431 @classmethod
2433 2432 def frompath(cls, path):
2434 2433 try:
2435 2434 stat = os.stat(path)
2436 2435 except OSError as err:
2437 2436 if err.errno != errno.ENOENT:
2438 2437 raise
2439 2438 stat = None
2440 2439 return cls(stat)
2441 2440
2442 2441 @classmethod
2443 2442 def fromfp(cls, fp):
2444 2443 stat = os.fstat(fp.fileno())
2445 2444 return cls(stat)
2446 2445
2447 2446 __hash__ = object.__hash__
2448 2447
2449 2448 def __eq__(self, old):
2450 2449 try:
2451 2450 # if ambiguity between stat of new and old file is
2452 2451 # avoided, comparison of size, ctime and mtime is enough
2453 2452 # to exactly detect change of a file regardless of platform
2454 2453 return (
2455 2454 self.stat.st_size == old.stat.st_size
2456 2455 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2457 2456 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2458 2457 )
2459 2458 except AttributeError:
2460 2459 pass
2461 2460 try:
2462 2461 return self.stat is None and old.stat is None
2463 2462 except AttributeError:
2464 2463 return False
2465 2464
2466 2465 def isambig(self, old):
2467 2466 """Examine whether new (= self) stat is ambiguous against old one
2468 2467
2469 2468 "S[N]" below means stat of a file at N-th change:
2470 2469
2471 2470 - S[n-1].ctime < S[n].ctime: can detect change of a file
2472 2471 - S[n-1].ctime == S[n].ctime
2473 2472 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2474 2473 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2475 2474 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2476 2475 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2477 2476
2478 2477 Case (*2) above means that a file was changed twice or more at
2479 2478 same time in sec (= S[n-1].ctime), and comparison of timestamp
2480 2479 is ambiguous.
2481 2480
2482 2481 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2483 2482 timestamp is ambiguous".
2484 2483
2485 2484 But advancing mtime only in case (*2) doesn't work as
2486 2485 expected, because naturally advanced S[n].mtime in case (*1)
2487 2486 might be equal to manually advanced S[n-1 or earlier].mtime.
2488 2487
2489 2488 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2490 2489 treated as ambiguous regardless of mtime, to avoid overlooking
2491 2490 by confliction between such mtime.
2492 2491
2493 2492 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2494 2493 S[n].mtime", even if size of a file isn't changed.
2495 2494 """
2496 2495 try:
2497 2496 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2498 2497 except AttributeError:
2499 2498 return False
2500 2499
2501 2500 def avoidambig(self, path, old):
2502 2501 """Change file stat of specified path to avoid ambiguity
2503 2502
2504 2503 'old' should be previous filestat of 'path'.
2505 2504
2506 2505 This skips avoiding ambiguity, if a process doesn't have
2507 2506 appropriate privileges for 'path'. This returns False in this
2508 2507 case.
2509 2508
2510 2509 Otherwise, this returns True, as "ambiguity is avoided".
2511 2510 """
2512 2511 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2513 2512 try:
2514 2513 os.utime(path, (advanced, advanced))
2515 2514 except OSError as inst:
2516 2515 if inst.errno == errno.EPERM:
2517 2516 # utime() on the file created by another user causes EPERM,
2518 2517 # if a process doesn't have appropriate privileges
2519 2518 return False
2520 2519 raise
2521 2520 return True
2522 2521
2523 2522 def __ne__(self, other):
2524 2523 return not self == other
2525 2524
2526 2525
2527 2526 class atomictempfile(object):
2528 2527 """writable file object that atomically updates a file
2529 2528
2530 2529 All writes will go to a temporary copy of the original file. Call
2531 2530 close() when you are done writing, and atomictempfile will rename
2532 2531 the temporary copy to the original name, making the changes
2533 2532 visible. If the object is destroyed without being closed, all your
2534 2533 writes are discarded.
2535 2534
2536 2535 checkambig argument of constructor is used with filestat, and is
2537 2536 useful only if target file is guarded by any lock (e.g. repo.lock
2538 2537 or repo.wlock).
2539 2538 """
2540 2539
2541 2540 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2542 2541 self.__name = name # permanent name
2543 2542 self._tempname = mktempcopy(
2544 2543 name,
2545 2544 emptyok=(b'w' in mode),
2546 2545 createmode=createmode,
2547 2546 enforcewritable=(b'w' in mode),
2548 2547 )
2549 2548
2550 2549 self._fp = posixfile(self._tempname, mode)
2551 2550 self._checkambig = checkambig
2552 2551
2553 2552 # delegated methods
2554 2553 self.read = self._fp.read
2555 2554 self.write = self._fp.write
2556 2555 self.seek = self._fp.seek
2557 2556 self.tell = self._fp.tell
2558 2557 self.fileno = self._fp.fileno
2559 2558
2560 2559 def close(self):
2561 2560 if not self._fp.closed:
2562 2561 self._fp.close()
2563 2562 filename = localpath(self.__name)
2564 2563 oldstat = self._checkambig and filestat.frompath(filename)
2565 2564 if oldstat and oldstat.stat:
2566 2565 rename(self._tempname, filename)
2567 2566 newstat = filestat.frompath(filename)
2568 2567 if newstat.isambig(oldstat):
2569 2568 # stat of changed file is ambiguous to original one
2570 2569 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2571 2570 os.utime(filename, (advanced, advanced))
2572 2571 else:
2573 2572 rename(self._tempname, filename)
2574 2573
2575 2574 def discard(self):
2576 2575 if not self._fp.closed:
2577 2576 try:
2578 2577 os.unlink(self._tempname)
2579 2578 except OSError:
2580 2579 pass
2581 2580 self._fp.close()
2582 2581
2583 2582 def __del__(self):
2584 2583 if safehasattr(self, '_fp'): # constructor actually did something
2585 2584 self.discard()
2586 2585
2587 2586 def __enter__(self):
2588 2587 return self
2589 2588
2590 2589 def __exit__(self, exctype, excvalue, traceback):
2591 2590 if exctype is not None:
2592 2591 self.discard()
2593 2592 else:
2594 2593 self.close()
2595 2594
2596 2595
2597 2596 def unlinkpath(f, ignoremissing=False, rmdir=True):
2598 2597 # type: (bytes, bool, bool) -> None
2599 2598 """unlink and remove the directory if it is empty"""
2600 2599 if ignoremissing:
2601 2600 tryunlink(f)
2602 2601 else:
2603 2602 unlink(f)
2604 2603 if rmdir:
2605 2604 # try removing directories that might now be empty
2606 2605 try:
2607 2606 removedirs(os.path.dirname(f))
2608 2607 except OSError:
2609 2608 pass
2610 2609
2611 2610
2612 2611 def tryunlink(f):
2613 2612 # type: (bytes) -> None
2614 2613 """Attempt to remove a file, ignoring ENOENT errors."""
2615 2614 try:
2616 2615 unlink(f)
2617 2616 except OSError as e:
2618 2617 if e.errno != errno.ENOENT:
2619 2618 raise
2620 2619
2621 2620
2622 2621 def makedirs(name, mode=None, notindexed=False):
2623 2622 # type: (bytes, Optional[int], bool) -> None
2624 2623 """recursive directory creation with parent mode inheritance
2625 2624
2626 2625 Newly created directories are marked as "not to be indexed by
2627 2626 the content indexing service", if ``notindexed`` is specified
2628 2627 for "write" mode access.
2629 2628 """
2630 2629 try:
2631 2630 makedir(name, notindexed)
2632 2631 except OSError as err:
2633 2632 if err.errno == errno.EEXIST:
2634 2633 return
2635 2634 if err.errno != errno.ENOENT or not name:
2636 2635 raise
2637 2636 parent = os.path.dirname(abspath(name))
2638 2637 if parent == name:
2639 2638 raise
2640 2639 makedirs(parent, mode, notindexed)
2641 2640 try:
2642 2641 makedir(name, notindexed)
2643 2642 except OSError as err:
2644 2643 # Catch EEXIST to handle races
2645 2644 if err.errno == errno.EEXIST:
2646 2645 return
2647 2646 raise
2648 2647 if mode is not None:
2649 2648 os.chmod(name, mode)
2650 2649
2651 2650
2652 2651 def readfile(path):
2653 2652 # type: (bytes) -> bytes
2654 2653 with open(path, b'rb') as fp:
2655 2654 return fp.read()
2656 2655
2657 2656
2658 2657 def writefile(path, text):
2659 2658 # type: (bytes, bytes) -> None
2660 2659 with open(path, b'wb') as fp:
2661 2660 fp.write(text)
2662 2661
2663 2662
2664 2663 def appendfile(path, text):
2665 2664 # type: (bytes, bytes) -> None
2666 2665 with open(path, b'ab') as fp:
2667 2666 fp.write(text)
2668 2667
2669 2668
2670 2669 class chunkbuffer(object):
2671 2670 """Allow arbitrary sized chunks of data to be efficiently read from an
2672 2671 iterator over chunks of arbitrary size."""
2673 2672
2674 2673 def __init__(self, in_iter):
2675 2674 """in_iter is the iterator that's iterating over the input chunks."""
2676 2675
2677 2676 def splitbig(chunks):
2678 2677 for chunk in chunks:
2679 2678 if len(chunk) > 2 ** 20:
2680 2679 pos = 0
2681 2680 while pos < len(chunk):
2682 2681 end = pos + 2 ** 18
2683 2682 yield chunk[pos:end]
2684 2683 pos = end
2685 2684 else:
2686 2685 yield chunk
2687 2686
2688 2687 self.iter = splitbig(in_iter)
2689 2688 self._queue = collections.deque()
2690 2689 self._chunkoffset = 0
2691 2690
2692 2691 def read(self, l=None):
2693 2692 """Read L bytes of data from the iterator of chunks of data.
2694 2693 Returns less than L bytes if the iterator runs dry.
2695 2694
2696 2695 If size parameter is omitted, read everything"""
2697 2696 if l is None:
2698 2697 return b''.join(self.iter)
2699 2698
2700 2699 left = l
2701 2700 buf = []
2702 2701 queue = self._queue
2703 2702 while left > 0:
2704 2703 # refill the queue
2705 2704 if not queue:
2706 2705 target = 2 ** 18
2707 2706 for chunk in self.iter:
2708 2707 queue.append(chunk)
2709 2708 target -= len(chunk)
2710 2709 if target <= 0:
2711 2710 break
2712 2711 if not queue:
2713 2712 break
2714 2713
2715 2714 # The easy way to do this would be to queue.popleft(), modify the
2716 2715 # chunk (if necessary), then queue.appendleft(). However, for cases
2717 2716 # where we read partial chunk content, this incurs 2 dequeue
2718 2717 # mutations and creates a new str for the remaining chunk in the
2719 2718 # queue. Our code below avoids this overhead.
2720 2719
2721 2720 chunk = queue[0]
2722 2721 chunkl = len(chunk)
2723 2722 offset = self._chunkoffset
2724 2723
2725 2724 # Use full chunk.
2726 2725 if offset == 0 and left >= chunkl:
2727 2726 left -= chunkl
2728 2727 queue.popleft()
2729 2728 buf.append(chunk)
2730 2729 # self._chunkoffset remains at 0.
2731 2730 continue
2732 2731
2733 2732 chunkremaining = chunkl - offset
2734 2733
2735 2734 # Use all of unconsumed part of chunk.
2736 2735 if left >= chunkremaining:
2737 2736 left -= chunkremaining
2738 2737 queue.popleft()
2739 2738 # offset == 0 is enabled by block above, so this won't merely
2740 2739 # copy via ``chunk[0:]``.
2741 2740 buf.append(chunk[offset:])
2742 2741 self._chunkoffset = 0
2743 2742
2744 2743 # Partial chunk needed.
2745 2744 else:
2746 2745 buf.append(chunk[offset : offset + left])
2747 2746 self._chunkoffset += left
2748 2747 left -= chunkremaining
2749 2748
2750 2749 return b''.join(buf)
2751 2750
2752 2751
2753 2752 def filechunkiter(f, size=131072, limit=None):
2754 2753 """Create a generator that produces the data in the file size
2755 2754 (default 131072) bytes at a time, up to optional limit (default is
2756 2755 to read all data). Chunks may be less than size bytes if the
2757 2756 chunk is the last chunk in the file, or the file is a socket or
2758 2757 some other type of file that sometimes reads less data than is
2759 2758 requested."""
2760 2759 assert size >= 0
2761 2760 assert limit is None or limit >= 0
2762 2761 while True:
2763 2762 if limit is None:
2764 2763 nbytes = size
2765 2764 else:
2766 2765 nbytes = min(limit, size)
2767 2766 s = nbytes and f.read(nbytes)
2768 2767 if not s:
2769 2768 break
2770 2769 if limit:
2771 2770 limit -= len(s)
2772 2771 yield s
2773 2772
2774 2773
2775 2774 class cappedreader(object):
2776 2775 """A file object proxy that allows reading up to N bytes.
2777 2776
2778 2777 Given a source file object, instances of this type allow reading up to
2779 2778 N bytes from that source file object. Attempts to read past the allowed
2780 2779 limit are treated as EOF.
2781 2780
2782 2781 It is assumed that I/O is not performed on the original file object
2783 2782 in addition to I/O that is performed by this instance. If there is,
2784 2783 state tracking will get out of sync and unexpected results will ensue.
2785 2784 """
2786 2785
2787 2786 def __init__(self, fh, limit):
2788 2787 """Allow reading up to <limit> bytes from <fh>."""
2789 2788 self._fh = fh
2790 2789 self._left = limit
2791 2790
2792 2791 def read(self, n=-1):
2793 2792 if not self._left:
2794 2793 return b''
2795 2794
2796 2795 if n < 0:
2797 2796 n = self._left
2798 2797
2799 2798 data = self._fh.read(min(n, self._left))
2800 2799 self._left -= len(data)
2801 2800 assert self._left >= 0
2802 2801
2803 2802 return data
2804 2803
2805 2804 def readinto(self, b):
2806 2805 res = self.read(len(b))
2807 2806 if res is None:
2808 2807 return None
2809 2808
2810 2809 b[0 : len(res)] = res
2811 2810 return len(res)
2812 2811
2813 2812
2814 2813 def unitcountfn(*unittable):
2815 2814 '''return a function that renders a readable count of some quantity'''
2816 2815
2817 2816 def go(count):
2818 2817 for multiplier, divisor, format in unittable:
2819 2818 if abs(count) >= divisor * multiplier:
2820 2819 return format % (count / float(divisor))
2821 2820 return unittable[-1][2] % count
2822 2821
2823 2822 return go
2824 2823
2825 2824
2826 2825 def processlinerange(fromline, toline):
2827 2826 # type: (int, int) -> Tuple[int, int]
2828 2827 """Check that linerange <fromline>:<toline> makes sense and return a
2829 2828 0-based range.
2830 2829
2831 2830 >>> processlinerange(10, 20)
2832 2831 (9, 20)
2833 2832 >>> processlinerange(2, 1)
2834 2833 Traceback (most recent call last):
2835 2834 ...
2836 2835 ParseError: line range must be positive
2837 2836 >>> processlinerange(0, 5)
2838 2837 Traceback (most recent call last):
2839 2838 ...
2840 2839 ParseError: fromline must be strictly positive
2841 2840 """
2842 2841 if toline - fromline < 0:
2843 2842 raise error.ParseError(_(b"line range must be positive"))
2844 2843 if fromline < 1:
2845 2844 raise error.ParseError(_(b"fromline must be strictly positive"))
2846 2845 return fromline - 1, toline
2847 2846
2848 2847
2849 2848 bytecount = unitcountfn(
2850 2849 (100, 1 << 30, _(b'%.0f GB')),
2851 2850 (10, 1 << 30, _(b'%.1f GB')),
2852 2851 (1, 1 << 30, _(b'%.2f GB')),
2853 2852 (100, 1 << 20, _(b'%.0f MB')),
2854 2853 (10, 1 << 20, _(b'%.1f MB')),
2855 2854 (1, 1 << 20, _(b'%.2f MB')),
2856 2855 (100, 1 << 10, _(b'%.0f KB')),
2857 2856 (10, 1 << 10, _(b'%.1f KB')),
2858 2857 (1, 1 << 10, _(b'%.2f KB')),
2859 2858 (1, 1, _(b'%.0f bytes')),
2860 2859 )
2861 2860
2862 2861
2863 2862 class transformingwriter(object):
2864 2863 """Writable file wrapper to transform data by function"""
2865 2864
2866 2865 def __init__(self, fp, encode):
2867 2866 self._fp = fp
2868 2867 self._encode = encode
2869 2868
2870 2869 def close(self):
2871 2870 self._fp.close()
2872 2871
2873 2872 def flush(self):
2874 2873 self._fp.flush()
2875 2874
2876 2875 def write(self, data):
2877 2876 return self._fp.write(self._encode(data))
2878 2877
2879 2878
2880 2879 # Matches a single EOL which can either be a CRLF where repeated CR
2881 2880 # are removed or a LF. We do not care about old Macintosh files, so a
2882 2881 # stray CR is an error.
2883 2882 _eolre = remod.compile(br'\r*\n')
2884 2883
2885 2884
2886 2885 def tolf(s):
2887 2886 # type: (bytes) -> bytes
2888 2887 return _eolre.sub(b'\n', s)
2889 2888
2890 2889
2891 2890 def tocrlf(s):
2892 2891 # type: (bytes) -> bytes
2893 2892 return _eolre.sub(b'\r\n', s)
2894 2893
2895 2894
2896 2895 def _crlfwriter(fp):
2897 2896 return transformingwriter(fp, tocrlf)
2898 2897
2899 2898
2900 2899 if pycompat.oslinesep == b'\r\n':
2901 2900 tonativeeol = tocrlf
2902 2901 fromnativeeol = tolf
2903 2902 nativeeolwriter = _crlfwriter
2904 2903 else:
2905 2904 tonativeeol = pycompat.identity
2906 2905 fromnativeeol = pycompat.identity
2907 2906 nativeeolwriter = pycompat.identity
2908 2907
2909 2908 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2910 2909 3,
2911 2910 0,
2912 2911 ):
2913 2912 # There is an issue in CPython that some IO methods do not handle EINTR
2914 2913 # correctly. The following table shows what CPython version (and functions)
2915 2914 # are affected (buggy: has the EINTR bug, okay: otherwise):
2916 2915 #
2917 2916 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2918 2917 # --------------------------------------------------
2919 2918 # fp.__iter__ | buggy | buggy | okay
2920 2919 # fp.read* | buggy | okay [1] | okay
2921 2920 #
2922 2921 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2923 2922 #
2924 2923 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2925 2924 # like "read*" work fine, as we do not support Python < 2.7.4.
2926 2925 #
2927 2926 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2928 2927 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2929 2928 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2930 2929 # fp.__iter__ but not other fp.read* methods.
2931 2930 #
2932 2931 # On modern systems like Linux, the "read" syscall cannot be interrupted
2933 2932 # when reading "fast" files like on-disk files. So the EINTR issue only
2934 2933 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2935 2934 # files approximately as "fast" files and use the fast (unsafe) code path,
2936 2935 # to minimize the performance impact.
2937 2936
2938 2937 def iterfile(fp):
2939 2938 fastpath = True
2940 2939 if type(fp) is file:
2941 2940 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2942 2941 if fastpath:
2943 2942 return fp
2944 2943 else:
2945 2944 # fp.readline deals with EINTR correctly, use it as a workaround.
2946 2945 return iter(fp.readline, b'')
2947 2946
2948 2947
2949 2948 else:
2950 2949 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2951 2950 def iterfile(fp):
2952 2951 return fp
2953 2952
2954 2953
2955 2954 def iterlines(iterator):
2956 2955 # type: (Iterator[bytes]) -> Iterator[bytes]
2957 2956 for chunk in iterator:
2958 2957 for line in chunk.splitlines():
2959 2958 yield line
2960 2959
2961 2960
2962 2961 def expandpath(path):
2963 2962 # type: (bytes) -> bytes
2964 2963 return os.path.expanduser(os.path.expandvars(path))
2965 2964
2966 2965
2967 2966 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2968 2967 """Return the result of interpolating items in the mapping into string s.
2969 2968
2970 2969 prefix is a single character string, or a two character string with
2971 2970 a backslash as the first character if the prefix needs to be escaped in
2972 2971 a regular expression.
2973 2972
2974 2973 fn is an optional function that will be applied to the replacement text
2975 2974 just before replacement.
2976 2975
2977 2976 escape_prefix is an optional flag that allows using doubled prefix for
2978 2977 its escaping.
2979 2978 """
2980 2979 fn = fn or (lambda s: s)
2981 2980 patterns = b'|'.join(mapping.keys())
2982 2981 if escape_prefix:
2983 2982 patterns += b'|' + prefix
2984 2983 if len(prefix) > 1:
2985 2984 prefix_char = prefix[1:]
2986 2985 else:
2987 2986 prefix_char = prefix
2988 2987 mapping[prefix_char] = prefix_char
2989 2988 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2990 2989 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2991 2990
2992 2991
2993 2992 timecount = unitcountfn(
2994 2993 (1, 1e3, _(b'%.0f s')),
2995 2994 (100, 1, _(b'%.1f s')),
2996 2995 (10, 1, _(b'%.2f s')),
2997 2996 (1, 1, _(b'%.3f s')),
2998 2997 (100, 0.001, _(b'%.1f ms')),
2999 2998 (10, 0.001, _(b'%.2f ms')),
3000 2999 (1, 0.001, _(b'%.3f ms')),
3001 3000 (100, 0.000001, _(b'%.1f us')),
3002 3001 (10, 0.000001, _(b'%.2f us')),
3003 3002 (1, 0.000001, _(b'%.3f us')),
3004 3003 (100, 0.000000001, _(b'%.1f ns')),
3005 3004 (10, 0.000000001, _(b'%.2f ns')),
3006 3005 (1, 0.000000001, _(b'%.3f ns')),
3007 3006 )
3008 3007
3009 3008
3010 3009 @attr.s
3011 3010 class timedcmstats(object):
3012 3011 """Stats information produced by the timedcm context manager on entering."""
3013 3012
3014 3013 # the starting value of the timer as a float (meaning and resulution is
3015 3014 # platform dependent, see util.timer)
3016 3015 start = attr.ib(default=attr.Factory(lambda: timer()))
3017 3016 # the number of seconds as a floating point value; starts at 0, updated when
3018 3017 # the context is exited.
3019 3018 elapsed = attr.ib(default=0)
3020 3019 # the number of nested timedcm context managers.
3021 3020 level = attr.ib(default=1)
3022 3021
3023 3022 def __bytes__(self):
3024 3023 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3025 3024
3026 3025 __str__ = encoding.strmethod(__bytes__)
3027 3026
3028 3027
3029 3028 @contextlib.contextmanager
3030 3029 def timedcm(whencefmt, *whenceargs):
3031 3030 """A context manager that produces timing information for a given context.
3032 3031
3033 3032 On entering a timedcmstats instance is produced.
3034 3033
3035 3034 This context manager is reentrant.
3036 3035
3037 3036 """
3038 3037 # track nested context managers
3039 3038 timedcm._nested += 1
3040 3039 timing_stats = timedcmstats(level=timedcm._nested)
3041 3040 try:
3042 3041 with tracing.log(whencefmt, *whenceargs):
3043 3042 yield timing_stats
3044 3043 finally:
3045 3044 timing_stats.elapsed = timer() - timing_stats.start
3046 3045 timedcm._nested -= 1
3047 3046
3048 3047
3049 3048 timedcm._nested = 0
3050 3049
3051 3050
3052 3051 def timed(func):
3053 3052 """Report the execution time of a function call to stderr.
3054 3053
3055 3054 During development, use as a decorator when you need to measure
3056 3055 the cost of a function, e.g. as follows:
3057 3056
3058 3057 @util.timed
3059 3058 def foo(a, b, c):
3060 3059 pass
3061 3060 """
3062 3061
3063 3062 def wrapper(*args, **kwargs):
3064 3063 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3065 3064 result = func(*args, **kwargs)
3066 3065 stderr = procutil.stderr
3067 3066 stderr.write(
3068 3067 b'%s%s: %s\n'
3069 3068 % (
3070 3069 b' ' * time_stats.level * 2,
3071 3070 pycompat.bytestr(func.__name__),
3072 3071 time_stats,
3073 3072 )
3074 3073 )
3075 3074 return result
3076 3075
3077 3076 return wrapper
3078 3077
3079 3078
3080 3079 _sizeunits = (
3081 3080 (b'm', 2 ** 20),
3082 3081 (b'k', 2 ** 10),
3083 3082 (b'g', 2 ** 30),
3084 3083 (b'kb', 2 ** 10),
3085 3084 (b'mb', 2 ** 20),
3086 3085 (b'gb', 2 ** 30),
3087 3086 (b'b', 1),
3088 3087 )
3089 3088
3090 3089
3091 3090 def sizetoint(s):
3092 3091 # type: (bytes) -> int
3093 3092 """Convert a space specifier to a byte count.
3094 3093
3095 3094 >>> sizetoint(b'30')
3096 3095 30
3097 3096 >>> sizetoint(b'2.2kb')
3098 3097 2252
3099 3098 >>> sizetoint(b'6M')
3100 3099 6291456
3101 3100 """
3102 3101 t = s.strip().lower()
3103 3102 try:
3104 3103 for k, u in _sizeunits:
3105 3104 if t.endswith(k):
3106 3105 return int(float(t[: -len(k)]) * u)
3107 3106 return int(t)
3108 3107 except ValueError:
3109 3108 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3110 3109
3111 3110
3112 3111 class hooks(object):
3113 3112 """A collection of hook functions that can be used to extend a
3114 3113 function's behavior. Hooks are called in lexicographic order,
3115 3114 based on the names of their sources."""
3116 3115
3117 3116 def __init__(self):
3118 3117 self._hooks = []
3119 3118
3120 3119 def add(self, source, hook):
3121 3120 self._hooks.append((source, hook))
3122 3121
3123 3122 def __call__(self, *args):
3124 3123 self._hooks.sort(key=lambda x: x[0])
3125 3124 results = []
3126 3125 for source, hook in self._hooks:
3127 3126 results.append(hook(*args))
3128 3127 return results
3129 3128
3130 3129
3131 3130 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3132 3131 """Yields lines for a nicely formatted stacktrace.
3133 3132 Skips the 'skip' last entries, then return the last 'depth' entries.
3134 3133 Each file+linenumber is formatted according to fileline.
3135 3134 Each line is formatted according to line.
3136 3135 If line is None, it yields:
3137 3136 length of longest filepath+line number,
3138 3137 filepath+linenumber,
3139 3138 function
3140 3139
3141 3140 Not be used in production code but very convenient while developing.
3142 3141 """
3143 3142 entries = [
3144 3143 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3145 3144 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3146 3145 ][-depth:]
3147 3146 if entries:
3148 3147 fnmax = max(len(entry[0]) for entry in entries)
3149 3148 for fnln, func in entries:
3150 3149 if line is None:
3151 3150 yield (fnmax, fnln, func)
3152 3151 else:
3153 3152 yield line % (fnmax, fnln, func)
3154 3153
3155 3154
3156 3155 def debugstacktrace(
3157 3156 msg=b'stacktrace',
3158 3157 skip=0,
3159 3158 f=procutil.stderr,
3160 3159 otherf=procutil.stdout,
3161 3160 depth=0,
3162 3161 prefix=b'',
3163 3162 ):
3164 3163 """Writes a message to f (stderr) with a nicely formatted stacktrace.
3165 3164 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3166 3165 By default it will flush stdout first.
3167 3166 It can be used everywhere and intentionally does not require an ui object.
3168 3167 Not be used in production code but very convenient while developing.
3169 3168 """
3170 3169 if otherf:
3171 3170 otherf.flush()
3172 3171 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3173 3172 for line in getstackframes(skip + 1, depth=depth):
3174 3173 f.write(prefix + line)
3175 3174 f.flush()
3176 3175
3177 3176
3178 3177 # convenient shortcut
3179 3178 dst = debugstacktrace
3180 3179
3181 3180
3182 3181 def safename(f, tag, ctx, others=None):
3183 3182 """
3184 3183 Generate a name that it is safe to rename f to in the given context.
3185 3184
3186 3185 f: filename to rename
3187 3186 tag: a string tag that will be included in the new name
3188 3187 ctx: a context, in which the new name must not exist
3189 3188 others: a set of other filenames that the new name must not be in
3190 3189
3191 3190 Returns a file name of the form oldname~tag[~number] which does not exist
3192 3191 in the provided context and is not in the set of other names.
3193 3192 """
3194 3193 if others is None:
3195 3194 others = set()
3196 3195
3197 3196 fn = b'%s~%s' % (f, tag)
3198 3197 if fn not in ctx and fn not in others:
3199 3198 return fn
3200 3199 for n in itertools.count(1):
3201 3200 fn = b'%s~%s~%s' % (f, tag, n)
3202 3201 if fn not in ctx and fn not in others:
3203 3202 return fn
3204 3203
3205 3204
3206 3205 def readexactly(stream, n):
3207 3206 '''read n bytes from stream.read and abort if less was available'''
3208 3207 s = stream.read(n)
3209 3208 if len(s) < n:
3210 3209 raise error.Abort(
3211 3210 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3212 3211 % (len(s), n)
3213 3212 )
3214 3213 return s
3215 3214
3216 3215
3217 3216 def uvarintencode(value):
3218 3217 """Encode an unsigned integer value to a varint.
3219 3218
3220 3219 A varint is a variable length integer of 1 or more bytes. Each byte
3221 3220 except the last has the most significant bit set. The lower 7 bits of
3222 3221 each byte store the 2's complement representation, least significant group
3223 3222 first.
3224 3223
3225 3224 >>> uvarintencode(0)
3226 3225 '\\x00'
3227 3226 >>> uvarintencode(1)
3228 3227 '\\x01'
3229 3228 >>> uvarintencode(127)
3230 3229 '\\x7f'
3231 3230 >>> uvarintencode(1337)
3232 3231 '\\xb9\\n'
3233 3232 >>> uvarintencode(65536)
3234 3233 '\\x80\\x80\\x04'
3235 3234 >>> uvarintencode(-1)
3236 3235 Traceback (most recent call last):
3237 3236 ...
3238 3237 ProgrammingError: negative value for uvarint: -1
3239 3238 """
3240 3239 if value < 0:
3241 3240 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3242 3241 bits = value & 0x7F
3243 3242 value >>= 7
3244 3243 bytes = []
3245 3244 while value:
3246 3245 bytes.append(pycompat.bytechr(0x80 | bits))
3247 3246 bits = value & 0x7F
3248 3247 value >>= 7
3249 3248 bytes.append(pycompat.bytechr(bits))
3250 3249
3251 3250 return b''.join(bytes)
3252 3251
3253 3252
3254 3253 def uvarintdecodestream(fh):
3255 3254 """Decode an unsigned variable length integer from a stream.
3256 3255
3257 3256 The passed argument is anything that has a ``.read(N)`` method.
3258 3257
3259 3258 >>> try:
3260 3259 ... from StringIO import StringIO as BytesIO
3261 3260 ... except ImportError:
3262 3261 ... from io import BytesIO
3263 3262 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3264 3263 0
3265 3264 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3266 3265 1
3267 3266 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3268 3267 127
3269 3268 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3270 3269 1337
3271 3270 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3272 3271 65536
3273 3272 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3274 3273 Traceback (most recent call last):
3275 3274 ...
3276 3275 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3277 3276 """
3278 3277 result = 0
3279 3278 shift = 0
3280 3279 while True:
3281 3280 byte = ord(readexactly(fh, 1))
3282 3281 result |= (byte & 0x7F) << shift
3283 3282 if not (byte & 0x80):
3284 3283 return result
3285 3284 shift += 7
3286 3285
3287 3286
3288 3287 # Passing the '' locale means that the locale should be set according to the
3289 3288 # user settings (environment variables).
3290 3289 # Python sometimes avoids setting the global locale settings. When interfacing
3291 3290 # with C code (e.g. the curses module or the Subversion bindings), the global
3292 3291 # locale settings must be initialized correctly. Python 2 does not initialize
3293 3292 # the global locale settings on interpreter startup. Python 3 sometimes
3294 3293 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3295 3294 # explicitly initialize it to get consistent behavior if it's not already
3296 3295 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3297 3296 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3298 3297 # if we can remove this code.
3299 3298 @contextlib.contextmanager
3300 3299 def with_lc_ctype():
3301 3300 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3302 3301 if oldloc == 'C':
3303 3302 try:
3304 3303 try:
3305 3304 locale.setlocale(locale.LC_CTYPE, '')
3306 3305 except locale.Error:
3307 3306 # The likely case is that the locale from the environment
3308 3307 # variables is unknown.
3309 3308 pass
3310 3309 yield
3311 3310 finally:
3312 3311 locale.setlocale(locale.LC_CTYPE, oldloc)
3313 3312 else:
3314 3313 yield
3315 3314
3316 3315
3317 3316 def _estimatememory():
3318 3317 # type: () -> Optional[int]
3319 3318 """Provide an estimate for the available system memory in Bytes.
3320 3319
3321 3320 If no estimate can be provided on the platform, returns None.
3322 3321 """
3323 3322 if pycompat.sysplatform.startswith(b'win'):
3324 3323 # On Windows, use the GlobalMemoryStatusEx kernel function directly.
3325 3324 from ctypes import c_long as DWORD, c_ulonglong as DWORDLONG
3326 3325 from ctypes.wintypes import ( # pytype: disable=import-error
3327 3326 Structure,
3328 3327 byref,
3329 3328 sizeof,
3330 3329 windll,
3331 3330 )
3332 3331
3333 3332 class MEMORYSTATUSEX(Structure):
3334 3333 _fields_ = [
3335 3334 ('dwLength', DWORD),
3336 3335 ('dwMemoryLoad', DWORD),
3337 3336 ('ullTotalPhys', DWORDLONG),
3338 3337 ('ullAvailPhys', DWORDLONG),
3339 3338 ('ullTotalPageFile', DWORDLONG),
3340 3339 ('ullAvailPageFile', DWORDLONG),
3341 3340 ('ullTotalVirtual', DWORDLONG),
3342 3341 ('ullAvailVirtual', DWORDLONG),
3343 3342 ('ullExtendedVirtual', DWORDLONG),
3344 3343 ]
3345 3344
3346 3345 x = MEMORYSTATUSEX()
3347 3346 x.dwLength = sizeof(x)
3348 3347 windll.kernel32.GlobalMemoryStatusEx(byref(x))
3349 3348 return x.ullAvailPhys
3350 3349
3351 3350 # On newer Unix-like systems and Mac OSX, the sysconf interface
3352 3351 # can be used. _SC_PAGE_SIZE is part of POSIX; _SC_PHYS_PAGES
3353 3352 # seems to be implemented on most systems.
3354 3353 try:
3355 3354 pagesize = os.sysconf(os.sysconf_names['SC_PAGE_SIZE'])
3356 3355 pages = os.sysconf(os.sysconf_names['SC_PHYS_PAGES'])
3357 3356 return pagesize * pages
3358 3357 except OSError: # sysconf can fail
3359 3358 pass
3360 3359 except KeyError: # unknown parameter
3361 3360 pass
@@ -1,468 +1,468 b''
1 1 # worker.py - master-slave parallelism support
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 import pickle
12 13 import signal
13 14 import sys
14 15 import threading
15 16 import time
16 17
17 18 try:
18 19 import selectors
19 20
20 21 selectors.BaseSelector
21 22 except ImportError:
22 23 from .thirdparty import selectors2 as selectors
23 24
24 25 from .i18n import _
25 26 from . import (
26 27 encoding,
27 28 error,
28 29 pycompat,
29 30 scmutil,
30 util,
31 31 )
32 32
33 33
34 34 def countcpus():
35 35 '''try to count the number of CPUs on the system'''
36 36
37 37 # posix
38 38 try:
39 39 n = int(os.sysconf('SC_NPROCESSORS_ONLN'))
40 40 if n > 0:
41 41 return n
42 42 except (AttributeError, ValueError):
43 43 pass
44 44
45 45 # windows
46 46 try:
47 47 n = int(encoding.environ[b'NUMBER_OF_PROCESSORS'])
48 48 if n > 0:
49 49 return n
50 50 except (KeyError, ValueError):
51 51 pass
52 52
53 53 return 1
54 54
55 55
56 56 def _numworkers(ui):
57 57 s = ui.config(b'worker', b'numcpus')
58 58 if s:
59 59 try:
60 60 n = int(s)
61 61 if n >= 1:
62 62 return n
63 63 except ValueError:
64 64 raise error.Abort(_(b'number of cpus must be an integer'))
65 65 return min(max(countcpus(), 4), 32)
66 66
67 67
68 68 if pycompat.ispy3:
69 69
70 70 def ismainthread():
71 71 return threading.current_thread() == threading.main_thread()
72 72
73 73 class _blockingreader(object):
74 74 def __init__(self, wrapped):
75 75 self._wrapped = wrapped
76 76
77 77 # Do NOT implement readinto() by making it delegate to
78 78 # _wrapped.readinto(), since that is unbuffered. The unpickler is fine
79 79 # with just read() and readline(), so we don't need to implement it.
80 80
81 81 def readline(self):
82 82 return self._wrapped.readline()
83 83
84 84 # issue multiple reads until size is fulfilled
85 85 def read(self, size=-1):
86 86 if size < 0:
87 87 return self._wrapped.readall()
88 88
89 89 buf = bytearray(size)
90 90 view = memoryview(buf)
91 91 pos = 0
92 92
93 93 while pos < size:
94 94 ret = self._wrapped.readinto(view[pos:])
95 95 if not ret:
96 96 break
97 97 pos += ret
98 98
99 99 del view
100 100 del buf[pos:]
101 101 return bytes(buf)
102 102
103 103
104 104 else:
105 105
106 106 def ismainthread():
107 107 # pytype: disable=module-attr
108 108 return isinstance(threading.current_thread(), threading._MainThread)
109 109 # pytype: enable=module-attr
110 110
111 111 def _blockingreader(wrapped):
112 112 return wrapped
113 113
114 114
115 115 if pycompat.isposix or pycompat.iswindows:
116 116 _STARTUP_COST = 0.01
117 117 # The Windows worker is thread based. If tasks are CPU bound, threads
118 118 # in the presence of the GIL result in excessive context switching and
119 119 # this overhead can slow down execution.
120 120 _DISALLOW_THREAD_UNSAFE = pycompat.iswindows
121 121 else:
122 122 _STARTUP_COST = 1e30
123 123 _DISALLOW_THREAD_UNSAFE = False
124 124
125 125
126 126 def worthwhile(ui, costperop, nops, threadsafe=True):
127 127 """try to determine whether the benefit of multiple processes can
128 128 outweigh the cost of starting them"""
129 129
130 130 if not threadsafe and _DISALLOW_THREAD_UNSAFE:
131 131 return False
132 132
133 133 linear = costperop * nops
134 134 workers = _numworkers(ui)
135 135 benefit = linear - (_STARTUP_COST * workers + linear / workers)
136 136 return benefit >= 0.15
137 137
138 138
139 139 def worker(
140 140 ui, costperarg, func, staticargs, args, hasretval=False, threadsafe=True
141 141 ):
142 142 """run a function, possibly in parallel in multiple worker
143 143 processes.
144 144
145 145 returns a progress iterator
146 146
147 147 costperarg - cost of a single task
148 148
149 149 func - function to run. It is expected to return a progress iterator.
150 150
151 151 staticargs - arguments to pass to every invocation of the function
152 152
153 153 args - arguments to split into chunks, to pass to individual
154 154 workers
155 155
156 156 hasretval - when True, func and the current function return an progress
157 157 iterator then a dict (encoded as an iterator that yield many (False, ..)
158 158 then a (True, dict)). The dicts are joined in some arbitrary order, so
159 159 overlapping keys are a bad idea.
160 160
161 161 threadsafe - whether work items are thread safe and can be executed using
162 162 a thread-based worker. Should be disabled for CPU heavy tasks that don't
163 163 release the GIL.
164 164 """
165 165 enabled = ui.configbool(b'worker', b'enabled')
166 166 if enabled and _platformworker is _posixworker and not ismainthread():
167 167 # The POSIX worker has to install a handler for SIGCHLD.
168 168 # Python up to 3.9 only allows this in the main thread.
169 169 enabled = False
170 170
171 171 if enabled and worthwhile(ui, costperarg, len(args), threadsafe=threadsafe):
172 172 return _platformworker(ui, func, staticargs, args, hasretval)
173 173 return func(*staticargs + (args,))
174 174
175 175
176 176 def _posixworker(ui, func, staticargs, args, hasretval):
177 177 workers = _numworkers(ui)
178 178 oldhandler = signal.getsignal(signal.SIGINT)
179 179 signal.signal(signal.SIGINT, signal.SIG_IGN)
180 180 pids, problem = set(), [0]
181 181
182 182 def killworkers():
183 183 # unregister SIGCHLD handler as all children will be killed. This
184 184 # function shouldn't be interrupted by another SIGCHLD; otherwise pids
185 185 # could be updated while iterating, which would cause inconsistency.
186 186 signal.signal(signal.SIGCHLD, oldchldhandler)
187 187 # if one worker bails, there's no good reason to wait for the rest
188 188 for p in pids:
189 189 try:
190 190 os.kill(p, signal.SIGTERM)
191 191 except OSError as err:
192 192 if err.errno != errno.ESRCH:
193 193 raise
194 194
195 195 def waitforworkers(blocking=True):
196 196 for pid in pids.copy():
197 197 p = st = 0
198 198 while True:
199 199 try:
200 200 p, st = os.waitpid(pid, (0 if blocking else os.WNOHANG))
201 201 break
202 202 except OSError as e:
203 203 if e.errno == errno.EINTR:
204 204 continue
205 205 elif e.errno == errno.ECHILD:
206 206 # child would already be reaped, but pids yet been
207 207 # updated (maybe interrupted just after waitpid)
208 208 pids.discard(pid)
209 209 break
210 210 else:
211 211 raise
212 212 if not p:
213 213 # skip subsequent steps, because child process should
214 214 # be still running in this case
215 215 continue
216 216 pids.discard(p)
217 217 st = _exitstatus(st)
218 218 if st and not problem[0]:
219 219 problem[0] = st
220 220
221 221 def sigchldhandler(signum, frame):
222 222 waitforworkers(blocking=False)
223 223 if problem[0]:
224 224 killworkers()
225 225
226 226 oldchldhandler = signal.signal(signal.SIGCHLD, sigchldhandler)
227 227 ui.flush()
228 228 parentpid = os.getpid()
229 229 pipes = []
230 230 retval = {}
231 231 for pargs in partition(args, min(workers, len(args))):
232 232 # Every worker gets its own pipe to send results on, so we don't have to
233 233 # implement atomic writes larger than PIPE_BUF. Each forked process has
234 234 # its own pipe's descriptors in the local variables, and the parent
235 235 # process has the full list of pipe descriptors (and it doesn't really
236 236 # care what order they're in).
237 237 rfd, wfd = os.pipe()
238 238 pipes.append((rfd, wfd))
239 239 # make sure we use os._exit in all worker code paths. otherwise the
240 240 # worker may do some clean-ups which could cause surprises like
241 241 # deadlock. see sshpeer.cleanup for example.
242 242 # override error handling *before* fork. this is necessary because
243 243 # exception (signal) may arrive after fork, before "pid =" assignment
244 244 # completes, and other exception handler (dispatch.py) can lead to
245 245 # unexpected code path without os._exit.
246 246 ret = -1
247 247 try:
248 248 pid = os.fork()
249 249 if pid == 0:
250 250 signal.signal(signal.SIGINT, oldhandler)
251 251 signal.signal(signal.SIGCHLD, oldchldhandler)
252 252
253 253 def workerfunc():
254 254 for r, w in pipes[:-1]:
255 255 os.close(r)
256 256 os.close(w)
257 257 os.close(rfd)
258 258 for result in func(*(staticargs + (pargs,))):
259 os.write(wfd, util.pickle.dumps(result))
259 os.write(wfd, pickle.dumps(result))
260 260 return 0
261 261
262 262 ret = scmutil.callcatch(ui, workerfunc)
263 263 except: # parent re-raises, child never returns
264 264 if os.getpid() == parentpid:
265 265 raise
266 266 exctype = sys.exc_info()[0]
267 267 force = not issubclass(exctype, KeyboardInterrupt)
268 268 ui.traceback(force=force)
269 269 finally:
270 270 if os.getpid() != parentpid:
271 271 try:
272 272 ui.flush()
273 273 except: # never returns, no re-raises
274 274 pass
275 275 finally:
276 276 os._exit(ret & 255)
277 277 pids.add(pid)
278 278 selector = selectors.DefaultSelector()
279 279 for rfd, wfd in pipes:
280 280 os.close(wfd)
281 281 selector.register(os.fdopen(rfd, 'rb', 0), selectors.EVENT_READ)
282 282
283 283 def cleanup():
284 284 signal.signal(signal.SIGINT, oldhandler)
285 285 waitforworkers()
286 286 signal.signal(signal.SIGCHLD, oldchldhandler)
287 287 selector.close()
288 288 return problem[0]
289 289
290 290 try:
291 291 openpipes = len(pipes)
292 292 while openpipes > 0:
293 293 for key, events in selector.select():
294 294 try:
295 res = util.pickle.load(_blockingreader(key.fileobj))
295 res = pickle.load(_blockingreader(key.fileobj))
296 296 if hasretval and res[0]:
297 297 retval.update(res[1])
298 298 else:
299 299 yield res
300 300 except EOFError:
301 301 selector.unregister(key.fileobj)
302 302 key.fileobj.close()
303 303 openpipes -= 1
304 304 except IOError as e:
305 305 if e.errno == errno.EINTR:
306 306 continue
307 307 raise
308 308 except: # re-raises
309 309 killworkers()
310 310 cleanup()
311 311 raise
312 312 status = cleanup()
313 313 if status:
314 314 if status < 0:
315 315 os.kill(os.getpid(), -status)
316 316 raise error.WorkerError(status)
317 317 if hasretval:
318 318 yield True, retval
319 319
320 320
321 321 def _posixexitstatus(code):
322 322 """convert a posix exit status into the same form returned by
323 323 os.spawnv
324 324
325 325 returns None if the process was stopped instead of exiting"""
326 326 if os.WIFEXITED(code):
327 327 return os.WEXITSTATUS(code)
328 328 elif os.WIFSIGNALED(code):
329 329 return -(os.WTERMSIG(code))
330 330
331 331
332 332 def _windowsworker(ui, func, staticargs, args, hasretval):
333 333 class Worker(threading.Thread):
334 334 def __init__(
335 335 self, taskqueue, resultqueue, func, staticargs, *args, **kwargs
336 336 ):
337 337 threading.Thread.__init__(self, *args, **kwargs)
338 338 self._taskqueue = taskqueue
339 339 self._resultqueue = resultqueue
340 340 self._func = func
341 341 self._staticargs = staticargs
342 342 self._interrupted = False
343 343 self.daemon = True
344 344 self.exception = None
345 345
346 346 def interrupt(self):
347 347 self._interrupted = True
348 348
349 349 def run(self):
350 350 try:
351 351 while not self._taskqueue.empty():
352 352 try:
353 353 args = self._taskqueue.get_nowait()
354 354 for res in self._func(*self._staticargs + (args,)):
355 355 self._resultqueue.put(res)
356 356 # threading doesn't provide a native way to
357 357 # interrupt execution. handle it manually at every
358 358 # iteration.
359 359 if self._interrupted:
360 360 return
361 361 except pycompat.queue.Empty:
362 362 break
363 363 except Exception as e:
364 364 # store the exception such that the main thread can resurface
365 365 # it as if the func was running without workers.
366 366 self.exception = e
367 367 raise
368 368
369 369 threads = []
370 370
371 371 def trykillworkers():
372 372 # Allow up to 1 second to clean worker threads nicely
373 373 cleanupend = time.time() + 1
374 374 for t in threads:
375 375 t.interrupt()
376 376 for t in threads:
377 377 remainingtime = cleanupend - time.time()
378 378 t.join(remainingtime)
379 379 if t.is_alive():
380 380 # pass over the workers joining failure. it is more
381 381 # important to surface the inital exception than the
382 382 # fact that one of workers may be processing a large
383 383 # task and does not get to handle the interruption.
384 384 ui.warn(
385 385 _(
386 386 b"failed to kill worker threads while "
387 387 b"handling an exception\n"
388 388 )
389 389 )
390 390 return
391 391
392 392 workers = _numworkers(ui)
393 393 resultqueue = pycompat.queue.Queue()
394 394 taskqueue = pycompat.queue.Queue()
395 395 retval = {}
396 396 # partition work to more pieces than workers to minimize the chance
397 397 # of uneven distribution of large tasks between the workers
398 398 for pargs in partition(args, workers * 20):
399 399 taskqueue.put(pargs)
400 400 for _i in range(workers):
401 401 t = Worker(taskqueue, resultqueue, func, staticargs)
402 402 threads.append(t)
403 403 t.start()
404 404 try:
405 405 while len(threads) > 0:
406 406 while not resultqueue.empty():
407 407 res = resultqueue.get()
408 408 if hasretval and res[0]:
409 409 retval.update(res[1])
410 410 else:
411 411 yield res
412 412 threads[0].join(0.05)
413 413 finishedthreads = [_t for _t in threads if not _t.is_alive()]
414 414 for t in finishedthreads:
415 415 if t.exception is not None:
416 416 raise t.exception
417 417 threads.remove(t)
418 418 except (Exception, KeyboardInterrupt): # re-raises
419 419 trykillworkers()
420 420 raise
421 421 while not resultqueue.empty():
422 422 res = resultqueue.get()
423 423 if hasretval and res[0]:
424 424 retval.update(res[1])
425 425 else:
426 426 yield res
427 427 if hasretval:
428 428 yield True, retval
429 429
430 430
431 431 if pycompat.iswindows:
432 432 _platformworker = _windowsworker
433 433 else:
434 434 _platformworker = _posixworker
435 435 _exitstatus = _posixexitstatus
436 436
437 437
438 438 def partition(lst, nslices):
439 439 """partition a list into N slices of roughly equal size
440 440
441 441 The current strategy takes every Nth element from the input. If
442 442 we ever write workers that need to preserve grouping in input
443 443 we should consider allowing callers to specify a partition strategy.
444 444
445 445 olivia is not a fan of this partitioning strategy when files are involved.
446 446 In his words:
447 447
448 448 Single-threaded Mercurial makes a point of creating and visiting
449 449 files in a fixed order (alphabetical). When creating files in order,
450 450 a typical filesystem is likely to allocate them on nearby regions on
451 451 disk. Thus, when revisiting in the same order, locality is maximized
452 452 and various forms of OS and disk-level caching and read-ahead get a
453 453 chance to work.
454 454
455 455 This effect can be quite significant on spinning disks. I discovered it
456 456 circa Mercurial v0.4 when revlogs were named by hashes of filenames.
457 457 Tarring a repo and copying it to another disk effectively randomized
458 458 the revlog ordering on disk by sorting the revlogs by hash and suddenly
459 459 performance of my kernel checkout benchmark dropped by ~10x because the
460 460 "working set" of sectors visited no longer fit in the drive's cache and
461 461 the workload switched from streaming to random I/O.
462 462
463 463 What we should really be doing is have workers read filenames from a
464 464 ordered queue. This preserves locality and also keeps any worker from
465 465 getting more than one file out of balance.
466 466 """
467 467 for i in range(nslices):
468 468 yield lst[i::nslices]
@@ -1,973 +1,973 b''
1 1 #testcases dirstate-v1 dirstate-v2
2 2
3 3 #if dirstate-v2
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [format]
6 6 > use-dirstate-v2=1
7 7 > [storage]
8 8 > dirstate-v2.slow-path=allow
9 9 > EOF
10 10 #endif
11 11
12 12 $ hg init repo1
13 13 $ cd repo1
14 14 $ mkdir a b a/1 b/1 b/2
15 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 17 hg status in repo root:
18 18
19 19 $ hg status
20 20 ? a/1/in_a_1
21 21 ? a/in_a
22 22 ? b/1/in_b_1
23 23 ? b/2/in_b_2
24 24 ? b/in_b
25 25 ? in_root
26 26
27 27 hg status . in repo root:
28 28
29 29 $ hg status .
30 30 ? a/1/in_a_1
31 31 ? a/in_a
32 32 ? b/1/in_b_1
33 33 ? b/2/in_b_2
34 34 ? b/in_b
35 35 ? in_root
36 36
37 37 $ hg status --cwd a
38 38 ? a/1/in_a_1
39 39 ? a/in_a
40 40 ? b/1/in_b_1
41 41 ? b/2/in_b_2
42 42 ? b/in_b
43 43 ? in_root
44 44 $ hg status --cwd a .
45 45 ? 1/in_a_1
46 46 ? in_a
47 47 $ hg status --cwd a ..
48 48 ? 1/in_a_1
49 49 ? in_a
50 50 ? ../b/1/in_b_1
51 51 ? ../b/2/in_b_2
52 52 ? ../b/in_b
53 53 ? ../in_root
54 54
55 55 $ hg status --cwd b
56 56 ? a/1/in_a_1
57 57 ? a/in_a
58 58 ? b/1/in_b_1
59 59 ? b/2/in_b_2
60 60 ? b/in_b
61 61 ? in_root
62 62 $ hg status --cwd b .
63 63 ? 1/in_b_1
64 64 ? 2/in_b_2
65 65 ? in_b
66 66 $ hg status --cwd b ..
67 67 ? ../a/1/in_a_1
68 68 ? ../a/in_a
69 69 ? 1/in_b_1
70 70 ? 2/in_b_2
71 71 ? in_b
72 72 ? ../in_root
73 73
74 74 $ hg status --cwd a/1
75 75 ? a/1/in_a_1
76 76 ? a/in_a
77 77 ? b/1/in_b_1
78 78 ? b/2/in_b_2
79 79 ? b/in_b
80 80 ? in_root
81 81 $ hg status --cwd a/1 .
82 82 ? in_a_1
83 83 $ hg status --cwd a/1 ..
84 84 ? in_a_1
85 85 ? ../in_a
86 86
87 87 $ hg status --cwd b/1
88 88 ? a/1/in_a_1
89 89 ? a/in_a
90 90 ? b/1/in_b_1
91 91 ? b/2/in_b_2
92 92 ? b/in_b
93 93 ? in_root
94 94 $ hg status --cwd b/1 .
95 95 ? in_b_1
96 96 $ hg status --cwd b/1 ..
97 97 ? in_b_1
98 98 ? ../2/in_b_2
99 99 ? ../in_b
100 100
101 101 $ hg status --cwd b/2
102 102 ? a/1/in_a_1
103 103 ? a/in_a
104 104 ? b/1/in_b_1
105 105 ? b/2/in_b_2
106 106 ? b/in_b
107 107 ? in_root
108 108 $ hg status --cwd b/2 .
109 109 ? in_b_2
110 110 $ hg status --cwd b/2 ..
111 111 ? ../1/in_b_1
112 112 ? in_b_2
113 113 ? ../in_b
114 114
115 115 combining patterns with root and patterns without a root works
116 116
117 117 $ hg st a/in_a re:.*b$
118 118 ? a/in_a
119 119 ? b/in_b
120 120
121 121 tweaking defaults works
122 122 $ hg status --cwd a --config ui.tweakdefaults=yes
123 123 ? 1/in_a_1
124 124 ? in_a
125 125 ? ../b/1/in_b_1
126 126 ? ../b/2/in_b_2
127 127 ? ../b/in_b
128 128 ? ../in_root
129 129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
130 130 ? a/1/in_a_1 (glob)
131 131 ? a/in_a (glob)
132 132 ? b/1/in_b_1 (glob)
133 133 ? b/2/in_b_2 (glob)
134 134 ? b/in_b (glob)
135 135 ? in_root
136 136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
137 137 ? 1/in_a_1
138 138 ? in_a
139 139 ? ../b/1/in_b_1
140 140 ? ../b/2/in_b_2
141 141 ? ../b/in_b
142 142 ? ../in_root (glob)
143 143
144 144 relative paths can be requested
145 145
146 146 $ hg status --cwd a --config ui.relative-paths=yes
147 147 ? 1/in_a_1
148 148 ? in_a
149 149 ? ../b/1/in_b_1
150 150 ? ../b/2/in_b_2
151 151 ? ../b/in_b
152 152 ? ../in_root
153 153
154 154 $ hg status --cwd a . --config ui.relative-paths=legacy
155 155 ? 1/in_a_1
156 156 ? in_a
157 157 $ hg status --cwd a . --config ui.relative-paths=no
158 158 ? a/1/in_a_1
159 159 ? a/in_a
160 160
161 161 commands.status.relative overrides ui.relative-paths
162 162
163 163 $ cat >> $HGRCPATH <<EOF
164 164 > [ui]
165 165 > relative-paths = False
166 166 > [commands]
167 167 > status.relative = True
168 168 > EOF
169 169 $ hg status --cwd a
170 170 ? 1/in_a_1
171 171 ? in_a
172 172 ? ../b/1/in_b_1
173 173 ? ../b/2/in_b_2
174 174 ? ../b/in_b
175 175 ? ../in_root
176 176 $ HGPLAIN=1 hg status --cwd a
177 177 ? a/1/in_a_1 (glob)
178 178 ? a/in_a (glob)
179 179 ? b/1/in_b_1 (glob)
180 180 ? b/2/in_b_2 (glob)
181 181 ? b/in_b (glob)
182 182 ? in_root
183 183
184 184 if relative paths are explicitly off, tweakdefaults doesn't change it
185 185 $ cat >> $HGRCPATH <<EOF
186 186 > [commands]
187 187 > status.relative = False
188 188 > EOF
189 189 $ hg status --cwd a --config ui.tweakdefaults=yes
190 190 ? a/1/in_a_1
191 191 ? a/in_a
192 192 ? b/1/in_b_1
193 193 ? b/2/in_b_2
194 194 ? b/in_b
195 195 ? in_root
196 196
197 197 $ cd ..
198 198
199 199 $ hg init repo2
200 200 $ cd repo2
201 201 $ touch modified removed deleted ignored
202 202 $ echo "^ignored$" > .hgignore
203 203 $ hg ci -A -m 'initial checkin'
204 204 adding .hgignore
205 205 adding deleted
206 206 adding modified
207 207 adding removed
208 208 $ touch modified added unknown ignored
209 209 $ hg add added
210 210 $ hg remove removed
211 211 $ rm deleted
212 212
213 213 hg status:
214 214
215 215 $ hg status
216 216 A added
217 217 R removed
218 218 ! deleted
219 219 ? unknown
220 220
221 221 hg status -n:
222 222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
223 223 added
224 224 removed
225 225 deleted
226 226 unknown
227 227
228 228 hg status modified added removed deleted unknown never-existed ignored:
229 229
230 230 $ hg status modified added removed deleted unknown never-existed ignored
231 231 never-existed: * (glob)
232 232 A added
233 233 R removed
234 234 ! deleted
235 235 ? unknown
236 236
237 237 $ hg copy modified copied
238 238
239 239 hg status -C:
240 240
241 241 $ hg status -C
242 242 A added
243 243 A copied
244 244 modified
245 245 R removed
246 246 ! deleted
247 247 ? unknown
248 248
249 249 hg status -A:
250 250
251 251 $ hg status -A
252 252 A added
253 253 A copied
254 254 modified
255 255 R removed
256 256 ! deleted
257 257 ? unknown
258 258 I ignored
259 259 C .hgignore
260 260 C modified
261 261
262 262 $ hg status -A -T '{status} {path} {node|shortest}\n'
263 263 A added ffff
264 264 A copied ffff
265 265 R removed ffff
266 266 ! deleted ffff
267 267 ? unknown ffff
268 268 I ignored ffff
269 269 C .hgignore ffff
270 270 C modified ffff
271 271
272 272 $ hg status -A -Tjson
273 273 [
274 274 {
275 275 "itemtype": "file",
276 276 "path": "added",
277 277 "status": "A"
278 278 },
279 279 {
280 280 "itemtype": "file",
281 281 "path": "copied",
282 282 "source": "modified",
283 283 "status": "A"
284 284 },
285 285 {
286 286 "itemtype": "file",
287 287 "path": "removed",
288 288 "status": "R"
289 289 },
290 290 {
291 291 "itemtype": "file",
292 292 "path": "deleted",
293 293 "status": "!"
294 294 },
295 295 {
296 296 "itemtype": "file",
297 297 "path": "unknown",
298 298 "status": "?"
299 299 },
300 300 {
301 301 "itemtype": "file",
302 302 "path": "ignored",
303 303 "status": "I"
304 304 },
305 305 {
306 306 "itemtype": "file",
307 307 "path": ".hgignore",
308 308 "status": "C"
309 309 },
310 310 {
311 311 "itemtype": "file",
312 312 "path": "modified",
313 313 "status": "C"
314 314 }
315 315 ]
316 316
317 317 $ hg status -A -Tpickle > pickle
318 318 >>> from __future__ import print_function
319 >>> import pickle
319 320 >>> from mercurial import util
320 >>> pickle = util.pickle
321 321 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
322 322 >>> for s, p in data: print("%s %s" % (s, p))
323 323 ! deleted
324 324 ? pickle
325 325 ? unknown
326 326 A added
327 327 A copied
328 328 C .hgignore
329 329 C modified
330 330 I ignored
331 331 R removed
332 332 $ rm pickle
333 333
334 334 $ echo "^ignoreddir$" > .hgignore
335 335 $ mkdir ignoreddir
336 336 $ touch ignoreddir/file
337 337
338 338 Test templater support:
339 339
340 340 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
341 341 [M] .hgignore
342 342 [A] added
343 343 [A] modified -> copied
344 344 [R] removed
345 345 [!] deleted
346 346 [?] ignored
347 347 [?] unknown
348 348 [I] ignoreddir/file
349 349 [C] modified
350 350 $ hg status -AT default
351 351 M .hgignore
352 352 A added
353 353 A copied
354 354 modified
355 355 R removed
356 356 ! deleted
357 357 ? ignored
358 358 ? unknown
359 359 I ignoreddir/file
360 360 C modified
361 361 $ hg status -T compact
362 362 abort: "status" not in template map
363 363 [255]
364 364
365 365 hg status ignoreddir/file:
366 366
367 367 $ hg status ignoreddir/file
368 368
369 369 hg status -i ignoreddir/file:
370 370
371 371 $ hg status -i ignoreddir/file
372 372 I ignoreddir/file
373 373 $ cd ..
374 374
375 375 Check 'status -q' and some combinations
376 376
377 377 $ hg init repo3
378 378 $ cd repo3
379 379 $ touch modified removed deleted ignored
380 380 $ echo "^ignored$" > .hgignore
381 381 $ hg commit -A -m 'initial checkin'
382 382 adding .hgignore
383 383 adding deleted
384 384 adding modified
385 385 adding removed
386 386 $ touch added unknown ignored
387 387 $ hg add added
388 388 $ echo "test" >> modified
389 389 $ hg remove removed
390 390 $ rm deleted
391 391 $ hg copy modified copied
392 392
393 393 Specify working directory revision explicitly, that should be the same as
394 394 "hg status"
395 395
396 396 $ hg status --change "wdir()"
397 397 M modified
398 398 A added
399 399 A copied
400 400 R removed
401 401 ! deleted
402 402 ? unknown
403 403
404 404 Run status with 2 different flags.
405 405 Check if result is the same or different.
406 406 If result is not as expected, raise error
407 407
408 408 $ assert() {
409 409 > hg status $1 > ../a
410 410 > hg status $2 > ../b
411 411 > if diff ../a ../b > /dev/null; then
412 412 > out=0
413 413 > else
414 414 > out=1
415 415 > fi
416 416 > if [ $3 -eq 0 ]; then
417 417 > df="same"
418 418 > else
419 419 > df="different"
420 420 > fi
421 421 > if [ $out -ne $3 ]; then
422 422 > echo "Error on $1 and $2, should be $df."
423 423 > fi
424 424 > }
425 425
426 426 Assert flag1 flag2 [0-same | 1-different]
427 427
428 428 $ assert "-q" "-mard" 0
429 429 $ assert "-A" "-marduicC" 0
430 430 $ assert "-qA" "-mardcC" 0
431 431 $ assert "-qAui" "-A" 0
432 432 $ assert "-qAu" "-marducC" 0
433 433 $ assert "-qAi" "-mardicC" 0
434 434 $ assert "-qu" "-u" 0
435 435 $ assert "-q" "-u" 1
436 436 $ assert "-m" "-a" 1
437 437 $ assert "-r" "-d" 1
438 438 $ cd ..
439 439
440 440 $ hg init repo4
441 441 $ cd repo4
442 442 $ touch modified removed deleted
443 443 $ hg ci -q -A -m 'initial checkin'
444 444 $ touch added unknown
445 445 $ hg add added
446 446 $ hg remove removed
447 447 $ rm deleted
448 448 $ echo x > modified
449 449 $ hg copy modified copied
450 450 $ hg ci -m 'test checkin' -d "1000001 0"
451 451 $ rm *
452 452 $ touch unrelated
453 453 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
454 454
455 455 hg status --change 1:
456 456
457 457 $ hg status --change 1
458 458 M modified
459 459 A added
460 460 A copied
461 461 R removed
462 462
463 463 hg status --change 1 unrelated:
464 464
465 465 $ hg status --change 1 unrelated
466 466
467 467 hg status -C --change 1 added modified copied removed deleted:
468 468
469 469 $ hg status -C --change 1 added modified copied removed deleted
470 470 M modified
471 471 A added
472 472 A copied
473 473 modified
474 474 R removed
475 475
476 476 hg status -A --change 1 and revset:
477 477
478 478 $ hg status -A --change '1|1'
479 479 M modified
480 480 A added
481 481 A copied
482 482 modified
483 483 R removed
484 484 C deleted
485 485
486 486 $ cd ..
487 487
488 488 hg status with --rev and reverted changes:
489 489
490 490 $ hg init reverted-changes-repo
491 491 $ cd reverted-changes-repo
492 492 $ echo a > file
493 493 $ hg add file
494 494 $ hg ci -m a
495 495 $ echo b > file
496 496 $ hg ci -m b
497 497
498 498 reverted file should appear clean
499 499
500 500 $ hg revert -r 0 .
501 501 reverting file
502 502 $ hg status -A --rev 0
503 503 C file
504 504
505 505 #if execbit
506 506 reverted file with changed flag should appear modified
507 507
508 508 $ chmod +x file
509 509 $ hg status -A --rev 0
510 510 M file
511 511
512 512 $ hg revert -r 0 .
513 513 reverting file
514 514
515 515 reverted and committed file with changed flag should appear modified
516 516
517 517 $ hg co -C .
518 518 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
519 519 $ chmod +x file
520 520 $ hg ci -m 'change flag'
521 521 $ hg status -A --rev 1 --rev 2
522 522 M file
523 523 $ hg diff -r 1 -r 2
524 524
525 525 #endif
526 526
527 527 $ cd ..
528 528
529 529 hg status of binary file starting with '\1\n', a separator for metadata:
530 530
531 531 $ hg init repo5
532 532 $ cd repo5
533 533 >>> open("010a", r"wb").write(b"\1\nfoo") and None
534 534 $ hg ci -q -A -m 'initial checkin'
535 535 $ hg status -A
536 536 C 010a
537 537
538 538 >>> open("010a", r"wb").write(b"\1\nbar") and None
539 539 $ hg status -A
540 540 M 010a
541 541 $ hg ci -q -m 'modify 010a'
542 542 $ hg status -A --rev 0:1
543 543 M 010a
544 544
545 545 $ touch empty
546 546 $ hg ci -q -A -m 'add another file'
547 547 $ hg status -A --rev 1:2 010a
548 548 C 010a
549 549
550 550 $ cd ..
551 551
552 552 test "hg status" with "directory pattern" which matches against files
553 553 only known on target revision.
554 554
555 555 $ hg init repo6
556 556 $ cd repo6
557 557
558 558 $ echo a > a.txt
559 559 $ hg add a.txt
560 560 $ hg commit -m '#0'
561 561 $ mkdir -p 1/2/3/4/5
562 562 $ echo b > 1/2/3/4/5/b.txt
563 563 $ hg add 1/2/3/4/5/b.txt
564 564 $ hg commit -m '#1'
565 565
566 566 $ hg update -C 0 > /dev/null
567 567 $ hg status -A
568 568 C a.txt
569 569
570 570 the directory matching against specified pattern should be removed,
571 571 because directory existence prevents 'dirstate.walk()' from showing
572 572 warning message about such pattern.
573 573
574 574 $ test ! -d 1
575 575 $ hg status -A --rev 1 1/2/3/4/5/b.txt
576 576 R 1/2/3/4/5/b.txt
577 577 $ hg status -A --rev 1 1/2/3/4/5
578 578 R 1/2/3/4/5/b.txt
579 579 $ hg status -A --rev 1 1/2/3
580 580 R 1/2/3/4/5/b.txt
581 581 $ hg status -A --rev 1 1
582 582 R 1/2/3/4/5/b.txt
583 583
584 584 $ hg status --config ui.formatdebug=True --rev 1 1
585 585 status = [
586 586 {
587 587 'itemtype': 'file',
588 588 'path': '1/2/3/4/5/b.txt',
589 589 'status': 'R'
590 590 },
591 591 ]
592 592
593 593 #if windows
594 594 $ hg --config ui.slash=false status -A --rev 1 1
595 595 R 1\2\3\4\5\b.txt
596 596 #endif
597 597
598 598 $ cd ..
599 599
600 600 Status after move overwriting a file (issue4458)
601 601 =================================================
602 602
603 603
604 604 $ hg init issue4458
605 605 $ cd issue4458
606 606 $ echo a > a
607 607 $ echo b > b
608 608 $ hg commit -Am base
609 609 adding a
610 610 adding b
611 611
612 612
613 613 with --force
614 614
615 615 $ hg mv b --force a
616 616 $ hg st --copies
617 617 M a
618 618 b
619 619 R b
620 620 $ hg revert --all
621 621 reverting a
622 622 undeleting b
623 623 $ rm *.orig
624 624
625 625 without force
626 626
627 627 $ hg rm a
628 628 $ hg st --copies
629 629 R a
630 630 $ hg mv b a
631 631 $ hg st --copies
632 632 M a
633 633 b
634 634 R b
635 635
636 636 using ui.statuscopies setting
637 637 $ hg st --config ui.statuscopies=true
638 638 M a
639 639 b
640 640 R b
641 641 $ hg st --config ui.statuscopies=false
642 642 M a
643 643 R b
644 644 $ hg st --config ui.tweakdefaults=yes
645 645 M a
646 646 b
647 647 R b
648 648
649 649 using log status template (issue5155)
650 650 $ hg log -Tstatus -r 'wdir()' -C
651 651 changeset: 2147483647:ffffffffffff
652 652 parent: 0:8c55c58b4c0e
653 653 user: test
654 654 date: * (glob)
655 655 files:
656 656 M a
657 657 b
658 658 R b
659 659
660 660 $ hg log -GTstatus -r 'wdir()' -C
661 661 o changeset: 2147483647:ffffffffffff
662 662 | parent: 0:8c55c58b4c0e
663 663 ~ user: test
664 664 date: * (glob)
665 665 files:
666 666 M a
667 667 b
668 668 R b
669 669
670 670
671 671 Other "bug" highlight, the revision status does not report the copy information.
672 672 This is buggy behavior.
673 673
674 674 $ hg commit -m 'blah'
675 675 $ hg st --copies --change .
676 676 M a
677 677 R b
678 678
679 679 using log status template, the copy information is displayed correctly.
680 680 $ hg log -Tstatus -r. -C
681 681 changeset: 1:6685fde43d21
682 682 tag: tip
683 683 user: test
684 684 date: * (glob)
685 685 summary: blah
686 686 files:
687 687 M a
688 688 b
689 689 R b
690 690
691 691
692 692 $ cd ..
693 693
694 694 Make sure .hg doesn't show up even as a symlink
695 695
696 696 $ hg init repo0
697 697 $ mkdir symlink-repo0
698 698 $ cd symlink-repo0
699 699 $ ln -s ../repo0/.hg
700 700 $ hg status
701 701
702 702 If the size hasn’t changed but mtime has, status needs to read the contents
703 703 of the file to check whether it has changed
704 704
705 705 $ echo 1 > a
706 706 $ echo 1 > b
707 707 $ touch -t 200102030000 a b
708 708 $ hg commit -Aqm '#0'
709 709 $ echo 2 > a
710 710 $ touch -t 200102040000 a b
711 711 $ hg status
712 712 M a
713 713
714 714 Asking specifically for the status of a deleted/removed file
715 715
716 716 $ rm a
717 717 $ rm b
718 718 $ hg status a
719 719 ! a
720 720 $ hg rm a
721 721 $ hg rm b
722 722 $ hg status a
723 723 R a
724 724 $ hg commit -qm '#1'
725 725 $ hg status a
726 726 a: $ENOENT$
727 727
728 728 Check using include flag with pattern when status does not need to traverse
729 729 the working directory (issue6483)
730 730
731 731 $ cd ..
732 732 $ hg init issue6483
733 733 $ cd issue6483
734 734 $ touch a.py b.rs
735 735 $ hg add a.py b.rs
736 736 $ hg st -aI "*.py"
737 737 A a.py
738 738
739 739 Also check exclude pattern
740 740
741 741 $ hg st -aX "*.rs"
742 742 A a.py
743 743
744 744 issue6335
745 745 When a directory containing a tracked file gets symlinked, as of 5.8
746 746 `hg st` only gives the correct answer about clean (or deleted) files
747 747 if also listing unknowns.
748 748 The tree-based dirstate and status algorithm fix this:
749 749
750 750 #if symlink no-dirstate-v1 rust
751 751
752 752 $ cd ..
753 753 $ hg init issue6335
754 754 $ cd issue6335
755 755 $ mkdir foo
756 756 $ touch foo/a
757 757 $ hg ci -Ama
758 758 adding foo/a
759 759 $ mv foo bar
760 760 $ ln -s bar foo
761 761 $ hg status
762 762 ! foo/a
763 763 ? bar/a
764 764 ? foo
765 765
766 766 $ hg status -c # incorrect output without the Rust implementation
767 767 $ hg status -cu
768 768 ? bar/a
769 769 ? foo
770 770 $ hg status -d # incorrect output without the Rust implementation
771 771 ! foo/a
772 772 $ hg status -du
773 773 ! foo/a
774 774 ? bar/a
775 775 ? foo
776 776
777 777 #endif
778 778
779 779
780 780 Create a repo with files in each possible status
781 781
782 782 $ cd ..
783 783 $ hg init repo7
784 784 $ cd repo7
785 785 $ mkdir subdir
786 786 $ touch clean modified deleted removed
787 787 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
788 788 $ echo ignored > .hgignore
789 789 $ hg ci -Aqm '#0'
790 790 $ echo 1 > modified
791 791 $ echo 1 > subdir/modified
792 792 $ rm deleted
793 793 $ rm subdir/deleted
794 794 $ hg rm removed
795 795 $ hg rm subdir/removed
796 796 $ touch unknown ignored
797 797 $ touch subdir/unknown subdir/ignored
798 798
799 799 Check the output
800 800
801 801 $ hg status
802 802 M modified
803 803 M subdir/modified
804 804 R removed
805 805 R subdir/removed
806 806 ! deleted
807 807 ! subdir/deleted
808 808 ? subdir/unknown
809 809 ? unknown
810 810
811 811 $ hg status -mard
812 812 M modified
813 813 M subdir/modified
814 814 R removed
815 815 R subdir/removed
816 816 ! deleted
817 817 ! subdir/deleted
818 818
819 819 $ hg status -A
820 820 M modified
821 821 M subdir/modified
822 822 R removed
823 823 R subdir/removed
824 824 ! deleted
825 825 ! subdir/deleted
826 826 ? subdir/unknown
827 827 ? unknown
828 828 I ignored
829 829 I subdir/ignored
830 830 C .hgignore
831 831 C clean
832 832 C subdir/clean
833 833
834 834 Note: `hg status some-name` creates a patternmatcher which is not supported
835 835 yet by the Rust implementation of status, but includematcher is supported.
836 836 --include is used below for that reason
837 837
838 838 #if unix-permissions
839 839
840 840 Not having permission to read a directory that contains tracked files makes
841 841 status emit a warning then behave as if the directory was empty or removed
842 842 entirely:
843 843
844 844 $ chmod 0 subdir
845 845 $ hg status --include subdir
846 846 subdir: Permission denied
847 847 R subdir/removed
848 848 ! subdir/clean
849 849 ! subdir/deleted
850 850 ! subdir/modified
851 851 $ chmod 755 subdir
852 852
853 853 #endif
854 854
855 855 Remove a directory that contains tracked files
856 856
857 857 $ rm -r subdir
858 858 $ hg status --include subdir
859 859 R subdir/removed
860 860 ! subdir/clean
861 861 ! subdir/deleted
862 862 ! subdir/modified
863 863
864 864 … and replace it by a file
865 865
866 866 $ touch subdir
867 867 $ hg status --include subdir
868 868 R subdir/removed
869 869 ! subdir/clean
870 870 ! subdir/deleted
871 871 ! subdir/modified
872 872 ? subdir
873 873
874 874 Replaced a deleted or removed file with a directory
875 875
876 876 $ mkdir deleted removed
877 877 $ touch deleted/1 removed/1
878 878 $ hg status --include deleted --include removed
879 879 R removed
880 880 ! deleted
881 881 ? deleted/1
882 882 ? removed/1
883 883 $ hg add removed/1
884 884 $ hg status --include deleted --include removed
885 885 A removed/1
886 886 R removed
887 887 ! deleted
888 888 ? deleted/1
889 889
890 890 Deeply nested files in an ignored directory are still listed on request
891 891
892 892 $ echo ignored-dir >> .hgignore
893 893 $ mkdir ignored-dir
894 894 $ mkdir ignored-dir/subdir
895 895 $ touch ignored-dir/subdir/1
896 896 $ hg status --ignored
897 897 I ignored
898 898 I ignored-dir/subdir/1
899 899
900 900 Check using include flag while listing ignored composes correctly (issue6514)
901 901
902 902 $ cd ..
903 903 $ hg init issue6514
904 904 $ cd issue6514
905 905 $ mkdir ignored-folder
906 906 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
907 907 $ cat >.hgignore <<EOF
908 908 > A.hs
909 909 > B.hs
910 910 > ignored-folder/
911 911 > EOF
912 912 $ hg st -i -I 're:.*\.hs$'
913 913 I A.hs
914 914 I B.hs
915 915 I ignored-folder/ctest.hs
916 916
917 917 #if rust dirstate-v2
918 918
919 919 Check read_dir caching
920 920
921 921 $ cd ..
922 922 $ hg init repo8
923 923 $ cd repo8
924 924 $ mkdir subdir
925 925 $ touch subdir/a subdir/b
926 926 $ hg ci -Aqm '#0'
927 927
928 928 The cached mtime is initially unset
929 929
930 930 $ hg debugdirstate --all --no-dates | grep '^ '
931 931 0 -1 unset subdir
932 932
933 933 It is still not set when there are unknown files
934 934
935 935 $ touch subdir/unknown
936 936 $ hg status
937 937 ? subdir/unknown
938 938 $ hg debugdirstate --all --no-dates | grep '^ '
939 939 0 -1 unset subdir
940 940
941 941 Now the directory is eligible for caching, so its mtime is save in the dirstate
942 942
943 943 $ rm subdir/unknown
944 944 $ sleep 0.1 # ensure the kernel’s internal clock for mtimes has ticked
945 945 $ hg status
946 946 $ hg debugdirstate --all --no-dates | grep '^ '
947 947 0 -1 set subdir
948 948
949 949 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
950 950
951 951 $ hg status
952 952
953 953 Creating a new file changes the directory’s mtime, invalidating the cache
954 954
955 955 $ touch subdir/unknown
956 956 $ hg status
957 957 ? subdir/unknown
958 958
959 959 $ rm subdir/unknown
960 960 $ hg status
961 961
962 962 Removing a node from the dirstate resets the cache for its parent directory
963 963
964 964 $ hg forget subdir/a
965 965 $ hg debugdirstate --all --no-dates | grep '^ '
966 966 0 -1 set subdir
967 967 $ hg ci -qm '#1'
968 968 $ hg debugdirstate --all --no-dates | grep '^ '
969 969 0 -1 unset subdir
970 970 $ hg status
971 971 ? subdir/a
972 972
973 973 #endif
@@ -1,365 +1,366 b''
1 1 #!/usr/bin/env python
2 2 """
3 3 Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`.
4 4 """
5 5 from __future__ import absolute_import
6 6
7 7 import contextlib
8 8 import errno
9 9 import os
10 import pickle
10 11 import signal
11 12 import subprocess
12 13 import sys
13 14 import tempfile
14 15 import unittest
15 16
16 17 from mercurial import pycompat, util
17 18
18 19
19 20 if pycompat.ispy3:
20 21
21 22 def set_noninheritable(fd):
22 23 # On Python 3, file descriptors are non-inheritable by default.
23 24 pass
24 25
25 26
26 27 else:
27 28 if pycompat.iswindows:
28 29 # unused
29 30 set_noninheritable = None
30 31 else:
31 32 import fcntl
32 33
33 34 def set_noninheritable(fd):
34 35 old = fcntl.fcntl(fd, fcntl.F_GETFD)
35 36 fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
36 37
37 38
38 39 TEST_BUFFERING_CHILD_SCRIPT = r'''
39 40 import os
40 41
41 42 from mercurial import dispatch
42 43 from mercurial.utils import procutil
43 44
44 45 dispatch.initstdio()
45 46 procutil.{stream}.write(b'aaa')
46 47 os.write(procutil.{stream}.fileno(), b'[written aaa]')
47 48 procutil.{stream}.write(b'bbb\n')
48 49 os.write(procutil.{stream}.fileno(), b'[written bbb\\n]')
49 50 '''
50 51 UNBUFFERED = b'aaa[written aaa]bbb\n[written bbb\\n]'
51 52 LINE_BUFFERED = b'[written aaa]aaabbb\n[written bbb\\n]'
52 53 FULLY_BUFFERED = b'[written aaa][written bbb\\n]aaabbb\n'
53 54
54 55
55 56 TEST_LARGE_WRITE_CHILD_SCRIPT = r'''
56 57 import os
57 58 import signal
58 59 import sys
59 60
60 61 from mercurial import dispatch
61 62 from mercurial.utils import procutil
62 63
63 64 signal.signal(signal.SIGINT, lambda *x: None)
64 65 dispatch.initstdio()
65 66 write_result = procutil.{stream}.write(b'x' * 1048576)
66 67 with os.fdopen(
67 68 os.open({write_result_fn!r}, os.O_WRONLY | getattr(os, 'O_TEMPORARY', 0)),
68 69 'w',
69 70 ) as write_result_f:
70 71 write_result_f.write(str(write_result))
71 72 '''
72 73
73 74
74 75 TEST_BROKEN_PIPE_CHILD_SCRIPT = r'''
75 76 import os
76 77 import pickle
77 78
78 79 from mercurial import dispatch
79 80 from mercurial.utils import procutil
80 81
81 82 dispatch.initstdio()
82 83 procutil.stdin.read(1) # wait until parent process closed pipe
83 84 try:
84 85 procutil.{stream}.write(b'test')
85 86 procutil.{stream}.flush()
86 87 except EnvironmentError as e:
87 88 with os.fdopen(
88 89 os.open(
89 90 {err_fn!r},
90 91 os.O_WRONLY
91 92 | getattr(os, 'O_BINARY', 0)
92 93 | getattr(os, 'O_TEMPORARY', 0),
93 94 ),
94 95 'wb',
95 96 ) as err_f:
96 97 pickle.dump(e, err_f)
97 98 # Exit early to suppress further broken pipe errors at interpreter shutdown.
98 99 os._exit(0)
99 100 '''
100 101
101 102
102 103 @contextlib.contextmanager
103 104 def _closing(fds):
104 105 try:
105 106 yield
106 107 finally:
107 108 for fd in fds:
108 109 try:
109 110 os.close(fd)
110 111 except EnvironmentError:
111 112 pass
112 113
113 114
114 115 # In the following, we set the FDs non-inheritable mainly to make it possible
115 116 # for tests to close the receiving end of the pipe / PTYs.
116 117
117 118
118 119 @contextlib.contextmanager
119 120 def _devnull():
120 121 devnull = os.open(os.devnull, os.O_WRONLY)
121 122 # We don't have a receiving end, so it's not worth the effort on Python 2
122 123 # on Windows to make the FD non-inheritable.
123 124 with _closing([devnull]):
124 125 yield (None, devnull)
125 126
126 127
127 128 @contextlib.contextmanager
128 129 def _pipes():
129 130 rwpair = os.pipe()
130 131 # Pipes are already non-inheritable on Windows.
131 132 if not pycompat.iswindows:
132 133 set_noninheritable(rwpair[0])
133 134 set_noninheritable(rwpair[1])
134 135 with _closing(rwpair):
135 136 yield rwpair
136 137
137 138
138 139 @contextlib.contextmanager
139 140 def _ptys():
140 141 if pycompat.iswindows:
141 142 raise unittest.SkipTest("PTYs are not supported on Windows")
142 143 import pty
143 144 import tty
144 145
145 146 rwpair = pty.openpty()
146 147 set_noninheritable(rwpair[0])
147 148 set_noninheritable(rwpair[1])
148 149 with _closing(rwpair):
149 150 tty.setraw(rwpair[0])
150 151 yield rwpair
151 152
152 153
153 154 def _readall(fd, buffer_size, initial_buf=None):
154 155 buf = initial_buf or []
155 156 while True:
156 157 try:
157 158 s = os.read(fd, buffer_size)
158 159 except OSError as e:
159 160 if e.errno == errno.EIO:
160 161 # If the child-facing PTY got closed, reading from the
161 162 # parent-facing PTY raises EIO.
162 163 break
163 164 raise
164 165 if not s:
165 166 break
166 167 buf.append(s)
167 168 return b''.join(buf)
168 169
169 170
170 171 class TestStdio(unittest.TestCase):
171 172 def _test(
172 173 self,
173 174 child_script,
174 175 stream,
175 176 rwpair_generator,
176 177 check_output,
177 178 python_args=[],
178 179 post_child_check=None,
179 180 stdin_generator=None,
180 181 ):
181 182 assert stream in ('stdout', 'stderr')
182 183 if stdin_generator is None:
183 184 stdin_generator = open(os.devnull, 'rb')
184 185 with rwpair_generator() as (
185 186 stream_receiver,
186 187 child_stream,
187 188 ), stdin_generator as child_stdin:
188 189 proc = subprocess.Popen(
189 190 [sys.executable] + python_args + ['-c', child_script],
190 191 stdin=child_stdin,
191 192 stdout=child_stream if stream == 'stdout' else None,
192 193 stderr=child_stream if stream == 'stderr' else None,
193 194 )
194 195 try:
195 196 os.close(child_stream)
196 197 if stream_receiver is not None:
197 198 check_output(stream_receiver, proc)
198 199 except: # re-raises
199 200 proc.terminate()
200 201 raise
201 202 finally:
202 203 retcode = proc.wait()
203 204 self.assertEqual(retcode, 0)
204 205 if post_child_check is not None:
205 206 post_child_check()
206 207
207 208 def _test_buffering(
208 209 self, stream, rwpair_generator, expected_output, python_args=[]
209 210 ):
210 211 def check_output(stream_receiver, proc):
211 212 self.assertEqual(_readall(stream_receiver, 1024), expected_output)
212 213
213 214 self._test(
214 215 TEST_BUFFERING_CHILD_SCRIPT.format(stream=stream),
215 216 stream,
216 217 rwpair_generator,
217 218 check_output,
218 219 python_args,
219 220 )
220 221
221 222 def test_buffering_stdout_devnull(self):
222 223 self._test_buffering('stdout', _devnull, None)
223 224
224 225 def test_buffering_stdout_pipes(self):
225 226 self._test_buffering('stdout', _pipes, FULLY_BUFFERED)
226 227
227 228 def test_buffering_stdout_ptys(self):
228 229 self._test_buffering('stdout', _ptys, LINE_BUFFERED)
229 230
230 231 def test_buffering_stdout_devnull_unbuffered(self):
231 232 self._test_buffering('stdout', _devnull, None, python_args=['-u'])
232 233
233 234 def test_buffering_stdout_pipes_unbuffered(self):
234 235 self._test_buffering('stdout', _pipes, UNBUFFERED, python_args=['-u'])
235 236
236 237 def test_buffering_stdout_ptys_unbuffered(self):
237 238 self._test_buffering('stdout', _ptys, UNBUFFERED, python_args=['-u'])
238 239
239 240 if not pycompat.ispy3 and not pycompat.iswindows:
240 241 # On Python 2 on non-Windows, we manually open stdout in line-buffered
241 242 # mode if connected to a TTY. We should check if Python was configured
242 243 # to use unbuffered stdout, but it's hard to do that.
243 244 test_buffering_stdout_ptys_unbuffered = unittest.expectedFailure(
244 245 test_buffering_stdout_ptys_unbuffered
245 246 )
246 247
247 248 def _test_large_write(self, stream, rwpair_generator, python_args=[]):
248 249 if not pycompat.ispy3 and pycompat.isdarwin:
249 250 # Python 2 doesn't always retry on EINTR, but the libc might retry.
250 251 # So far, it was observed only on macOS that EINTR is raised at the
251 252 # Python level. As Python 2 support will be dropped soon-ish, we
252 253 # won't attempt to fix it.
253 254 raise unittest.SkipTest("raises EINTR on macOS")
254 255
255 256 def check_output(stream_receiver, proc):
256 257 if not pycompat.iswindows:
257 258 # On Unix, we can provoke a partial write() by interrupting it
258 259 # by a signal handler as soon as a bit of data was written.
259 260 # We test that write() is called until all data is written.
260 261 buf = [os.read(stream_receiver, 1)]
261 262 proc.send_signal(signal.SIGINT)
262 263 else:
263 264 # On Windows, there doesn't seem to be a way to cause partial
264 265 # writes.
265 266 buf = []
266 267 self.assertEqual(
267 268 _readall(stream_receiver, 131072, buf), b'x' * 1048576
268 269 )
269 270
270 271 def post_child_check():
271 272 write_result_str = write_result_f.read()
272 273 if pycompat.ispy3:
273 274 # On Python 3, we test that the correct number of bytes is
274 275 # claimed to have been written.
275 276 expected_write_result_str = '1048576'
276 277 else:
277 278 # On Python 2, we only check that the large write does not
278 279 # crash.
279 280 expected_write_result_str = 'None'
280 281 self.assertEqual(write_result_str, expected_write_result_str)
281 282
282 283 with tempfile.NamedTemporaryFile('r') as write_result_f:
283 284 self._test(
284 285 TEST_LARGE_WRITE_CHILD_SCRIPT.format(
285 286 stream=stream, write_result_fn=write_result_f.name
286 287 ),
287 288 stream,
288 289 rwpair_generator,
289 290 check_output,
290 291 python_args,
291 292 post_child_check=post_child_check,
292 293 )
293 294
294 295 def test_large_write_stdout_devnull(self):
295 296 self._test_large_write('stdout', _devnull)
296 297
297 298 def test_large_write_stdout_pipes(self):
298 299 self._test_large_write('stdout', _pipes)
299 300
300 301 def test_large_write_stdout_ptys(self):
301 302 self._test_large_write('stdout', _ptys)
302 303
303 304 def test_large_write_stdout_devnull_unbuffered(self):
304 305 self._test_large_write('stdout', _devnull, python_args=['-u'])
305 306
306 307 def test_large_write_stdout_pipes_unbuffered(self):
307 308 self._test_large_write('stdout', _pipes, python_args=['-u'])
308 309
309 310 def test_large_write_stdout_ptys_unbuffered(self):
310 311 self._test_large_write('stdout', _ptys, python_args=['-u'])
311 312
312 313 def test_large_write_stderr_devnull(self):
313 314 self._test_large_write('stderr', _devnull)
314 315
315 316 def test_large_write_stderr_pipes(self):
316 317 self._test_large_write('stderr', _pipes)
317 318
318 319 def test_large_write_stderr_ptys(self):
319 320 self._test_large_write('stderr', _ptys)
320 321
321 322 def test_large_write_stderr_devnull_unbuffered(self):
322 323 self._test_large_write('stderr', _devnull, python_args=['-u'])
323 324
324 325 def test_large_write_stderr_pipes_unbuffered(self):
325 326 self._test_large_write('stderr', _pipes, python_args=['-u'])
326 327
327 328 def test_large_write_stderr_ptys_unbuffered(self):
328 329 self._test_large_write('stderr', _ptys, python_args=['-u'])
329 330
330 331 def _test_broken_pipe(self, stream):
331 332 assert stream in ('stdout', 'stderr')
332 333
333 334 def check_output(stream_receiver, proc):
334 335 os.close(stream_receiver)
335 336 proc.stdin.write(b'x')
336 337 proc.stdin.close()
337 338
338 339 def post_child_check():
339 err = util.pickle.load(err_f)
340 err = pickle.load(err_f)
340 341 self.assertEqual(err.errno, errno.EPIPE)
341 342 self.assertEqual(err.strerror, "Broken pipe")
342 343
343 344 with tempfile.NamedTemporaryFile('rb') as err_f:
344 345 self._test(
345 346 TEST_BROKEN_PIPE_CHILD_SCRIPT.format(
346 347 stream=stream, err_fn=err_f.name
347 348 ),
348 349 stream,
349 350 _pipes,
350 351 check_output,
351 352 post_child_check=post_child_check,
352 353 stdin_generator=util.nullcontextmanager(subprocess.PIPE),
353 354 )
354 355
355 356 def test_broken_pipe_stdout(self):
356 357 self._test_broken_pipe('stdout')
357 358
358 359 def test_broken_pipe_stderr(self):
359 360 self._test_broken_pipe('stderr')
360 361
361 362
362 363 if __name__ == '__main__':
363 364 import silenttestrunner
364 365
365 366 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now