##// END OF EJS Templates
global: mass rewrite to use modern exception syntax...
Gregory Szorc -
r25660:328739ea default
parent child Browse files
Show More

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

@@ -1,571 +1,571 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Matt Mackall <mpm@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, # no-py24)
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 import re, glob, os, sys
23 23 import keyword
24 24 import optparse
25 25 try:
26 26 import re2
27 27 except ImportError:
28 28 re2 = None
29 29
30 30 def compilere(pat, multiline=False):
31 31 if multiline:
32 32 pat = '(?m)' + pat
33 33 if re2:
34 34 try:
35 35 return re2.compile(pat)
36 36 except re2.error:
37 37 pass
38 38 return re.compile(pat)
39 39
40 40 def repquote(m):
41 41 fromc = '.:'
42 42 tochr = 'pq'
43 43 def encodechr(i):
44 44 if i > 255:
45 45 return 'u'
46 46 c = chr(i)
47 47 if c in ' \n':
48 48 return c
49 49 if c.isalpha():
50 50 return 'x'
51 51 if c.isdigit():
52 52 return 'n'
53 53 try:
54 54 return tochr[fromc.find(c)]
55 55 except (ValueError, IndexError):
56 56 return 'o'
57 57 t = m.group('text')
58 58 tt = ''.join(encodechr(i) for i in xrange(256))
59 59 t = t.translate(tt)
60 60 return m.group('quote') + t + m.group('quote')
61 61
62 62 def reppython(m):
63 63 comment = m.group('comment')
64 64 if comment:
65 65 l = len(comment.rstrip())
66 66 return "#" * l + comment[l:]
67 67 return repquote(m)
68 68
69 69 def repcomment(m):
70 70 return m.group(1) + "#" * len(m.group(2))
71 71
72 72 def repccomment(m):
73 73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
74 74 return m.group(1) + t + "*/"
75 75
76 76 def repcallspaces(m):
77 77 t = re.sub(r"\n\s+", "\n", m.group(2))
78 78 return m.group(1) + t
79 79
80 80 def repinclude(m):
81 81 return m.group(1) + "<foo>"
82 82
83 83 def rephere(m):
84 84 t = re.sub(r"\S", "x", m.group(2))
85 85 return m.group(1) + t
86 86
87 87
88 88 testpats = [
89 89 [
90 90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
91 91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
92 92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
93 93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
94 94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
95 95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
96 96 (r'echo -n', "don't use 'echo -n', use printf"),
97 97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
98 98 (r'head -c', "don't use 'head -c', use 'dd'"),
99 99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
100 100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
101 101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
102 102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
103 103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
104 104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
105 105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
106 106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
107 107 "use egrep for extended grep syntax"),
108 108 (r'/bin/', "don't use explicit paths for tools"),
109 109 (r'[^\n]\Z', "no trailing newline"),
110 110 (r'export.*=', "don't export and assign at once"),
111 111 (r'^source\b', "don't use 'source', use '.'"),
112 112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
113 113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
114 114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
115 115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
116 116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
117 117 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
118 118 (r'^alias\b.*=', "don't use alias, use a function"),
119 119 (r'if\s*!', "don't use '!' to negate exit status"),
120 120 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
121 121 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
122 122 (r'^( *)\t', "don't use tabs to indent"),
123 123 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
124 124 "put a backslash-escaped newline after sed 'i' command"),
125 125 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
126 126 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py")
127 127 ],
128 128 # warnings
129 129 [
130 130 (r'^function', "don't use 'function', use old style"),
131 131 (r'^diff.*-\w*N', "don't use 'diff -N'"),
132 132 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
133 133 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
134 134 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
135 135 ]
136 136 ]
137 137
138 138 testfilters = [
139 139 (r"( *)(#([^\n]*\S)?)", repcomment),
140 140 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
141 141 ]
142 142
143 143 winglobmsg = "use (glob) to match Windows paths too"
144 144 uprefix = r"^ \$ "
145 145 utestpats = [
146 146 [
147 147 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
148 148 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
149 149 "use regex test output patterns instead of sed"),
150 150 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
151 151 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
152 152 (uprefix + r'.*\|\| echo.*(fail|error)',
153 153 "explicit exit code checks unnecessary"),
154 154 (uprefix + r'set -e', "don't use set -e"),
155 155 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
156 156 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
157 157 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
158 158 '# no-msys'), # in test-pull.t which is skipped on windows
159 159 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
160 160 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
161 161 winglobmsg),
162 162 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
163 163 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
164 164 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
165 165 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
166 166 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
167 167 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
168 168 (r'^ moving \S+/.*[^)]$', winglobmsg),
169 169 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
170 170 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
171 171 (r'^ .*file://\$TESTTMP',
172 172 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
173 173 (r'^ (cat|find): .*: No such file or directory',
174 174 'use test -f to test for file existence'),
175 175 ],
176 176 # warnings
177 177 [
178 178 (r'^ [^*?/\n]* \(glob\)$',
179 179 "glob match with no glob character (?*/)"),
180 180 ]
181 181 ]
182 182
183 183 for i in [0, 1]:
184 184 for tp in testpats[i]:
185 185 p = tp[0]
186 186 m = tp[1]
187 187 if p.startswith(r'^'):
188 188 p = r"^ [$>] (%s)" % p[1:]
189 189 else:
190 190 p = r"^ [$>] .*(%s)" % p
191 191 utestpats[i].append((p, m) + tp[2:])
192 192
193 193 utestfilters = [
194 194 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
195 195 (r"( *)(#([^\n]*\S)?)", repcomment),
196 196 ]
197 197
198 198 pypats = [
199 199 [
200 200 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
201 201 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
202 202 "tuple parameter unpacking not available in Python 3+"),
203 203 (r'lambda\s*\(.*,.*\)',
204 204 "tuple parameter unpacking not available in Python 3+"),
205 205 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
206 206 '2to3 can\'t always rewrite "import qux, foo.bar", '
207 207 'use "import foo.bar" on its own line instead.'),
208 208 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
209 209 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
210 210 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
211 211 'dict-from-generator'),
212 212 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
213 213 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
214 214 (r'^\s*\t', "don't use tabs"),
215 215 (r'\S;\s*\n', "semicolon"),
216 216 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
217 217 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
218 218 (r'(\w|\)),\w', "missing whitespace after ,"),
219 219 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
220 220 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
221 221 (r'.{81}', "line too long"),
222 222 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
223 223 (r'[^\n]\Z', "no trailing newline"),
224 224 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
225 225 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
226 226 # "don't use underbars in identifiers"),
227 227 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
228 228 "don't use camelcase in identifiers"),
229 229 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
230 230 "linebreak after :"),
231 231 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
232 232 (r'class\s[^( \n]+\(\):',
233 233 "class foo() creates old style object, use class foo(object)"),
234 234 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
235 235 if k not in ('print', 'exec')),
236 236 "Python keyword is not a function"),
237 237 (r',]', "unneeded trailing ',' in list"),
238 238 # (r'class\s[A-Z][^\(]*\((?!Exception)',
239 239 # "don't capitalize non-exception classes"),
240 240 # (r'in range\(', "use xrange"),
241 241 # (r'^\s*print\s+', "avoid using print in core and extensions"),
242 242 (r'[\x80-\xff]', "non-ASCII character literal"),
243 243 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
244 244 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
245 245 "gratuitous whitespace after Python keyword"),
246 246 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
247 247 # (r'\s\s=', "gratuitous whitespace before ="),
248 248 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
249 249 "missing whitespace around operator"),
250 250 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
251 251 "missing whitespace around operator"),
252 252 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
253 253 "missing whitespace around operator"),
254 254 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
255 255 "wrong whitespace around ="),
256 256 (r'\([^()]*( =[^=]|[^<>!=]= )',
257 257 "no whitespace around = for named parameters"),
258 258 (r'raise Exception', "don't raise generic exceptions"),
259 259 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
260 260 "don't use old-style two-argument raise, use Exception(message)"),
261 261 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
262 262 (r' [=!]=\s+(True|False|None)',
263 263 "comparison with singleton, use 'is' or 'is not' instead"),
264 264 (r'^\s*(while|if) [01]:',
265 265 "use True/False for constant Boolean expression"),
266 266 (r'(?:(?<!def)\s+|\()hasattr',
267 267 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
268 268 (r'opener\([^)]*\).read\(',
269 269 "use opener.read() instead"),
270 270 (r'opener\([^)]*\).write\(',
271 271 "use opener.write() instead"),
272 272 (r'[\s\(](open|file)\([^)]*\)\.read\(',
273 273 "use util.readfile() instead"),
274 274 (r'[\s\(](open|file)\([^)]*\)\.write\(',
275 275 "use util.writefile() instead"),
276 276 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
277 277 "always assign an opened file to a variable, and close it afterwards"),
278 278 (r'[\s\(](open|file)\([^)]*\)\.',
279 279 "always assign an opened file to a variable, and close it afterwards"),
280 280 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
281 281 (r'\.debug\(\_', "don't mark debug messages for translation"),
282 282 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
283 283 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
284 284 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
285 285 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
286 286 "missing _() in ui message (use () to hide false-positives)"),
287 287 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
288 288 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
289 289 (r'os\.path\.join\(.*, *(""|\'\')\)',
290 290 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
291 291 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
292 292 ],
293 293 # warnings
294 294 [
295 295 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
296 296 ]
297 297 ]
298 298
299 299 pyfilters = [
300 300 (r"""(?msx)(?P<comment>\#.*?$)|
301 301 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
302 302 (?P<text>(([^\\]|\\.)*?))
303 303 (?P=quote))""", reppython),
304 304 ]
305 305
306 306 txtfilters = []
307 307
308 308 txtpats = [
309 309 [
310 310 ('\s$', 'trailing whitespace'),
311 311 ('.. note::[ \n][^\n]', 'add two newlines after note::')
312 312 ],
313 313 []
314 314 ]
315 315
316 316 cpats = [
317 317 [
318 318 (r'//', "don't use //-style comments"),
319 319 (r'^ ', "don't use spaces to indent"),
320 320 (r'\S\t', "don't use tabs except for indent"),
321 321 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
322 322 (r'.{81}', "line too long"),
323 323 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
324 324 (r'return\(', "return is not a function"),
325 325 (r' ;', "no space before ;"),
326 326 (r'[^;] \)', "no space before )"),
327 327 (r'[)][{]', "space between ) and {"),
328 328 (r'\w+\* \w+', "use int *foo, not int* foo"),
329 329 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
330 330 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
331 331 (r'\w,\w', "missing whitespace after ,"),
332 332 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
333 333 (r'^#\s+\w', "use #foo, not # foo"),
334 334 (r'[^\n]\Z', "no trailing newline"),
335 335 (r'^\s*#import\b', "use only #include in standard C code"),
336 336 ],
337 337 # warnings
338 338 []
339 339 ]
340 340
341 341 cfilters = [
342 342 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
343 343 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
344 344 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
345 345 (r'(\()([^)]+\))', repcallspaces),
346 346 ]
347 347
348 348 inutilpats = [
349 349 [
350 350 (r'\bui\.', "don't use ui in util"),
351 351 ],
352 352 # warnings
353 353 []
354 354 ]
355 355
356 356 inrevlogpats = [
357 357 [
358 358 (r'\brepo\.', "don't use repo in revlog"),
359 359 ],
360 360 # warnings
361 361 []
362 362 ]
363 363
364 364 webtemplatefilters = []
365 365
366 366 webtemplatepats = [
367 367 [],
368 368 [
369 369 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
370 370 'follow desc keyword with either firstline or websub'),
371 371 ]
372 372 ]
373 373
374 374 checks = [
375 375 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
376 376 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
377 377 ('c', r'.*\.[ch]$', '', cfilters, cpats),
378 378 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
379 379 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
380 380 pyfilters, inrevlogpats),
381 381 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
382 382 inutilpats),
383 383 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
384 384 ('web template', r'mercurial/templates/.*\.tmpl', '',
385 385 webtemplatefilters, webtemplatepats),
386 386 ]
387 387
388 388 def _preparepats():
389 389 for c in checks:
390 390 failandwarn = c[-1]
391 391 for pats in failandwarn:
392 392 for i, pseq in enumerate(pats):
393 393 # fix-up regexes for multi-line searches
394 394 p = pseq[0]
395 395 # \s doesn't match \n
396 396 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
397 397 # [^...] doesn't match newline
398 398 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
399 399
400 400 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
401 401 filters = c[3]
402 402 for i, flt in enumerate(filters):
403 403 filters[i] = re.compile(flt[0]), flt[1]
404 404 _preparepats()
405 405
406 406 class norepeatlogger(object):
407 407 def __init__(self):
408 408 self._lastseen = None
409 409
410 410 def log(self, fname, lineno, line, msg, blame):
411 411 """print error related a to given line of a given file.
412 412
413 413 The faulty line will also be printed but only once in the case
414 414 of multiple errors.
415 415
416 416 :fname: filename
417 417 :lineno: line number
418 418 :line: actual content of the line
419 419 :msg: error message
420 420 """
421 421 msgid = fname, lineno, line
422 422 if msgid != self._lastseen:
423 423 if blame:
424 424 print "%s:%d (%s):" % (fname, lineno, blame)
425 425 else:
426 426 print "%s:%d:" % (fname, lineno)
427 427 print " > %s" % line
428 428 self._lastseen = msgid
429 429 print " " + msg
430 430
431 431 _defaultlogger = norepeatlogger()
432 432
433 433 def getblame(f):
434 434 lines = []
435 435 for l in os.popen('hg annotate -un %s' % f):
436 436 start, line = l.split(':', 1)
437 437 user, rev = start.split()
438 438 lines.append((line[1:-1], user, rev))
439 439 return lines
440 440
441 441 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
442 442 blame=False, debug=False, lineno=True):
443 443 """checks style and portability of a given file
444 444
445 445 :f: filepath
446 446 :logfunc: function used to report error
447 447 logfunc(filename, linenumber, linecontent, errormessage)
448 448 :maxerr: number of error to display before aborting.
449 449 Set to false (default) to report all errors
450 450
451 451 return True if no error is found, False otherwise.
452 452 """
453 453 blamecache = None
454 454 result = True
455 455
456 456 try:
457 457 fp = open(f)
458 except IOError, e:
458 except IOError as e:
459 459 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
460 460 return result
461 461 pre = post = fp.read()
462 462 fp.close()
463 463
464 464 for name, match, magic, filters, pats in checks:
465 465 if debug:
466 466 print name, f
467 467 fc = 0
468 468 if not (re.match(match, f) or (magic and re.search(magic, f))):
469 469 if debug:
470 470 print "Skipping %s for %s it doesn't match %s" % (
471 471 name, match, f)
472 472 continue
473 473 if "no-" "check-code" in pre:
474 474 print "Skipping %s it has no-" "check-code" % f
475 475 return "Skip" # skip checking this file
476 476 for p, r in filters:
477 477 post = re.sub(p, r, post)
478 478 nerrs = len(pats[0]) # nerr elements are errors
479 479 if warnings:
480 480 pats = pats[0] + pats[1]
481 481 else:
482 482 pats = pats[0]
483 483 # print post # uncomment to show filtered version
484 484
485 485 if debug:
486 486 print "Checking %s for %s" % (name, f)
487 487
488 488 prelines = None
489 489 errors = []
490 490 for i, pat in enumerate(pats):
491 491 if len(pat) == 3:
492 492 p, msg, ignore = pat
493 493 else:
494 494 p, msg = pat
495 495 ignore = None
496 496 if i >= nerrs:
497 497 msg = "warning: " + msg
498 498
499 499 pos = 0
500 500 n = 0
501 501 for m in p.finditer(post):
502 502 if prelines is None:
503 503 prelines = pre.splitlines()
504 504 postlines = post.splitlines(True)
505 505
506 506 start = m.start()
507 507 while n < len(postlines):
508 508 step = len(postlines[n])
509 509 if pos + step > start:
510 510 break
511 511 pos += step
512 512 n += 1
513 513 l = prelines[n]
514 514
515 515 if ignore and re.search(ignore, l, re.MULTILINE):
516 516 if debug:
517 517 print "Skipping %s for %s:%s (ignore pattern)" % (
518 518 name, f, n)
519 519 continue
520 520 bd = ""
521 521 if blame:
522 522 bd = 'working directory'
523 523 if not blamecache:
524 524 blamecache = getblame(f)
525 525 if n < len(blamecache):
526 526 bl, bu, br = blamecache[n]
527 527 if bl == l:
528 528 bd = '%s@%s' % (bu, br)
529 529
530 530 errors.append((f, lineno and n + 1, l, msg, bd))
531 531 result = False
532 532
533 533 errors.sort()
534 534 for e in errors:
535 535 logfunc(*e)
536 536 fc += 1
537 537 if maxerr and fc >= maxerr:
538 538 print " (too many errors, giving up)"
539 539 break
540 540
541 541 return result
542 542
543 543 if __name__ == "__main__":
544 544 parser = optparse.OptionParser("%prog [options] [files]")
545 545 parser.add_option("-w", "--warnings", action="store_true",
546 546 help="include warning-level checks")
547 547 parser.add_option("-p", "--per-file", type="int",
548 548 help="max warnings per file")
549 549 parser.add_option("-b", "--blame", action="store_true",
550 550 help="use annotate to generate blame info")
551 551 parser.add_option("", "--debug", action="store_true",
552 552 help="show debug information")
553 553 parser.add_option("", "--nolineno", action="store_false",
554 554 dest='lineno', help="don't show line numbers")
555 555
556 556 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
557 557 lineno=True)
558 558 (options, args) = parser.parse_args()
559 559
560 560 if len(args) == 0:
561 561 check = glob.glob("*")
562 562 else:
563 563 check = args
564 564
565 565 ret = 0
566 566 for f in check:
567 567 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
568 568 blame=options.blame, debug=options.debug,
569 569 lineno=options.lineno):
570 570 ret = 1
571 571 sys.exit(ret)
@@ -1,377 +1,377 b''
1 1 import ast
2 2 import os
3 3 import sys
4 4
5 5 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
6 6 # to work when run from a virtualenv. The modules were chosen empirically
7 7 # so that the return value matches the return value without virtualenv.
8 8 import BaseHTTPServer
9 9 import zlib
10 10
11 11 def dotted_name_of_path(path, trimpure=False):
12 12 """Given a relative path to a source file, return its dotted module name.
13 13
14 14 >>> dotted_name_of_path('mercurial/error.py')
15 15 'mercurial.error'
16 16 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
17 17 'mercurial.parsers'
18 18 >>> dotted_name_of_path('zlibmodule.so')
19 19 'zlib'
20 20 """
21 21 parts = path.split('/')
22 22 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
23 23 if parts[-1].endswith('module'):
24 24 parts[-1] = parts[-1][:-6]
25 25 if trimpure:
26 26 return '.'.join(p for p in parts if p != 'pure')
27 27 return '.'.join(parts)
28 28
29 29 def fromlocalfunc(modulename, localmods):
30 30 """Get a function to examine which locally defined module the
31 31 target source imports via a specified name.
32 32
33 33 `modulename` is an `dotted_name_of_path()`-ed source file path,
34 34 which may have `.__init__` at the end of it, of the target source.
35 35
36 36 `localmods` is a dict (or set), of which key is an absolute
37 37 `dotted_name_of_path()`-ed source file path of locally defined (=
38 38 Mercurial specific) modules.
39 39
40 40 This function assumes that module names not existing in
41 41 `localmods` are ones of Python standard libarary.
42 42
43 43 This function returns the function, which takes `name` argument,
44 44 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
45 45 matches against locally defined module. Otherwise, it returns
46 46 False.
47 47
48 48 It is assumed that `name` doesn't have `.__init__`.
49 49
50 50 `absname` is an absolute module name of specified `name`
51 51 (e.g. "hgext.convert"). This can be used to compose prefix for sub
52 52 modules or so.
53 53
54 54 `dottedpath` is a `dotted_name_of_path()`-ed source file path
55 55 (e.g. "hgext.convert.__init__") of `name`. This is used to look
56 56 module up in `localmods` again.
57 57
58 58 `hassubmod` is whether it may have sub modules under it (for
59 59 convenient, even though this is also equivalent to "absname !=
60 60 dottednpath")
61 61
62 62 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
63 63 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
64 64 ... 'baz.__init__': True, 'baz.baz1': True }
65 65 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
66 66 >>> # relative
67 67 >>> fromlocal('foo1')
68 68 ('foo.foo1', 'foo.foo1', False)
69 69 >>> fromlocal('bar')
70 70 ('foo.bar', 'foo.bar.__init__', True)
71 71 >>> fromlocal('bar.bar1')
72 72 ('foo.bar.bar1', 'foo.bar.bar1', False)
73 73 >>> # absolute
74 74 >>> fromlocal('baz')
75 75 ('baz', 'baz.__init__', True)
76 76 >>> fromlocal('baz.baz1')
77 77 ('baz.baz1', 'baz.baz1', False)
78 78 >>> # unknown = maybe standard library
79 79 >>> fromlocal('os')
80 80 False
81 81 """
82 82 prefix = '.'.join(modulename.split('.')[:-1])
83 83 if prefix:
84 84 prefix += '.'
85 85 def fromlocal(name):
86 86 # check relative name at first
87 87 for n in prefix + name, name:
88 88 if n in localmods:
89 89 return (n, n, False)
90 90 dottedpath = n + '.__init__'
91 91 if dottedpath in localmods:
92 92 return (n, dottedpath, True)
93 93 return False
94 94 return fromlocal
95 95
96 96 def list_stdlib_modules():
97 97 """List the modules present in the stdlib.
98 98
99 99 >>> mods = set(list_stdlib_modules())
100 100 >>> 'BaseHTTPServer' in mods
101 101 True
102 102
103 103 os.path isn't really a module, so it's missing:
104 104
105 105 >>> 'os.path' in mods
106 106 False
107 107
108 108 sys requires special treatment, because it's baked into the
109 109 interpreter, but it should still appear:
110 110
111 111 >>> 'sys' in mods
112 112 True
113 113
114 114 >>> 'collections' in mods
115 115 True
116 116
117 117 >>> 'cStringIO' in mods
118 118 True
119 119 """
120 120 for m in sys.builtin_module_names:
121 121 yield m
122 122 # These modules only exist on windows, but we should always
123 123 # consider them stdlib.
124 124 for m in ['msvcrt', '_winreg']:
125 125 yield m
126 126 # These get missed too
127 127 for m in 'ctypes', 'email':
128 128 yield m
129 129 yield 'builtins' # python3 only
130 130 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
131 131 yield m
132 132 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
133 133 # We need to supplement the list of prefixes for the search to work
134 134 # when run from within a virtualenv.
135 135 for mod in (BaseHTTPServer, zlib):
136 136 try:
137 137 # Not all module objects have a __file__ attribute.
138 138 filename = mod.__file__
139 139 except AttributeError:
140 140 continue
141 141 dirname = os.path.dirname(filename)
142 142 for prefix in stdlib_prefixes:
143 143 if dirname.startswith(prefix):
144 144 # Then this directory is redundant.
145 145 break
146 146 else:
147 147 stdlib_prefixes.add(dirname)
148 148 for libpath in sys.path:
149 149 # We want to walk everything in sys.path that starts with
150 150 # something in stdlib_prefixes. check-code suppressed because
151 151 # the ast module used by this script implies the availability
152 152 # of any().
153 153 if not any(libpath.startswith(p) for p in stdlib_prefixes): # no-py24
154 154 continue
155 155 if 'site-packages' in libpath:
156 156 continue
157 157 for top, dirs, files in os.walk(libpath):
158 158 for name in files:
159 159 if name == '__init__.py':
160 160 continue
161 161 if not (name.endswith('.py') or name.endswith('.so')
162 162 or name.endswith('.pyd')):
163 163 continue
164 164 full_path = os.path.join(top, name)
165 165 if 'site-packages' in full_path:
166 166 continue
167 167 rel_path = full_path[len(libpath) + 1:]
168 168 mod = dotted_name_of_path(rel_path)
169 169 yield mod
170 170
171 171 stdlib_modules = set(list_stdlib_modules())
172 172
173 173 def imported_modules(source, modulename, localmods, ignore_nested=False):
174 174 """Given the source of a file as a string, yield the names
175 175 imported by that file.
176 176
177 177 Args:
178 178 source: The python source to examine as a string.
179 179 modulename: of specified python source (may have `__init__`)
180 180 localmods: dict of locally defined module names (may have `__init__`)
181 181 ignore_nested: If true, import statements that do not start in
182 182 column zero will be ignored.
183 183
184 184 Returns:
185 185 A list of absolute module names imported by the given source.
186 186
187 187 >>> modulename = 'foo.xxx'
188 188 >>> localmods = {'foo.__init__': True,
189 189 ... 'foo.foo1': True, 'foo.foo2': True,
190 190 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
191 191 ... 'baz.__init__': True, 'baz.baz1': True }
192 192 >>> # standard library (= not locally defined ones)
193 193 >>> sorted(imported_modules(
194 194 ... 'from stdlib1 import foo, bar; import stdlib2',
195 195 ... modulename, localmods))
196 196 []
197 197 >>> # relative importing
198 198 >>> sorted(imported_modules(
199 199 ... 'import foo1; from bar import bar1',
200 200 ... modulename, localmods))
201 201 ['foo.bar.__init__', 'foo.bar.bar1', 'foo.foo1']
202 202 >>> sorted(imported_modules(
203 203 ... 'from bar.bar1 import name1, name2, name3',
204 204 ... modulename, localmods))
205 205 ['foo.bar.bar1']
206 206 >>> # absolute importing
207 207 >>> sorted(imported_modules(
208 208 ... 'from baz import baz1, name1',
209 209 ... modulename, localmods))
210 210 ['baz.__init__', 'baz.baz1']
211 211 >>> # mixed importing, even though it shouldn't be recommended
212 212 >>> sorted(imported_modules(
213 213 ... 'import stdlib, foo1, baz',
214 214 ... modulename, localmods))
215 215 ['baz.__init__', 'foo.foo1']
216 216 >>> # ignore_nested
217 217 >>> sorted(imported_modules(
218 218 ... '''import foo
219 219 ... def wat():
220 220 ... import bar
221 221 ... ''', modulename, localmods))
222 222 ['foo.__init__', 'foo.bar.__init__']
223 223 >>> sorted(imported_modules(
224 224 ... '''import foo
225 225 ... def wat():
226 226 ... import bar
227 227 ... ''', modulename, localmods, ignore_nested=True))
228 228 ['foo.__init__']
229 229 """
230 230 fromlocal = fromlocalfunc(modulename, localmods)
231 231 for node in ast.walk(ast.parse(source)):
232 232 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
233 233 continue
234 234 if isinstance(node, ast.Import):
235 235 for n in node.names:
236 236 found = fromlocal(n.name)
237 237 if not found:
238 238 # this should import standard library
239 239 continue
240 240 yield found[1]
241 241 elif isinstance(node, ast.ImportFrom):
242 242 found = fromlocal(node.module)
243 243 if not found:
244 244 # this should import standard library
245 245 continue
246 246
247 247 absname, dottedpath, hassubmod = found
248 248 yield dottedpath
249 249 if not hassubmod:
250 250 # examination of "node.names" should be redundant
251 251 # e.g.: from mercurial.node import nullid, nullrev
252 252 continue
253 253
254 254 prefix = absname + '.'
255 255 for n in node.names:
256 256 found = fromlocal(prefix + n.name)
257 257 if not found:
258 258 # this should be a function or a property of "node.module"
259 259 continue
260 260 yield found[1]
261 261
262 262 def verify_stdlib_on_own_line(source):
263 263 """Given some python source, verify that stdlib imports are done
264 264 in separate statements from relative local module imports.
265 265
266 266 Observing this limitation is important as it works around an
267 267 annoying lib2to3 bug in relative import rewrites:
268 268 http://bugs.python.org/issue19510.
269 269
270 270 >>> list(verify_stdlib_on_own_line('import sys, foo'))
271 271 ['mixed imports\\n stdlib: sys\\n relative: foo']
272 272 >>> list(verify_stdlib_on_own_line('import sys, os'))
273 273 []
274 274 >>> list(verify_stdlib_on_own_line('import foo, bar'))
275 275 []
276 276 """
277 277 for node in ast.walk(ast.parse(source)):
278 278 if isinstance(node, ast.Import):
279 279 from_stdlib = {False: [], True: []}
280 280 for n in node.names:
281 281 from_stdlib[n.name in stdlib_modules].append(n.name)
282 282 if from_stdlib[True] and from_stdlib[False]:
283 283 yield ('mixed imports\n stdlib: %s\n relative: %s' %
284 284 (', '.join(sorted(from_stdlib[True])),
285 285 ', '.join(sorted(from_stdlib[False]))))
286 286
287 287 class CircularImport(Exception):
288 288 pass
289 289
290 290 def checkmod(mod, imports):
291 291 shortest = {}
292 292 visit = [[mod]]
293 293 while visit:
294 294 path = visit.pop(0)
295 295 for i in sorted(imports.get(path[-1], [])):
296 296 if len(path) < shortest.get(i, 1000):
297 297 shortest[i] = len(path)
298 298 if i in path:
299 299 if i == path[0]:
300 300 raise CircularImport(path)
301 301 continue
302 302 visit.append(path + [i])
303 303
304 304 def rotatecycle(cycle):
305 305 """arrange a cycle so that the lexicographically first module listed first
306 306
307 307 >>> rotatecycle(['foo', 'bar'])
308 308 ['bar', 'foo', 'bar']
309 309 """
310 310 lowest = min(cycle)
311 311 idx = cycle.index(lowest)
312 312 return cycle[idx:] + cycle[:idx] + [lowest]
313 313
314 314 def find_cycles(imports):
315 315 """Find cycles in an already-loaded import graph.
316 316
317 317 All module names recorded in `imports` should be absolute one.
318 318
319 319 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
320 320 ... 'top.bar': ['top.baz', 'sys'],
321 321 ... 'top.baz': ['top.foo'],
322 322 ... 'top.qux': ['top.foo']}
323 323 >>> print '\\n'.join(sorted(find_cycles(imports)))
324 324 top.bar -> top.baz -> top.foo -> top.bar
325 325 top.foo -> top.qux -> top.foo
326 326 """
327 327 cycles = set()
328 328 for mod in sorted(imports.iterkeys()):
329 329 try:
330 330 checkmod(mod, imports)
331 except CircularImport, e:
331 except CircularImport as e:
332 332 cycle = e.args[0]
333 333 cycles.add(" -> ".join(rotatecycle(cycle)))
334 334 return cycles
335 335
336 336 def _cycle_sortkey(c):
337 337 return len(c), c
338 338
339 339 def main(argv):
340 340 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
341 341 print 'Usage: %s {-|file [file] [file] ...}'
342 342 return 1
343 343 if argv[1] == '-':
344 344 argv = argv[:1]
345 345 argv.extend(l.rstrip() for l in sys.stdin.readlines())
346 346 localmods = {}
347 347 used_imports = {}
348 348 any_errors = False
349 349 for source_path in argv[1:]:
350 350 modname = dotted_name_of_path(source_path, trimpure=True)
351 351 localmods[modname] = source_path
352 352 for modname, source_path in sorted(localmods.iteritems()):
353 353 f = open(source_path)
354 354 src = f.read()
355 355 used_imports[modname] = sorted(
356 356 imported_modules(src, modname, localmods, ignore_nested=True))
357 357 for error in verify_stdlib_on_own_line(src):
358 358 any_errors = True
359 359 print source_path, error
360 360 f.close()
361 361 cycles = find_cycles(used_imports)
362 362 if cycles:
363 363 firstmods = set()
364 364 for c in sorted(cycles, key=_cycle_sortkey):
365 365 first = c.split()[0]
366 366 # As a rough cut, ignore any cycle that starts with the
367 367 # same module as some other cycle. Otherwise we see lots
368 368 # of cycles that are effectively duplicates.
369 369 if first in firstmods:
370 370 continue
371 371 print 'Import cycle:', c
372 372 firstmods.add(first)
373 373 any_errors = True
374 374 return not any_errors
375 375
376 376 if __name__ == '__main__':
377 377 sys.exit(int(main(sys.argv)))
@@ -1,317 +1,317 b''
1 1 #!/usr/bin/env python
2 2
3 3 # Measure the performance of a list of revsets against multiple revisions
4 4 # defined by parameter. Checkout one by one and run perfrevset with every
5 5 # revset in the list to benchmark its performance.
6 6 #
7 7 # You should run this from the root of your mercurial repository.
8 8 #
9 9 # call with --help for details
10 10
11 11 import sys
12 12 import os
13 13 import re
14 14 import math
15 15 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
16 16 # cannot use argparse, python 2.7 only
17 17 from optparse import OptionParser
18 18
19 19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
20 20 'reverse', 'reverse+first', 'reverse+last',
21 21 'sort', 'sort+first', 'sort+last']
22 22
23 23 def check_output(*args, **kwargs):
24 24 kwargs.setdefault('stderr', PIPE)
25 25 kwargs.setdefault('stdout', PIPE)
26 26 proc = Popen(*args, **kwargs)
27 27 output, error = proc.communicate()
28 28 if proc.returncode != 0:
29 29 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
30 30 return output
31 31
32 32 def update(rev):
33 33 """update the repo to a revision"""
34 34 try:
35 35 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
36 except CalledProcessError, exc:
36 except CalledProcessError as exc:
37 37 print >> sys.stderr, 'update to revision %s failed, aborting' % rev
38 38 sys.exit(exc.returncode)
39 39
40 40
41 41 def hg(cmd, repo=None):
42 42 """run a mercurial command
43 43
44 44 <cmd> is the list of command + argument,
45 45 <repo> is an optional repository path to run this command in."""
46 46 fullcmd = ['./hg']
47 47 if repo is not None:
48 48 fullcmd += ['-R', repo]
49 49 fullcmd += ['--config',
50 50 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
51 51 fullcmd += cmd
52 52 return check_output(fullcmd, stderr=STDOUT)
53 53
54 54 def perf(revset, target=None):
55 55 """run benchmark for this very revset"""
56 56 try:
57 57 output = hg(['perfrevset', revset], repo=target)
58 58 return parseoutput(output)
59 except CalledProcessError, exc:
59 except CalledProcessError as exc:
60 60 print >> sys.stderr, 'abort: cannot run revset benchmark: %s' % exc.cmd
61 61 if exc.output is None:
62 62 print >> sys.stderr, '(no ouput)'
63 63 else:
64 64 print >> sys.stderr, exc.output
65 65 return None
66 66
67 67 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
68 68 'sys (\d+.\d+) \(best of (\d+)\)')
69 69
70 70 def parseoutput(output):
71 71 """parse a textual output into a dict
72 72
73 73 We cannot just use json because we want to compare with old
74 74 versions of Mercurial that may not support json output.
75 75 """
76 76 match = outputre.search(output)
77 77 if not match:
78 78 print >> sys.stderr, 'abort: invalid output:'
79 79 print >> sys.stderr, output
80 80 sys.exit(1)
81 81 return {'comb': float(match.group(2)),
82 82 'count': int(match.group(5)),
83 83 'sys': float(match.group(3)),
84 84 'user': float(match.group(4)),
85 85 'wall': float(match.group(1)),
86 86 }
87 87
88 88 def printrevision(rev):
89 89 """print data about a revision"""
90 90 sys.stdout.write("Revision ")
91 91 sys.stdout.flush()
92 92 check_call(['hg', 'log', '--rev', str(rev), '--template',
93 93 '{if(tags, " ({tags})")} '
94 94 '{rev}:{node|short}: {desc|firstline}\n'])
95 95
96 96 def idxwidth(nbidx):
97 97 """return the max width of number used for index
98 98
99 99 This is similar to log10(nbidx), but we use custom code here
100 100 because we start with zero and we'd rather not deal with all the
101 101 extra rounding business that log10 would imply.
102 102 """
103 103 nbidx -= 1 # starts at 0
104 104 idxwidth = 0
105 105 while nbidx:
106 106 idxwidth += 1
107 107 nbidx //= 10
108 108 if not idxwidth:
109 109 idxwidth = 1
110 110 return idxwidth
111 111
112 112 def getfactor(main, other, field, sensitivity=0.05):
113 113 """return the relative factor between values for 'field' in main and other
114 114
115 115 Return None if the factor is insignicant (less than <sensitivity>
116 116 variation)."""
117 117 factor = 1
118 118 if main is not None:
119 119 factor = other[field] / main[field]
120 120 low, high = 1 - sensitivity, 1 + sensitivity
121 121 if (low < factor < high):
122 122 return None
123 123 return factor
124 124
125 125 def formatfactor(factor):
126 126 """format a factor into a 4 char string
127 127
128 128 22%
129 129 156%
130 130 x2.4
131 131 x23
132 132 x789
133 133 x1e4
134 134 x5x7
135 135
136 136 """
137 137 if factor is None:
138 138 return ' '
139 139 elif factor < 2:
140 140 return '%3i%%' % (factor * 100)
141 141 elif factor < 10:
142 142 return 'x%3.1f' % factor
143 143 elif factor < 1000:
144 144 return '%4s' % ('x%i' % factor)
145 145 else:
146 146 order = int(math.log(factor)) + 1
147 147 while 1 < math.log(factor):
148 148 factor //= 0
149 149 return 'x%ix%i' % (factor, order)
150 150
151 151 def formattiming(value):
152 152 """format a value to strictly 8 char, dropping some precision if needed"""
153 153 if value < 10**7:
154 154 return ('%.6f' % value)[:8]
155 155 else:
156 156 # value is HUGE very unlikely to happen (4+ month run)
157 157 return '%i' % value
158 158
159 159 _marker = object()
160 160 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
161 161 """print a line of result to stdout"""
162 162 mask = '%%0%ii) %%s' % idxwidth(maxidx)
163 163
164 164 out = []
165 165 for var in variants:
166 166 if data[var] is None:
167 167 out.append('error ')
168 168 out.append(' ' * 4)
169 169 continue
170 170 out.append(formattiming(data[var]['wall']))
171 171 if reference is not _marker:
172 172 factor = None
173 173 if reference is not None:
174 174 factor = getfactor(reference[var], data[var], 'wall')
175 175 out.append(formatfactor(factor))
176 176 if verbose:
177 177 out.append(formattiming(data[var]['comb']))
178 178 out.append(formattiming(data[var]['user']))
179 179 out.append(formattiming(data[var]['sys']))
180 180 out.append('%6d' % data[var]['count'])
181 181 print mask % (idx, ' '.join(out))
182 182
183 183 def printheader(variants, maxidx, verbose=False, relative=False):
184 184 header = [' ' * (idxwidth(maxidx) + 1)]
185 185 for var in variants:
186 186 if not var:
187 187 var = 'iter'
188 188 if 8 < len(var):
189 189 var = var[:3] + '..' + var[-3:]
190 190 header.append('%-8s' % var)
191 191 if relative:
192 192 header.append(' ')
193 193 if verbose:
194 194 header.append('%-8s' % 'comb')
195 195 header.append('%-8s' % 'user')
196 196 header.append('%-8s' % 'sys')
197 197 header.append('%6s' % 'count')
198 198 print ' '.join(header)
199 199
200 200 def getrevs(spec):
201 201 """get the list of rev matched by a revset"""
202 202 try:
203 203 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
204 except CalledProcessError, exc:
204 except CalledProcessError as exc:
205 205 print >> sys.stderr, "abort, can't get revision from %s" % spec
206 206 sys.exit(exc.returncode)
207 207 return [r for r in out.split() if r]
208 208
209 209
210 210 def applyvariants(revset, variant):
211 211 if variant == 'plain':
212 212 return revset
213 213 for var in variant.split('+'):
214 214 revset = '%s(%s)' % (var, revset)
215 215 return revset
216 216
217 217 helptext="""This script will run multiple variants of provided revsets using
218 218 different revisions in your mercurial repository. After the benchmark are run
219 219 summary output is provided. Use itto demonstrate speed improvements or pin
220 220 point regressions. Revsets to run are specified in a file (or from stdin), one
221 221 revsets per line. Line starting with '#' will be ignored, allowing insertion of
222 222 comments."""
223 223 parser = OptionParser(usage="usage: %prog [options] <revs>",
224 224 description=helptext)
225 225 parser.add_option("-f", "--file",
226 226 help="read revset from FILE (stdin if omitted)",
227 227 metavar="FILE")
228 228 parser.add_option("-R", "--repo",
229 229 help="run benchmark on REPO", metavar="REPO")
230 230
231 231 parser.add_option("-v", "--verbose",
232 232 action='store_true',
233 233 help="display all timing data (not just best total time)")
234 234
235 235 parser.add_option("", "--variants",
236 236 default=','.join(DEFAULTVARIANTS),
237 237 help="comma separated list of variant to test "
238 238 "(eg: plain,min,sorted) (plain = no modification)")
239 239
240 240 (options, args) = parser.parse_args()
241 241
242 242 if not args:
243 243 parser.print_help()
244 244 sys.exit(255)
245 245
246 246 # the directory where both this script and the perf.py extension live.
247 247 contribdir = os.path.dirname(__file__)
248 248
249 249 revsetsfile = sys.stdin
250 250 if options.file:
251 251 revsetsfile = open(options.file)
252 252
253 253 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
254 254 revsets = [l for l in revsets if l]
255 255
256 256 print "Revsets to benchmark"
257 257 print "----------------------------"
258 258
259 259 for idx, rset in enumerate(revsets):
260 260 print "%i) %s" % (idx, rset)
261 261
262 262 print "----------------------------"
263 263 print
264 264
265 265 revs = []
266 266 for a in args:
267 267 revs.extend(getrevs(a))
268 268
269 269 variants = options.variants.split(',')
270 270
271 271 results = []
272 272 for r in revs:
273 273 print "----------------------------"
274 274 printrevision(r)
275 275 print "----------------------------"
276 276 update(r)
277 277 res = []
278 278 results.append(res)
279 279 printheader(variants, len(revsets), verbose=options.verbose)
280 280 for idx, rset in enumerate(revsets):
281 281 varres = {}
282 282 for var in variants:
283 283 varrset = applyvariants(rset, var)
284 284 data = perf(varrset, target=options.repo)
285 285 varres[var] = data
286 286 res.append(varres)
287 287 printresult(variants, idx, varres, len(revsets),
288 288 verbose=options.verbose)
289 289 sys.stdout.flush()
290 290 print "----------------------------"
291 291
292 292
293 293 print """
294 294
295 295 Result by revset
296 296 ================
297 297 """
298 298
299 299 print 'Revision:'
300 300 for idx, rev in enumerate(revs):
301 301 sys.stdout.write('%i) ' % idx)
302 302 sys.stdout.flush()
303 303 printrevision(rev)
304 304
305 305 print
306 306 print
307 307
308 308 for ridx, rset in enumerate(revsets):
309 309
310 310 print "revset #%i: %s" % (ridx, rset)
311 311 printheader(variants, len(results), verbose=options.verbose, relative=True)
312 312 ref = None
313 313 for idx, data in enumerate(results):
314 314 printresult(variants, idx, data[ridx], len(results),
315 315 verbose=options.verbose, reference=ref)
316 316 ref = data[ridx]
317 317 print
@@ -1,495 +1,495 b''
1 1 # synthrepo.py - repo synthesis
2 2 #
3 3 # Copyright 2012 Facebook
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 '''synthesize structurally interesting change history
9 9
10 10 This extension is useful for creating a repository with properties
11 11 that are statistically similar to an existing repository. During
12 12 analysis, a simple probability table is constructed from the history
13 13 of an existing repository. During synthesis, these properties are
14 14 reconstructed.
15 15
16 16 Properties that are analyzed and synthesized include the following:
17 17
18 18 - Lines added or removed when an existing file is modified
19 19 - Number and sizes of files added
20 20 - Number of files removed
21 21 - Line lengths
22 22 - Topological distance to parent changeset(s)
23 23 - Probability of a commit being a merge
24 24 - Probability of a newly added file being added to a new directory
25 25 - Interarrival time, and time zone, of commits
26 26 - Number of files in each directory
27 27
28 28 A few obvious properties that are not currently handled realistically:
29 29
30 30 - Merges are treated as regular commits with two parents, which is not
31 31 realistic
32 32 - Modifications are not treated as operations on hunks of lines, but
33 33 as insertions and deletions of randomly chosen single lines
34 34 - Committer ID (always random)
35 35 - Executability of files
36 36 - Symlinks and binary files are ignored
37 37 '''
38 38
39 39 import bisect, collections, itertools, json, os, random, time, sys
40 40 from mercurial import cmdutil, context, patch, scmutil, util, hg
41 41 from mercurial.i18n import _
42 42 from mercurial.node import nullrev, nullid, short
43 43
44 44 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 46 # be specifying the version(s) of Mercurial they are tested with, or
47 47 # leave the attribute unspecified.
48 48 testedwith = 'internal'
49 49
50 50 cmdtable = {}
51 51 command = cmdutil.command(cmdtable)
52 52
53 53 newfile = set(('new fi', 'rename', 'copy f', 'copy t'))
54 54
55 55 def zerodict():
56 56 return collections.defaultdict(lambda: 0)
57 57
58 58 def roundto(x, k):
59 59 if x > k * 2:
60 60 return int(round(x / float(k)) * k)
61 61 return int(round(x))
62 62
63 63 def parsegitdiff(lines):
64 64 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
65 65 binary = False
66 66 for line in lines:
67 67 start = line[:6]
68 68 if start == 'diff -':
69 69 if filename:
70 70 yield filename, mar, lineadd, lineremove, binary
71 71 mar, lineadd, lineremove, binary = 'm', zerodict(), 0, False
72 72 filename = patch.gitre.match(line).group(1)
73 73 elif start in newfile:
74 74 mar = 'a'
75 75 elif start == 'GIT bi':
76 76 binary = True
77 77 elif start == 'delete':
78 78 mar = 'r'
79 79 elif start:
80 80 s = start[0]
81 81 if s == '-' and not line.startswith('--- '):
82 82 lineremove += 1
83 83 elif s == '+' and not line.startswith('+++ '):
84 84 lineadd[roundto(len(line) - 1, 5)] += 1
85 85 if filename:
86 86 yield filename, mar, lineadd, lineremove, binary
87 87
88 88 @command('analyze',
89 89 [('o', 'output', '', _('write output to given file'), _('FILE')),
90 90 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
91 91 _('hg analyze'), optionalrepo=True)
92 92 def analyze(ui, repo, *revs, **opts):
93 93 '''create a simple model of a repository to use for later synthesis
94 94
95 95 This command examines every changeset in the given range (or all
96 96 of history if none are specified) and creates a simple statistical
97 97 model of the history of the repository. It also measures the directory
98 98 structure of the repository as checked out.
99 99
100 100 The model is written out to a JSON file, and can be used by
101 101 :hg:`synthesize` to create or augment a repository with synthetic
102 102 commits that have a structure that is statistically similar to the
103 103 analyzed repository.
104 104 '''
105 105 root = repo.root
106 106 if not root.endswith(os.path.sep):
107 107 root += os.path.sep
108 108
109 109 revs = list(revs)
110 110 revs.extend(opts['rev'])
111 111 if not revs:
112 112 revs = [':']
113 113
114 114 output = opts['output']
115 115 if not output:
116 116 output = os.path.basename(root) + '.json'
117 117
118 118 if output == '-':
119 119 fp = sys.stdout
120 120 else:
121 121 fp = open(output, 'w')
122 122
123 123 # Always obtain file counts of each directory in the given root directory.
124 124 def onerror(e):
125 125 ui.warn(_('error walking directory structure: %s\n') % e)
126 126
127 127 dirs = {}
128 128 rootprefixlen = len(root)
129 129 for dirpath, dirnames, filenames in os.walk(root, onerror=onerror):
130 130 dirpathfromroot = dirpath[rootprefixlen:]
131 131 dirs[dirpathfromroot] = len(filenames)
132 132 if '.hg' in dirnames:
133 133 dirnames.remove('.hg')
134 134
135 135 lineschanged = zerodict()
136 136 children = zerodict()
137 137 p1distance = zerodict()
138 138 p2distance = zerodict()
139 139 linesinfilesadded = zerodict()
140 140 fileschanged = zerodict()
141 141 filesadded = zerodict()
142 142 filesremoved = zerodict()
143 143 linelengths = zerodict()
144 144 interarrival = zerodict()
145 145 parents = zerodict()
146 146 dirsadded = zerodict()
147 147 tzoffset = zerodict()
148 148
149 149 # If a mercurial repo is available, also model the commit history.
150 150 if repo:
151 151 revs = scmutil.revrange(repo, revs)
152 152 revs.sort()
153 153
154 154 progress = ui.progress
155 155 _analyzing = _('analyzing')
156 156 _changesets = _('changesets')
157 157 _total = len(revs)
158 158
159 159 for i, rev in enumerate(revs):
160 160 progress(_analyzing, i, unit=_changesets, total=_total)
161 161 ctx = repo[rev]
162 162 pl = ctx.parents()
163 163 pctx = pl[0]
164 164 prev = pctx.rev()
165 165 children[prev] += 1
166 166 p1distance[rev - prev] += 1
167 167 parents[len(pl)] += 1
168 168 tzoffset[ctx.date()[1]] += 1
169 169 if len(pl) > 1:
170 170 p2distance[rev - pl[1].rev()] += 1
171 171 if prev == rev - 1:
172 172 lastctx = pctx
173 173 else:
174 174 lastctx = repo[rev - 1]
175 175 if lastctx.rev() != nullrev:
176 176 timedelta = ctx.date()[0] - lastctx.date()[0]
177 177 interarrival[roundto(timedelta, 300)] += 1
178 178 diff = sum((d.splitlines() for d in ctx.diff(pctx, git=True)), [])
179 179 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
180 180 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
181 181 if isbin:
182 182 continue
183 183 added = sum(lineadd.itervalues(), 0)
184 184 if mar == 'm':
185 185 if added and lineremove:
186 186 lineschanged[roundto(added, 5),
187 187 roundto(lineremove, 5)] += 1
188 188 filechanges += 1
189 189 elif mar == 'a':
190 190 fileadds += 1
191 191 if '/' in filename:
192 192 filedir = filename.rsplit('/', 1)[0]
193 193 if filedir not in pctx.dirs():
194 194 diradds += 1
195 195 linesinfilesadded[roundto(added, 5)] += 1
196 196 elif mar == 'r':
197 197 fileremoves += 1
198 198 for length, count in lineadd.iteritems():
199 199 linelengths[length] += count
200 200 fileschanged[filechanges] += 1
201 201 filesadded[fileadds] += 1
202 202 dirsadded[diradds] += 1
203 203 filesremoved[fileremoves] += 1
204 204
205 205 invchildren = zerodict()
206 206
207 207 for rev, count in children.iteritems():
208 208 invchildren[count] += 1
209 209
210 210 if output != '-':
211 211 ui.status(_('writing output to %s\n') % output)
212 212
213 213 def pronk(d):
214 214 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
215 215
216 216 json.dump({'revs': len(revs),
217 217 'initdirs': pronk(dirs),
218 218 'lineschanged': pronk(lineschanged),
219 219 'children': pronk(invchildren),
220 220 'fileschanged': pronk(fileschanged),
221 221 'filesadded': pronk(filesadded),
222 222 'linesinfilesadded': pronk(linesinfilesadded),
223 223 'dirsadded': pronk(dirsadded),
224 224 'filesremoved': pronk(filesremoved),
225 225 'linelengths': pronk(linelengths),
226 226 'parents': pronk(parents),
227 227 'p1distance': pronk(p1distance),
228 228 'p2distance': pronk(p2distance),
229 229 'interarrival': pronk(interarrival),
230 230 'tzoffset': pronk(tzoffset),
231 231 },
232 232 fp)
233 233 fp.close()
234 234
235 235 @command('synthesize',
236 236 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
237 237 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
238 238 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
239 239 _('hg synthesize [OPTION].. DESCFILE'))
240 240 def synthesize(ui, repo, descpath, **opts):
241 241 '''synthesize commits based on a model of an existing repository
242 242
243 243 The model must have been generated by :hg:`analyze`. Commits will
244 244 be generated randomly according to the probabilities described in
245 245 the model. If --initfiles is set, the repository will be seeded with
246 246 the given number files following the modeled repository's directory
247 247 structure.
248 248
249 249 When synthesizing new content, commit descriptions, and user
250 250 names, words will be chosen randomly from a dictionary that is
251 251 presumed to contain one word per line. Use --dict to specify the
252 252 path to an alternate dictionary to use.
253 253 '''
254 254 try:
255 255 fp = hg.openpath(ui, descpath)
256 except Exception, err:
256 except Exception as err:
257 257 raise util.Abort('%s: %s' % (descpath, err[0].strerror))
258 258 desc = json.load(fp)
259 259 fp.close()
260 260
261 261 def cdf(l):
262 262 if not l:
263 263 return [], []
264 264 vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True))
265 265 t = float(sum(probs, 0))
266 266 s, cdfs = 0, []
267 267 for v in probs:
268 268 s += v
269 269 cdfs.append(s / t)
270 270 return vals, cdfs
271 271
272 272 lineschanged = cdf(desc['lineschanged'])
273 273 fileschanged = cdf(desc['fileschanged'])
274 274 filesadded = cdf(desc['filesadded'])
275 275 dirsadded = cdf(desc['dirsadded'])
276 276 filesremoved = cdf(desc['filesremoved'])
277 277 linelengths = cdf(desc['linelengths'])
278 278 parents = cdf(desc['parents'])
279 279 p1distance = cdf(desc['p1distance'])
280 280 p2distance = cdf(desc['p2distance'])
281 281 interarrival = cdf(desc['interarrival'])
282 282 linesinfilesadded = cdf(desc['linesinfilesadded'])
283 283 tzoffset = cdf(desc['tzoffset'])
284 284
285 285 dictfile = opts.get('dict') or '/usr/share/dict/words'
286 286 try:
287 287 fp = open(dictfile, 'rU')
288 except IOError, err:
288 except IOError as err:
289 289 raise util.Abort('%s: %s' % (dictfile, err.strerror))
290 290 words = fp.read().splitlines()
291 291 fp.close()
292 292
293 293 initdirs = {}
294 294 if desc['initdirs']:
295 295 for k, v in desc['initdirs']:
296 296 initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v
297 297 initdirs = renamedirs(initdirs, words)
298 298 initdirscdf = cdf(initdirs)
299 299
300 300 def pick(cdf):
301 301 return cdf[0][bisect.bisect_left(cdf[1], random.random())]
302 302
303 303 def pickpath():
304 304 return os.path.join(pick(initdirscdf), random.choice(words))
305 305
306 306 def makeline(minimum=0):
307 307 total = max(minimum, pick(linelengths))
308 308 c, l = 0, []
309 309 while c < total:
310 310 w = random.choice(words)
311 311 c += len(w) + 1
312 312 l.append(w)
313 313 return ' '.join(l)
314 314
315 315 wlock = repo.wlock()
316 316 lock = repo.lock()
317 317
318 318 nevertouch = set(('.hgsub', '.hgignore', '.hgtags'))
319 319
320 320 progress = ui.progress
321 321 _synthesizing = _('synthesizing')
322 322 _files = _('initial files')
323 323 _changesets = _('changesets')
324 324
325 325 # Synthesize a single initial revision adding files to the repo according
326 326 # to the modeled directory structure.
327 327 initcount = int(opts['initfiles'])
328 328 if initcount and initdirs:
329 329 pctx = repo[None].parents()[0]
330 330 dirs = set(pctx.dirs())
331 331 files = {}
332 332
333 333 def validpath(path):
334 334 # Don't pick filenames which are already directory names.
335 335 if path in dirs:
336 336 return False
337 337 # Don't pick directories which were used as file names.
338 338 while path:
339 339 if path in files:
340 340 return False
341 341 path = os.path.dirname(path)
342 342 return True
343 343
344 344 for i in xrange(0, initcount):
345 345 ui.progress(_synthesizing, i, unit=_files, total=initcount)
346 346
347 347 path = pickpath()
348 348 while not validpath(path):
349 349 path = pickpath()
350 350 data = '%s contents\n' % path
351 351 files[path] = context.memfilectx(repo, path, data)
352 352 dir = os.path.dirname(path)
353 353 while dir and dir not in dirs:
354 354 dirs.add(dir)
355 355 dir = os.path.dirname(dir)
356 356
357 357 def filectxfn(repo, memctx, path):
358 358 return files[path]
359 359
360 360 ui.progress(_synthesizing, None)
361 361 message = 'synthesized wide repo with %d files' % (len(files),)
362 362 mc = context.memctx(repo, [pctx.node(), nullid], message,
363 363 files.iterkeys(), filectxfn, ui.username(),
364 364 '%d %d' % util.makedate())
365 365 initnode = mc.commit()
366 366 if ui.debugflag:
367 367 hexfn = hex
368 368 else:
369 369 hexfn = short
370 370 ui.status(_('added commit %s with %d files\n')
371 371 % (hexfn(initnode), len(files)))
372 372
373 373 # Synthesize incremental revisions to the repository, adding repo depth.
374 374 count = int(opts['count'])
375 375 heads = set(map(repo.changelog.rev, repo.heads()))
376 376 for i in xrange(count):
377 377 progress(_synthesizing, i, unit=_changesets, total=count)
378 378
379 379 node = repo.changelog.node
380 380 revs = len(repo)
381 381
382 382 def pickhead(heads, distance):
383 383 if heads:
384 384 lheads = sorted(heads)
385 385 rev = revs - min(pick(distance), revs)
386 386 if rev < lheads[-1]:
387 387 rev = lheads[bisect.bisect_left(lheads, rev)]
388 388 else:
389 389 rev = lheads[-1]
390 390 return rev, node(rev)
391 391 return nullrev, nullid
392 392
393 393 r1 = revs - min(pick(p1distance), revs)
394 394 p1 = node(r1)
395 395
396 396 # the number of heads will grow without bound if we use a pure
397 397 # model, so artificially constrain their proliferation
398 398 toomanyheads = len(heads) > random.randint(1, 20)
399 399 if p2distance[0] and (pick(parents) == 2 or toomanyheads):
400 400 r2, p2 = pickhead(heads.difference([r1]), p2distance)
401 401 else:
402 402 r2, p2 = nullrev, nullid
403 403
404 404 pl = [p1, p2]
405 405 pctx = repo[r1]
406 406 mf = pctx.manifest()
407 407 mfk = mf.keys()
408 408 changes = {}
409 409 if mfk:
410 410 for __ in xrange(pick(fileschanged)):
411 411 for __ in xrange(10):
412 412 fctx = pctx.filectx(random.choice(mfk))
413 413 path = fctx.path()
414 414 if not (path in nevertouch or fctx.isbinary() or
415 415 'l' in fctx.flags()):
416 416 break
417 417 lines = fctx.data().splitlines()
418 418 add, remove = pick(lineschanged)
419 419 for __ in xrange(remove):
420 420 if not lines:
421 421 break
422 422 del lines[random.randrange(0, len(lines))]
423 423 for __ in xrange(add):
424 424 lines.insert(random.randint(0, len(lines)), makeline())
425 425 path = fctx.path()
426 426 changes[path] = context.memfilectx(repo, path,
427 427 '\n'.join(lines) + '\n')
428 428 for __ in xrange(pick(filesremoved)):
429 429 path = random.choice(mfk)
430 430 for __ in xrange(10):
431 431 path = random.choice(mfk)
432 432 if path not in changes:
433 433 changes[path] = None
434 434 break
435 435 if filesadded:
436 436 dirs = list(pctx.dirs())
437 437 dirs.insert(0, '')
438 438 for __ in xrange(pick(filesadded)):
439 439 pathstr = ''
440 440 while pathstr in dirs:
441 441 path = [random.choice(dirs)]
442 442 if pick(dirsadded):
443 443 path.append(random.choice(words))
444 444 path.append(random.choice(words))
445 445 pathstr = '/'.join(filter(None, path))
446 446 data = '\n'.join(makeline()
447 447 for __ in xrange(pick(linesinfilesadded))) + '\n'
448 448 changes[pathstr] = context.memfilectx(repo, pathstr, data)
449 449 def filectxfn(repo, memctx, path):
450 450 return changes[path]
451 451 if not changes:
452 452 continue
453 453 if revs:
454 454 date = repo['tip'].date()[0] + pick(interarrival)
455 455 else:
456 456 date = time.time() - (86400 * count)
457 457 # dates in mercurial must be positive, fit in 32-bit signed integers.
458 458 date = min(0x7fffffff, max(0, date))
459 459 user = random.choice(words) + '@' + random.choice(words)
460 460 mc = context.memctx(repo, pl, makeline(minimum=2),
461 461 sorted(changes.iterkeys()),
462 462 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
463 463 newnode = mc.commit()
464 464 heads.add(repo.changelog.rev(newnode))
465 465 heads.discard(r1)
466 466 heads.discard(r2)
467 467
468 468 lock.release()
469 469 wlock.release()
470 470
471 471 def renamedirs(dirs, words):
472 472 '''Randomly rename the directory names in the per-dir file count dict.'''
473 473 wordgen = itertools.cycle(words)
474 474 replacements = {'': ''}
475 475 def rename(dirpath):
476 476 '''Recursively rename the directory and all path prefixes.
477 477
478 478 The mapping from path to renamed path is stored for all path prefixes
479 479 as in dynamic programming, ensuring linear runtime and consistent
480 480 renaming regardless of iteration order through the model.
481 481 '''
482 482 if dirpath in replacements:
483 483 return replacements[dirpath]
484 484 head, _ = os.path.split(dirpath)
485 485 if head:
486 486 head = rename(head)
487 487 else:
488 488 head = ''
489 489 renamed = os.path.join(head, wordgen.next())
490 490 replacements[dirpath] = renamed
491 491 return renamed
492 492 result = []
493 493 for dirpath, count in dirs.iteritems():
494 494 result.append([rename(dirpath.lstrip(os.sep)), count])
495 495 return result
@@ -1,162 +1,162 b''
1 1 # blackbox.py - log repository events to a file for post-mortem debugging
2 2 #
3 3 # Copyright 2010 Nicolas Dumazet
4 4 # Copyright 2013 Facebook, Inc.
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """log repository events to a blackbox for debugging
10 10
11 11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
12 12 The events that get logged can be configured via the blackbox.track config key.
13 13 Examples::
14 14
15 15 [blackbox]
16 16 track = *
17 17
18 18 [blackbox]
19 19 track = command, commandfinish, commandexception, exthook, pythonhook
20 20
21 21 [blackbox]
22 22 track = incoming
23 23
24 24 [blackbox]
25 25 # limit the size of a log file
26 26 maxsize = 1.5 MB
27 27 # rotate up to N log files when the current one gets too big
28 28 maxfiles = 3
29 29
30 30 """
31 31
32 32 from mercurial import util, cmdutil
33 33 from mercurial.i18n import _
34 34 import errno, os, re
35 35
36 36 cmdtable = {}
37 37 command = cmdutil.command(cmdtable)
38 38 # Note for extension authors: ONLY specify testedwith = 'internal' for
39 39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 40 # be specifying the version(s) of Mercurial they are tested with, or
41 41 # leave the attribute unspecified.
42 42 testedwith = 'internal'
43 43 lastblackbox = None
44 44
45 45 def wrapui(ui):
46 46 class blackboxui(ui.__class__):
47 47 @util.propertycache
48 48 def track(self):
49 49 return self.configlist('blackbox', 'track', ['*'])
50 50
51 51 def _openlogfile(self):
52 52 def rotate(oldpath, newpath):
53 53 try:
54 54 os.unlink(newpath)
55 except OSError, err:
55 except OSError as err:
56 56 if err.errno != errno.ENOENT:
57 57 self.debug("warning: cannot remove '%s': %s\n" %
58 58 (newpath, err.strerror))
59 59 try:
60 60 if newpath:
61 61 os.rename(oldpath, newpath)
62 except OSError, err:
62 except OSError as err:
63 63 if err.errno != errno.ENOENT:
64 64 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
65 65 (newpath, oldpath, err.strerror))
66 66
67 67 fp = self._bbopener('blackbox.log', 'a')
68 68 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
69 69 if maxsize > 0:
70 70 st = os.fstat(fp.fileno())
71 71 if st.st_size >= maxsize:
72 72 path = fp.name
73 73 fp.close()
74 74 maxfiles = self.configint('blackbox', 'maxfiles', 7)
75 75 for i in xrange(maxfiles - 1, 1, -1):
76 76 rotate(oldpath='%s.%d' % (path, i - 1),
77 77 newpath='%s.%d' % (path, i))
78 78 rotate(oldpath=path,
79 79 newpath=maxfiles > 0 and path + '.1')
80 80 fp = self._bbopener('blackbox.log', 'a')
81 81 return fp
82 82
83 83 def log(self, event, *msg, **opts):
84 84 global lastblackbox
85 85 super(blackboxui, self).log(event, *msg, **opts)
86 86
87 87 if not '*' in self.track and not event in self.track:
88 88 return
89 89
90 90 if util.safehasattr(self, '_blackbox'):
91 91 blackbox = self._blackbox
92 92 elif util.safehasattr(self, '_bbopener'):
93 93 try:
94 94 self._blackbox = self._openlogfile()
95 except (IOError, OSError), err:
95 except (IOError, OSError) as err:
96 96 self.debug('warning: cannot write to blackbox.log: %s\n' %
97 97 err.strerror)
98 98 del self._bbopener
99 99 self._blackbox = None
100 100 blackbox = self._blackbox
101 101 else:
102 102 # certain ui instances exist outside the context of
103 103 # a repo, so just default to the last blackbox that
104 104 # was seen.
105 105 blackbox = lastblackbox
106 106
107 107 if blackbox:
108 108 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
109 109 user = util.getuser()
110 110 formattedmsg = msg[0] % msg[1:]
111 111 try:
112 112 blackbox.write('%s %s> %s' % (date, user, formattedmsg))
113 except IOError, err:
113 except IOError as err:
114 114 self.debug('warning: cannot write to blackbox.log: %s\n' %
115 115 err.strerror)
116 116 lastblackbox = blackbox
117 117
118 118 def setrepo(self, repo):
119 119 self._bbopener = repo.vfs
120 120
121 121 ui.__class__ = blackboxui
122 122
123 123 def uisetup(ui):
124 124 wrapui(ui)
125 125
126 126 def reposetup(ui, repo):
127 127 # During 'hg pull' a httppeer repo is created to represent the remote repo.
128 128 # It doesn't have a .hg directory to put a blackbox in, so we don't do
129 129 # the blackbox setup for it.
130 130 if not repo.local():
131 131 return
132 132
133 133 if util.safehasattr(ui, 'setrepo'):
134 134 ui.setrepo(repo)
135 135
136 136 @command('^blackbox',
137 137 [('l', 'limit', 10, _('the number of events to show')),
138 138 ],
139 139 _('hg blackbox [OPTION]...'))
140 140 def blackbox(ui, repo, *revs, **opts):
141 141 '''view the recent repository events
142 142 '''
143 143
144 144 if not os.path.exists(repo.join('blackbox.log')):
145 145 return
146 146
147 147 limit = opts.get('limit')
148 148 blackbox = repo.vfs('blackbox.log', 'r')
149 149 lines = blackbox.read().split('\n')
150 150
151 151 count = 0
152 152 output = []
153 153 for line in reversed(lines):
154 154 if count >= limit:
155 155 break
156 156
157 157 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
158 158 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
159 159 count += 1
160 160 output.append(line)
161 161
162 162 ui.status('\n'.join(reversed(output)))
@@ -1,914 +1,914 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Three basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21 21
22 22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24 24
25 25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 26 using MySQL are supported. Requires Python MySQLdb.
27 27
28 28 Writing directly to the database is susceptible to schema changes, and
29 29 relies on a Bugzilla contrib script to send out bug change
30 30 notification emails. This script runs as the user running Mercurial,
31 31 must be run on the host with the Bugzilla install, and requires
32 32 permission to read Bugzilla configuration details and the necessary
33 33 MySQL user and password to have full access rights to the Bugzilla
34 34 database. For these reasons this access mode is now considered
35 35 deprecated, and will not be updated for new Bugzilla versions going
36 36 forward. Only adding comments is supported in this access mode.
37 37
38 38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 39 in the configuration. Comments are added under that username. Since the
40 40 configuration must be readable by all Mercurial users, it is recommended
41 41 that the rights of that user are restricted in Bugzilla to the minimum
42 42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43 43
44 44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 45 email to the Bugzilla email interface to submit comments to bugs.
46 46 The From: address in the email is set to the email address of the Mercurial
47 47 user, so the comment appears to come from the Mercurial user. In the event
48 48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
49 49 user, the email associated with the Bugzilla username used to log into
50 50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 51 works on all supported Bugzilla versions.
52 52
53 53 Configuration items common to all access modes:
54 54
55 55 bugzilla.version
56 56 The access type to use. Values recognized are:
57 57
58 58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 62 including 3.0.
63 63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 64 including 2.18.
65 65
66 66 bugzilla.regexp
67 67 Regular expression to match bug IDs for update in changeset commit message.
68 68 It must contain one "()" named group ``<ids>`` containing the bug
69 69 IDs separated by non-digit characters. It may also contain
70 70 a named group ``<hours>`` with a floating-point number giving the
71 71 hours worked on the bug. If no named groups are present, the first
72 72 "()" group is assumed to contain the bug IDs, and work time is not
73 73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 75 variations thereof, followed by an hours number prefixed by ``h`` or
76 76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77 77
78 78 bugzilla.fixregexp
79 79 Regular expression to match bug IDs for marking fixed in changeset
80 80 commit message. This must contain a "()" named group ``<ids>` containing
81 81 the bug IDs separated by non-digit characters. It may also contain
82 82 a named group ``<hours>`` with a floating-point number giving the
83 83 hours worked on the bug. If no named groups are present, the first
84 84 "()" group is assumed to contain the bug IDs, and work time is not
85 85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 87 variations thereof, followed by an hours number prefixed by ``h`` or
88 88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89 89
90 90 bugzilla.fixstatus
91 91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92 92
93 93 bugzilla.fixresolution
94 94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95 95
96 96 bugzilla.style
97 97 The style file to use when formatting comments.
98 98
99 99 bugzilla.template
100 100 Template to use when formatting comments. Overrides style if
101 101 specified. In addition to the usual Mercurial keywords, the
102 102 extension specifies:
103 103
104 104 :``{bug}``: The Bugzilla bug ID.
105 105 :``{root}``: The full pathname of the Mercurial repository.
106 106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108 108
109 109 Default ``changeset {node|short} in repo {root} refers to bug
110 110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111 111
112 112 bugzilla.strip
113 113 The number of path separator characters to strip from the front of
114 114 the Mercurial repository path (``{root}`` in templates) to produce
115 115 ``{webroot}``. For example, a repository with ``{root}``
116 116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 117 ``{webroot}`` of ``my-project``. Default 0.
118 118
119 119 web.baseurl
120 120 Base URL for browsing Mercurial repositories. Referenced from
121 121 templates as ``{hgweb}``.
122 122
123 123 Configuration items common to XMLRPC+email and MySQL access modes:
124 124
125 125 bugzilla.usermap
126 126 Path of file containing Mercurial committer email to Bugzilla user email
127 127 mappings. If specified, the file should contain one mapping per
128 128 line::
129 129
130 130 committer = Bugzilla user
131 131
132 132 See also the ``[usermap]`` section.
133 133
134 134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 136 Contains entries of the form ``committer = Bugzilla user``.
137 137
138 138 XMLRPC access mode configuration:
139 139
140 140 bugzilla.bzurl
141 141 The base URL for the Bugzilla installation.
142 142 Default ``http://localhost/bugzilla``.
143 143
144 144 bugzilla.user
145 145 The username to use to log into Bugzilla via XMLRPC. Default
146 146 ``bugs``.
147 147
148 148 bugzilla.password
149 149 The password for Bugzilla login.
150 150
151 151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 152 and also:
153 153
154 154 bugzilla.bzemail
155 155 The Bugzilla email address.
156 156
157 157 In addition, the Mercurial email settings must be configured. See the
158 158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159 159
160 160 MySQL access mode configuration:
161 161
162 162 bugzilla.host
163 163 Hostname of the MySQL server holding the Bugzilla database.
164 164 Default ``localhost``.
165 165
166 166 bugzilla.db
167 167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168 168
169 169 bugzilla.user
170 170 Username to use to access MySQL server. Default ``bugs``.
171 171
172 172 bugzilla.password
173 173 Password to use to access MySQL server.
174 174
175 175 bugzilla.timeout
176 176 Database connection timeout (seconds). Default 5.
177 177
178 178 bugzilla.bzuser
179 179 Fallback Bugzilla user name to record comments with, if changeset
180 180 committer cannot be found as a Bugzilla user.
181 181
182 182 bugzilla.bzdir
183 183 Bugzilla install directory. Used by default notify. Default
184 184 ``/var/www/html/bugzilla``.
185 185
186 186 bugzilla.notify
187 187 The command to run to get Bugzilla to send bug change notification
188 188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 189 id) and ``user`` (committer bugzilla email). Default depends on
190 190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 191 contrib/sendbugmail.pl %(id)s %(user)s".
192 192
193 193 Activating the extension::
194 194
195 195 [extensions]
196 196 bugzilla =
197 197
198 198 [hooks]
199 199 # run bugzilla hook on every change pulled or pushed in here
200 200 incoming.bugzilla = python:hgext.bugzilla.hook
201 201
202 202 Example configurations:
203 203
204 204 XMLRPC example configuration. This uses the Bugzilla at
205 205 ``http://my-project.org/bugzilla``, logging in as user
206 206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 208 with a web interface at ``http://my-project.org/hg``. ::
209 209
210 210 [bugzilla]
211 211 bzurl=http://my-project.org/bugzilla
212 212 user=bugmail@my-project.org
213 213 password=plugh
214 214 version=xmlrpc
215 215 template=Changeset {node|short} in {root|basename}.
216 216 {hgweb}/{webroot}/rev/{node|short}\\n
217 217 {desc}\\n
218 218 strip=5
219 219
220 220 [web]
221 221 baseurl=http://my-project.org/hg
222 222
223 223 XMLRPC+email example configuration. This uses the Bugzilla at
224 224 ``http://my-project.org/bugzilla``, logging in as user
225 225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 228 are sent to the Bugzilla email address
229 229 ``bugzilla@my-project.org``. ::
230 230
231 231 [bugzilla]
232 232 bzurl=http://my-project.org/bugzilla
233 233 user=bugmail@my-project.org
234 234 password=plugh
235 235 version=xmlrpc+email
236 236 bzemail=bugzilla@my-project.org
237 237 template=Changeset {node|short} in {root|basename}.
238 238 {hgweb}/{webroot}/rev/{node|short}\\n
239 239 {desc}\\n
240 240 strip=5
241 241
242 242 [web]
243 243 baseurl=http://my-project.org/hg
244 244
245 245 [usermap]
246 246 user@emaildomain.com=user.name@bugzilladomain.com
247 247
248 248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 250 the Bugzilla database name is ``bugs`` and MySQL is
251 251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 253 with a web interface at ``http://my-project.org/hg``. ::
254 254
255 255 [bugzilla]
256 256 host=localhost
257 257 password=XYZZY
258 258 version=3.0
259 259 bzuser=unknown@domain.com
260 260 bzdir=/opt/bugzilla-3.2
261 261 template=Changeset {node|short} in {root|basename}.
262 262 {hgweb}/{webroot}/rev/{node|short}\\n
263 263 {desc}\\n
264 264 strip=5
265 265
266 266 [web]
267 267 baseurl=http://my-project.org/hg
268 268
269 269 [usermap]
270 270 user@emaildomain.com=user.name@bugzilladomain.com
271 271
272 272 All the above add a comment to the Bugzilla bug record of the form::
273 273
274 274 Changeset 3b16791d6642 in repository-name.
275 275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276 276
277 277 Changeset commit comment. Bug 1234.
278 278 '''
279 279
280 280 from mercurial.i18n import _
281 281 from mercurial.node import short
282 282 from mercurial import cmdutil, mail, util
283 283 import re, time, urlparse, xmlrpclib
284 284
285 285 # Note for extension authors: ONLY specify testedwith = 'internal' for
286 286 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
287 287 # be specifying the version(s) of Mercurial they are tested with, or
288 288 # leave the attribute unspecified.
289 289 testedwith = 'internal'
290 290
291 291 class bzaccess(object):
292 292 '''Base class for access to Bugzilla.'''
293 293
294 294 def __init__(self, ui):
295 295 self.ui = ui
296 296 usermap = self.ui.config('bugzilla', 'usermap')
297 297 if usermap:
298 298 self.ui.readconfig(usermap, sections=['usermap'])
299 299
300 300 def map_committer(self, user):
301 301 '''map name of committer to Bugzilla user name.'''
302 302 for committer, bzuser in self.ui.configitems('usermap'):
303 303 if committer.lower() == user.lower():
304 304 return bzuser
305 305 return user
306 306
307 307 # Methods to be implemented by access classes.
308 308 #
309 309 # 'bugs' is a dict keyed on bug id, where values are a dict holding
310 310 # updates to bug state. Recognized dict keys are:
311 311 #
312 312 # 'hours': Value, float containing work hours to be updated.
313 313 # 'fix': If key present, bug is to be marked fixed. Value ignored.
314 314
315 315 def filter_real_bug_ids(self, bugs):
316 316 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
317 317 pass
318 318
319 319 def filter_cset_known_bug_ids(self, node, bugs):
320 320 '''remove bug IDs where node occurs in comment text from bugs.'''
321 321 pass
322 322
323 323 def updatebug(self, bugid, newstate, text, committer):
324 324 '''update the specified bug. Add comment text and set new states.
325 325
326 326 If possible add the comment as being from the committer of
327 327 the changeset. Otherwise use the default Bugzilla user.
328 328 '''
329 329 pass
330 330
331 331 def notify(self, bugs, committer):
332 332 '''Force sending of Bugzilla notification emails.
333 333
334 334 Only required if the access method does not trigger notification
335 335 emails automatically.
336 336 '''
337 337 pass
338 338
339 339 # Bugzilla via direct access to MySQL database.
340 340 class bzmysql(bzaccess):
341 341 '''Support for direct MySQL access to Bugzilla.
342 342
343 343 The earliest Bugzilla version this is tested with is version 2.16.
344 344
345 345 If your Bugzilla is version 3.4 or above, you are strongly
346 346 recommended to use the XMLRPC access method instead.
347 347 '''
348 348
349 349 @staticmethod
350 350 def sql_buglist(ids):
351 351 '''return SQL-friendly list of bug ids'''
352 352 return '(' + ','.join(map(str, ids)) + ')'
353 353
354 354 _MySQLdb = None
355 355
356 356 def __init__(self, ui):
357 357 try:
358 358 import MySQLdb as mysql
359 359 bzmysql._MySQLdb = mysql
360 except ImportError, err:
360 except ImportError as err:
361 361 raise util.Abort(_('python mysql support not available: %s') % err)
362 362
363 363 bzaccess.__init__(self, ui)
364 364
365 365 host = self.ui.config('bugzilla', 'host', 'localhost')
366 366 user = self.ui.config('bugzilla', 'user', 'bugs')
367 367 passwd = self.ui.config('bugzilla', 'password')
368 368 db = self.ui.config('bugzilla', 'db', 'bugs')
369 369 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
370 370 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
371 371 (host, db, user, '*' * len(passwd)))
372 372 self.conn = bzmysql._MySQLdb.connect(host=host,
373 373 user=user, passwd=passwd,
374 374 db=db,
375 375 connect_timeout=timeout)
376 376 self.cursor = self.conn.cursor()
377 377 self.longdesc_id = self.get_longdesc_id()
378 378 self.user_ids = {}
379 379 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
380 380
381 381 def run(self, *args, **kwargs):
382 382 '''run a query.'''
383 383 self.ui.note(_('query: %s %s\n') % (args, kwargs))
384 384 try:
385 385 self.cursor.execute(*args, **kwargs)
386 386 except bzmysql._MySQLdb.MySQLError:
387 387 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
388 388 raise
389 389
390 390 def get_longdesc_id(self):
391 391 '''get identity of longdesc field'''
392 392 self.run('select fieldid from fielddefs where name = "longdesc"')
393 393 ids = self.cursor.fetchall()
394 394 if len(ids) != 1:
395 395 raise util.Abort(_('unknown database schema'))
396 396 return ids[0][0]
397 397
398 398 def filter_real_bug_ids(self, bugs):
399 399 '''filter not-existing bugs from set.'''
400 400 self.run('select bug_id from bugs where bug_id in %s' %
401 401 bzmysql.sql_buglist(bugs.keys()))
402 402 existing = [id for (id,) in self.cursor.fetchall()]
403 403 for id in bugs.keys():
404 404 if id not in existing:
405 405 self.ui.status(_('bug %d does not exist\n') % id)
406 406 del bugs[id]
407 407
408 408 def filter_cset_known_bug_ids(self, node, bugs):
409 409 '''filter bug ids that already refer to this changeset from set.'''
410 410 self.run('''select bug_id from longdescs where
411 411 bug_id in %s and thetext like "%%%s%%"''' %
412 412 (bzmysql.sql_buglist(bugs.keys()), short(node)))
413 413 for (id,) in self.cursor.fetchall():
414 414 self.ui.status(_('bug %d already knows about changeset %s\n') %
415 415 (id, short(node)))
416 416 del bugs[id]
417 417
418 418 def notify(self, bugs, committer):
419 419 '''tell bugzilla to send mail.'''
420 420 self.ui.status(_('telling bugzilla to send mail:\n'))
421 421 (user, userid) = self.get_bugzilla_user(committer)
422 422 for id in bugs.keys():
423 423 self.ui.status(_(' bug %s\n') % id)
424 424 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
425 425 bzdir = self.ui.config('bugzilla', 'bzdir',
426 426 '/var/www/html/bugzilla')
427 427 try:
428 428 # Backwards-compatible with old notify string, which
429 429 # took one string. This will throw with a new format
430 430 # string.
431 431 cmd = cmdfmt % id
432 432 except TypeError:
433 433 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
434 434 self.ui.note(_('running notify command %s\n') % cmd)
435 435 fp = util.popen('(%s) 2>&1' % cmd)
436 436 out = fp.read()
437 437 ret = fp.close()
438 438 if ret:
439 439 self.ui.warn(out)
440 440 raise util.Abort(_('bugzilla notify command %s') %
441 441 util.explainexit(ret)[0])
442 442 self.ui.status(_('done\n'))
443 443
444 444 def get_user_id(self, user):
445 445 '''look up numeric bugzilla user id.'''
446 446 try:
447 447 return self.user_ids[user]
448 448 except KeyError:
449 449 try:
450 450 userid = int(user)
451 451 except ValueError:
452 452 self.ui.note(_('looking up user %s\n') % user)
453 453 self.run('''select userid from profiles
454 454 where login_name like %s''', user)
455 455 all = self.cursor.fetchall()
456 456 if len(all) != 1:
457 457 raise KeyError(user)
458 458 userid = int(all[0][0])
459 459 self.user_ids[user] = userid
460 460 return userid
461 461
462 462 def get_bugzilla_user(self, committer):
463 463 '''See if committer is a registered bugzilla user. Return
464 464 bugzilla username and userid if so. If not, return default
465 465 bugzilla username and userid.'''
466 466 user = self.map_committer(committer)
467 467 try:
468 468 userid = self.get_user_id(user)
469 469 except KeyError:
470 470 try:
471 471 defaultuser = self.ui.config('bugzilla', 'bzuser')
472 472 if not defaultuser:
473 473 raise util.Abort(_('cannot find bugzilla user id for %s') %
474 474 user)
475 475 userid = self.get_user_id(defaultuser)
476 476 user = defaultuser
477 477 except KeyError:
478 478 raise util.Abort(_('cannot find bugzilla user id for %s or %s')
479 479 % (user, defaultuser))
480 480 return (user, userid)
481 481
482 482 def updatebug(self, bugid, newstate, text, committer):
483 483 '''update bug state with comment text.
484 484
485 485 Try adding comment as committer of changeset, otherwise as
486 486 default bugzilla user.'''
487 487 if len(newstate) > 0:
488 488 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
489 489
490 490 (user, userid) = self.get_bugzilla_user(committer)
491 491 now = time.strftime('%Y-%m-%d %H:%M:%S')
492 492 self.run('''insert into longdescs
493 493 (bug_id, who, bug_when, thetext)
494 494 values (%s, %s, %s, %s)''',
495 495 (bugid, userid, now, text))
496 496 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
497 497 values (%s, %s, %s, %s)''',
498 498 (bugid, userid, now, self.longdesc_id))
499 499 self.conn.commit()
500 500
501 501 class bzmysql_2_18(bzmysql):
502 502 '''support for bugzilla 2.18 series.'''
503 503
504 504 def __init__(self, ui):
505 505 bzmysql.__init__(self, ui)
506 506 self.default_notify = \
507 507 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
508 508
509 509 class bzmysql_3_0(bzmysql_2_18):
510 510 '''support for bugzilla 3.0 series.'''
511 511
512 512 def __init__(self, ui):
513 513 bzmysql_2_18.__init__(self, ui)
514 514
515 515 def get_longdesc_id(self):
516 516 '''get identity of longdesc field'''
517 517 self.run('select id from fielddefs where name = "longdesc"')
518 518 ids = self.cursor.fetchall()
519 519 if len(ids) != 1:
520 520 raise util.Abort(_('unknown database schema'))
521 521 return ids[0][0]
522 522
523 523 # Bugzilla via XMLRPC interface.
524 524
525 525 class cookietransportrequest(object):
526 526 """A Transport request method that retains cookies over its lifetime.
527 527
528 528 The regular xmlrpclib transports ignore cookies. Which causes
529 529 a bit of a problem when you need a cookie-based login, as with
530 530 the Bugzilla XMLRPC interface prior to 4.4.3.
531 531
532 532 So this is a helper for defining a Transport which looks for
533 533 cookies being set in responses and saves them to add to all future
534 534 requests.
535 535 """
536 536
537 537 # Inspiration drawn from
538 538 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
539 539 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
540 540
541 541 cookies = []
542 542 def send_cookies(self, connection):
543 543 if self.cookies:
544 544 for cookie in self.cookies:
545 545 connection.putheader("Cookie", cookie)
546 546
547 547 def request(self, host, handler, request_body, verbose=0):
548 548 self.verbose = verbose
549 549 self.accept_gzip_encoding = False
550 550
551 551 # issue XML-RPC request
552 552 h = self.make_connection(host)
553 553 if verbose:
554 554 h.set_debuglevel(1)
555 555
556 556 self.send_request(h, handler, request_body)
557 557 self.send_host(h, host)
558 558 self.send_cookies(h)
559 559 self.send_user_agent(h)
560 560 self.send_content(h, request_body)
561 561
562 562 # Deal with differences between Python 2.4-2.6 and 2.7.
563 563 # In the former h is a HTTP(S). In the latter it's a
564 564 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
565 565 # HTTP(S) has an underlying HTTP(S)Connection, so extract
566 566 # that and use it.
567 567 try:
568 568 response = h.getresponse()
569 569 except AttributeError:
570 570 response = h._conn.getresponse()
571 571
572 572 # Add any cookie definitions to our list.
573 573 for header in response.msg.getallmatchingheaders("Set-Cookie"):
574 574 val = header.split(": ", 1)[1]
575 575 cookie = val.split(";", 1)[0]
576 576 self.cookies.append(cookie)
577 577
578 578 if response.status != 200:
579 579 raise xmlrpclib.ProtocolError(host + handler, response.status,
580 580 response.reason, response.msg.headers)
581 581
582 582 payload = response.read()
583 583 parser, unmarshaller = self.getparser()
584 584 parser.feed(payload)
585 585 parser.close()
586 586
587 587 return unmarshaller.close()
588 588
589 589 # The explicit calls to the underlying xmlrpclib __init__() methods are
590 590 # necessary. The xmlrpclib.Transport classes are old-style classes, and
591 591 # it turns out their __init__() doesn't get called when doing multiple
592 592 # inheritance with a new-style class.
593 593 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
594 594 def __init__(self, use_datetime=0):
595 595 if util.safehasattr(xmlrpclib.Transport, "__init__"):
596 596 xmlrpclib.Transport.__init__(self, use_datetime)
597 597
598 598 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
599 599 def __init__(self, use_datetime=0):
600 600 if util.safehasattr(xmlrpclib.Transport, "__init__"):
601 601 xmlrpclib.SafeTransport.__init__(self, use_datetime)
602 602
603 603 class bzxmlrpc(bzaccess):
604 604 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
605 605
606 606 Requires a minimum Bugzilla version 3.4.
607 607 """
608 608
609 609 def __init__(self, ui):
610 610 bzaccess.__init__(self, ui)
611 611
612 612 bzweb = self.ui.config('bugzilla', 'bzurl',
613 613 'http://localhost/bugzilla/')
614 614 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
615 615
616 616 user = self.ui.config('bugzilla', 'user', 'bugs')
617 617 passwd = self.ui.config('bugzilla', 'password')
618 618
619 619 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
620 620 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
621 621 'FIXED')
622 622
623 623 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
624 624 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
625 625 self.bzvermajor = int(ver[0])
626 626 self.bzverminor = int(ver[1])
627 627 login = self.bzproxy.User.login({'login': user, 'password': passwd,
628 628 'restrict_login': True})
629 629 self.bztoken = login.get('token', '')
630 630
631 631 def transport(self, uri):
632 632 if urlparse.urlparse(uri, "http")[0] == "https":
633 633 return cookiesafetransport()
634 634 else:
635 635 return cookietransport()
636 636
637 637 def get_bug_comments(self, id):
638 638 """Return a string with all comment text for a bug."""
639 639 c = self.bzproxy.Bug.comments({'ids': [id],
640 640 'include_fields': ['text'],
641 641 'token': self.bztoken})
642 642 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
643 643
644 644 def filter_real_bug_ids(self, bugs):
645 645 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
646 646 'include_fields': [],
647 647 'permissive': True,
648 648 'token': self.bztoken,
649 649 })
650 650 for badbug in probe['faults']:
651 651 id = badbug['id']
652 652 self.ui.status(_('bug %d does not exist\n') % id)
653 653 del bugs[id]
654 654
655 655 def filter_cset_known_bug_ids(self, node, bugs):
656 656 for id in sorted(bugs.keys()):
657 657 if self.get_bug_comments(id).find(short(node)) != -1:
658 658 self.ui.status(_('bug %d already knows about changeset %s\n') %
659 659 (id, short(node)))
660 660 del bugs[id]
661 661
662 662 def updatebug(self, bugid, newstate, text, committer):
663 663 args = {}
664 664 if 'hours' in newstate:
665 665 args['work_time'] = newstate['hours']
666 666
667 667 if self.bzvermajor >= 4:
668 668 args['ids'] = [bugid]
669 669 args['comment'] = {'body' : text}
670 670 if 'fix' in newstate:
671 671 args['status'] = self.fixstatus
672 672 args['resolution'] = self.fixresolution
673 673 args['token'] = self.bztoken
674 674 self.bzproxy.Bug.update(args)
675 675 else:
676 676 if 'fix' in newstate:
677 677 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
678 678 "to mark bugs fixed\n"))
679 679 args['id'] = bugid
680 680 args['comment'] = text
681 681 self.bzproxy.Bug.add_comment(args)
682 682
683 683 class bzxmlrpcemail(bzxmlrpc):
684 684 """Read data from Bugzilla via XMLRPC, send updates via email.
685 685
686 686 Advantages of sending updates via email:
687 687 1. Comments can be added as any user, not just logged in user.
688 688 2. Bug statuses or other fields not accessible via XMLRPC can
689 689 potentially be updated.
690 690
691 691 There is no XMLRPC function to change bug status before Bugzilla
692 692 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
693 693 But bugs can be marked fixed via email from 3.4 onwards.
694 694 """
695 695
696 696 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
697 697 # in-email fields are specified as '@<fieldname> = <value>'. In
698 698 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
699 699 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
700 700 # compatibility, but rather than rely on this use the new format for
701 701 # 4.0 onwards.
702 702
703 703 def __init__(self, ui):
704 704 bzxmlrpc.__init__(self, ui)
705 705
706 706 self.bzemail = self.ui.config('bugzilla', 'bzemail')
707 707 if not self.bzemail:
708 708 raise util.Abort(_("configuration 'bzemail' missing"))
709 709 mail.validateconfig(self.ui)
710 710
711 711 def makecommandline(self, fieldname, value):
712 712 if self.bzvermajor >= 4:
713 713 return "@%s %s" % (fieldname, str(value))
714 714 else:
715 715 if fieldname == "id":
716 716 fieldname = "bug_id"
717 717 return "@%s = %s" % (fieldname, str(value))
718 718
719 719 def send_bug_modify_email(self, bugid, commands, comment, committer):
720 720 '''send modification message to Bugzilla bug via email.
721 721
722 722 The message format is documented in the Bugzilla email_in.pl
723 723 specification. commands is a list of command lines, comment is the
724 724 comment text.
725 725
726 726 To stop users from crafting commit comments with
727 727 Bugzilla commands, specify the bug ID via the message body, rather
728 728 than the subject line, and leave a blank line after it.
729 729 '''
730 730 user = self.map_committer(committer)
731 731 matches = self.bzproxy.User.get({'match': [user],
732 732 'token': self.bztoken})
733 733 if not matches['users']:
734 734 user = self.ui.config('bugzilla', 'user', 'bugs')
735 735 matches = self.bzproxy.User.get({'match': [user],
736 736 'token': self.bztoken})
737 737 if not matches['users']:
738 738 raise util.Abort(_("default bugzilla user %s email not found") %
739 739 user)
740 740 user = matches['users'][0]['email']
741 741 commands.append(self.makecommandline("id", bugid))
742 742
743 743 text = "\n".join(commands) + "\n\n" + comment
744 744
745 745 _charsets = mail._charsets(self.ui)
746 746 user = mail.addressencode(self.ui, user, _charsets)
747 747 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
748 748 msg = mail.mimeencode(self.ui, text, _charsets)
749 749 msg['From'] = user
750 750 msg['To'] = bzemail
751 751 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
752 752 sendmail = mail.connect(self.ui)
753 753 sendmail(user, bzemail, msg.as_string())
754 754
755 755 def updatebug(self, bugid, newstate, text, committer):
756 756 cmds = []
757 757 if 'hours' in newstate:
758 758 cmds.append(self.makecommandline("work_time", newstate['hours']))
759 759 if 'fix' in newstate:
760 760 cmds.append(self.makecommandline("bug_status", self.fixstatus))
761 761 cmds.append(self.makecommandline("resolution", self.fixresolution))
762 762 self.send_bug_modify_email(bugid, cmds, text, committer)
763 763
764 764 class bugzilla(object):
765 765 # supported versions of bugzilla. different versions have
766 766 # different schemas.
767 767 _versions = {
768 768 '2.16': bzmysql,
769 769 '2.18': bzmysql_2_18,
770 770 '3.0': bzmysql_3_0,
771 771 'xmlrpc': bzxmlrpc,
772 772 'xmlrpc+email': bzxmlrpcemail
773 773 }
774 774
775 775 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
776 776 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
777 777 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
778 778
779 779 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
780 780 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
781 781 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
782 782 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
783 783
784 784 def __init__(self, ui, repo):
785 785 self.ui = ui
786 786 self.repo = repo
787 787
788 788 bzversion = self.ui.config('bugzilla', 'version')
789 789 try:
790 790 bzclass = bugzilla._versions[bzversion]
791 791 except KeyError:
792 792 raise util.Abort(_('bugzilla version %s not supported') %
793 793 bzversion)
794 794 self.bzdriver = bzclass(self.ui)
795 795
796 796 self.bug_re = re.compile(
797 797 self.ui.config('bugzilla', 'regexp',
798 798 bugzilla._default_bug_re), re.IGNORECASE)
799 799 self.fix_re = re.compile(
800 800 self.ui.config('bugzilla', 'fixregexp',
801 801 bugzilla._default_fix_re), re.IGNORECASE)
802 802 self.split_re = re.compile(r'\D+')
803 803
804 804 def find_bugs(self, ctx):
805 805 '''return bugs dictionary created from commit comment.
806 806
807 807 Extract bug info from changeset comments. Filter out any that are
808 808 not known to Bugzilla, and any that already have a reference to
809 809 the given changeset in their comments.
810 810 '''
811 811 start = 0
812 812 hours = 0.0
813 813 bugs = {}
814 814 bugmatch = self.bug_re.search(ctx.description(), start)
815 815 fixmatch = self.fix_re.search(ctx.description(), start)
816 816 while True:
817 817 bugattribs = {}
818 818 if not bugmatch and not fixmatch:
819 819 break
820 820 if not bugmatch:
821 821 m = fixmatch
822 822 elif not fixmatch:
823 823 m = bugmatch
824 824 else:
825 825 if bugmatch.start() < fixmatch.start():
826 826 m = bugmatch
827 827 else:
828 828 m = fixmatch
829 829 start = m.end()
830 830 if m is bugmatch:
831 831 bugmatch = self.bug_re.search(ctx.description(), start)
832 832 if 'fix' in bugattribs:
833 833 del bugattribs['fix']
834 834 else:
835 835 fixmatch = self.fix_re.search(ctx.description(), start)
836 836 bugattribs['fix'] = None
837 837
838 838 try:
839 839 ids = m.group('ids')
840 840 except IndexError:
841 841 ids = m.group(1)
842 842 try:
843 843 hours = float(m.group('hours'))
844 844 bugattribs['hours'] = hours
845 845 except IndexError:
846 846 pass
847 847 except TypeError:
848 848 pass
849 849 except ValueError:
850 850 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
851 851
852 852 for id in self.split_re.split(ids):
853 853 if not id:
854 854 continue
855 855 bugs[int(id)] = bugattribs
856 856 if bugs:
857 857 self.bzdriver.filter_real_bug_ids(bugs)
858 858 if bugs:
859 859 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
860 860 return bugs
861 861
862 862 def update(self, bugid, newstate, ctx):
863 863 '''update bugzilla bug with reference to changeset.'''
864 864
865 865 def webroot(root):
866 866 '''strip leading prefix of repo root and turn into
867 867 url-safe path.'''
868 868 count = int(self.ui.config('bugzilla', 'strip', 0))
869 869 root = util.pconvert(root)
870 870 while count > 0:
871 871 c = root.find('/')
872 872 if c == -1:
873 873 break
874 874 root = root[c + 1:]
875 875 count -= 1
876 876 return root
877 877
878 878 mapfile = self.ui.config('bugzilla', 'style')
879 879 tmpl = self.ui.config('bugzilla', 'template')
880 880 if not mapfile and not tmpl:
881 881 tmpl = _('changeset {node|short} in repo {root} refers '
882 882 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
883 883 t = cmdutil.changeset_templater(self.ui, self.repo,
884 884 False, None, tmpl, mapfile, False)
885 885 self.ui.pushbuffer()
886 886 t.show(ctx, changes=ctx.changeset(),
887 887 bug=str(bugid),
888 888 hgweb=self.ui.config('web', 'baseurl'),
889 889 root=self.repo.root,
890 890 webroot=webroot(self.repo.root))
891 891 data = self.ui.popbuffer()
892 892 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
893 893
894 894 def notify(self, bugs, committer):
895 895 '''ensure Bugzilla users are notified of bug change.'''
896 896 self.bzdriver.notify(bugs, committer)
897 897
898 898 def hook(ui, repo, hooktype, node=None, **kwargs):
899 899 '''add comment to bugzilla for each changeset that refers to a
900 900 bugzilla bug id. only add a comment once per bug, so same change
901 901 seen multiple times does not fill bug with duplicate data.'''
902 902 if node is None:
903 903 raise util.Abort(_('hook type %s does not pass a changeset id') %
904 904 hooktype)
905 905 try:
906 906 bz = bugzilla(ui, repo)
907 907 ctx = repo[node]
908 908 bugs = bz.find_bugs(ctx)
909 909 if bugs:
910 910 for bug in bugs:
911 911 bz.update(bug, bugs[bug], ctx)
912 912 bz.notify(bugs, util.email(ctx.user()))
913 except Exception, e:
913 except Exception as e:
914 914 raise util.Abort(_('Bugzilla error: %s') % e)
@@ -1,165 +1,165 b''
1 1 # Copyright (C) 2015 - Mike Edgar <adgar@google.com>
2 2 #
3 3 # This extension enables removal of file content at a given revision,
4 4 # rewriting the data/metadata of successive revisions to preserve revision log
5 5 # integrity.
6 6
7 7 """erase file content at a given revision
8 8
9 9 The censor command instructs Mercurial to erase all content of a file at a given
10 10 revision *without updating the changeset hash.* This allows existing history to
11 11 remain valid while preventing future clones/pulls from receiving the erased
12 12 data.
13 13
14 14 Typical uses for censor are due to security or legal requirements, including::
15 15
16 16 * Passwords, private keys, crytographic material
17 17 * Licensed data/code/libraries for which the license has expired
18 18 * Personally Identifiable Information or other private data
19 19
20 20 Censored nodes can interrupt mercurial's typical operation whenever the excised
21 21 data needs to be materialized. Some commands, like ``hg cat``/``hg revert``,
22 22 simply fail when asked to produce censored data. Others, like ``hg verify`` and
23 23 ``hg update``, must be capable of tolerating censored data to continue to
24 24 function in a meaningful way. Such commands only tolerate censored file
25 25 revisions if they are allowed by the "censor.policy=ignore" config option.
26 26 """
27 27
28 28 from mercurial.node import short
29 29 from mercurial import cmdutil, error, filelog, revlog, scmutil, util
30 30 from mercurial.i18n import _
31 31
32 32 cmdtable = {}
33 33 command = cmdutil.command(cmdtable)
34 34 # Note for extension authors: ONLY specify testedwith = 'internal' for
35 35 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 36 # be specifying the version(s) of Mercurial they are tested with, or
37 37 # leave the attribute unspecified.
38 38 testedwith = 'internal'
39 39
40 40 @command('censor',
41 41 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
42 42 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
43 43 _('-r REV [-t TEXT] [FILE]'))
44 44 def censor(ui, repo, path, rev='', tombstone='', **opts):
45 45 if not path:
46 46 raise util.Abort(_('must specify file path to censor'))
47 47 if not rev:
48 48 raise util.Abort(_('must specify revision to censor'))
49 49
50 50 flog = repo.file(path)
51 51 if not len(flog):
52 52 raise util.Abort(_('cannot censor file with no history'))
53 53
54 54 rev = scmutil.revsingle(repo, rev, rev).rev()
55 55 try:
56 56 ctx = repo[rev]
57 57 except KeyError:
58 58 raise util.Abort(_('invalid revision identifier %s') % rev)
59 59
60 60 try:
61 61 fctx = ctx.filectx(path)
62 62 except error.LookupError:
63 63 raise util.Abort(_('file does not exist at revision %s') % rev)
64 64
65 65 fnode = fctx.filenode()
66 66 headctxs = [repo[c] for c in repo.heads()]
67 67 heads = [c for c in headctxs if path in c and c.filenode(path) == fnode]
68 68 if heads:
69 69 headlist = ', '.join([short(c.node()) for c in heads])
70 70 raise util.Abort(_('cannot censor file in heads (%s)') % headlist,
71 71 hint=_('clean/delete and commit first'))
72 72
73 73 wctx = repo[None]
74 74 wp = wctx.parents()
75 75 if ctx.node() in [p.node() for p in wp]:
76 76 raise util.Abort(_('cannot censor working directory'),
77 77 hint=_('clean/delete/update first'))
78 78
79 79 flogv = flog.version & 0xFFFF
80 80 if flogv != revlog.REVLOGNG:
81 81 raise util.Abort(
82 82 _('censor does not support revlog version %d') % (flogv,))
83 83
84 84 tombstone = filelog.packmeta({"censored": tombstone}, "")
85 85
86 86 crev = fctx.filerev()
87 87
88 88 if len(tombstone) > flog.rawsize(crev):
89 89 raise util.Abort(_(
90 90 'censor tombstone must be no longer than censored data'))
91 91
92 92 # Using two files instead of one makes it easy to rewrite entry-by-entry
93 93 idxread = repo.svfs(flog.indexfile, 'r')
94 94 idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True)
95 95 if flog.version & revlog.REVLOGNGINLINEDATA:
96 96 dataread, datawrite = idxread, idxwrite
97 97 else:
98 98 dataread = repo.svfs(flog.datafile, 'r')
99 99 datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True)
100 100
101 101 # Copy all revlog data up to the entry to be censored.
102 102 rio = revlog.revlogio()
103 103 offset = flog.start(crev)
104 104
105 105 for chunk in util.filechunkiter(idxread, limit=crev * rio.size):
106 106 idxwrite.write(chunk)
107 107 for chunk in util.filechunkiter(dataread, limit=offset):
108 108 datawrite.write(chunk)
109 109
110 110 def rewriteindex(r, newoffs, newdata=None):
111 111 """Rewrite the index entry with a new data offset and optional new data.
112 112
113 113 The newdata argument, if given, is a tuple of three positive integers:
114 114 (new compressed, new uncompressed, added flag bits).
115 115 """
116 116 offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r]
117 117 flags = revlog.gettype(offlags)
118 118 if newdata:
119 119 comp, uncomp, nflags = newdata
120 120 flags |= nflags
121 121 offlags = revlog.offset_type(newoffs, flags)
122 122 e = (offlags, comp, uncomp, r, link, p1, p2, nodeid)
123 123 idxwrite.write(rio.packentry(e, None, flog.version, r))
124 124 idxread.seek(rio.size, 1)
125 125
126 126 def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS):
127 127 """Write the given full text to the filelog with the given data offset.
128 128
129 129 Returns:
130 130 The integer number of data bytes written, for tracking data offsets.
131 131 """
132 132 flag, compdata = flog.compress(data)
133 133 newcomp = len(flag) + len(compdata)
134 134 rewriteindex(r, offs, (newcomp, len(data), nflags))
135 135 datawrite.write(flag)
136 136 datawrite.write(compdata)
137 137 dataread.seek(flog.length(r), 1)
138 138 return newcomp
139 139
140 140 # Rewrite censored revlog entry with (padded) tombstone data.
141 141 pad = ' ' * (flog.rawsize(crev) - len(tombstone))
142 142 offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED)
143 143
144 144 # Rewrite all following filelog revisions fixing up offsets and deltas.
145 145 for srev in xrange(crev + 1, len(flog)):
146 146 if crev in flog.parentrevs(srev):
147 147 # Immediate children of censored node must be re-added as fulltext.
148 148 try:
149 149 revdata = flog.revision(srev)
150 except error.CensoredNodeError, e:
150 except error.CensoredNodeError as e:
151 151 revdata = e.tombstone
152 152 dlen = rewrite(srev, offset, revdata)
153 153 else:
154 154 # Copy any other revision data verbatim after fixing up the offset.
155 155 rewriteindex(srev, offset)
156 156 dlen = flog.length(srev)
157 157 for chunk in util.filechunkiter(dataread, limit=dlen):
158 158 datawrite.write(chunk)
159 159 offset += dlen
160 160
161 161 idxread.close()
162 162 idxwrite.close()
163 163 if dataread is not idxread:
164 164 dataread.close()
165 165 datawrite.close()
@@ -1,205 +1,205 b''
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''command to display statistics about repository history'''
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import patch, cmdutil, scmutil, util, commands
13 13 from mercurial import encoding
14 14 import os
15 15 import time, datetime
16 16
17 17 cmdtable = {}
18 18 command = cmdutil.command(cmdtable)
19 19 # Note for extension authors: ONLY specify testedwith = 'internal' for
20 20 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
21 21 # be specifying the version(s) of Mercurial they are tested with, or
22 22 # leave the attribute unspecified.
23 23 testedwith = 'internal'
24 24
25 25 def maketemplater(ui, repo, tmpl):
26 26 try:
27 27 t = cmdutil.changeset_templater(ui, repo, False, None, tmpl,
28 28 None, False)
29 except SyntaxError, inst:
29 except SyntaxError as inst:
30 30 raise util.Abort(inst.args[0])
31 31 return t
32 32
33 33 def changedlines(ui, repo, ctx1, ctx2, fns):
34 34 added, removed = 0, 0
35 35 fmatch = scmutil.matchfiles(repo, fns)
36 36 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
37 37 for l in diff.split('\n'):
38 38 if l.startswith("+") and not l.startswith("+++ "):
39 39 added += 1
40 40 elif l.startswith("-") and not l.startswith("--- "):
41 41 removed += 1
42 42 return (added, removed)
43 43
44 44 def countrate(ui, repo, amap, *pats, **opts):
45 45 """Calculate stats"""
46 46 if opts.get('dateformat'):
47 47 def getkey(ctx):
48 48 t, tz = ctx.date()
49 49 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
50 50 return date.strftime(opts['dateformat'])
51 51 else:
52 52 tmpl = opts.get('oldtemplate') or opts.get('template')
53 53 tmpl = maketemplater(ui, repo, tmpl)
54 54 def getkey(ctx):
55 55 ui.pushbuffer()
56 56 tmpl.show(ctx)
57 57 return ui.popbuffer()
58 58
59 59 state = {'count': 0}
60 60 rate = {}
61 61 df = False
62 62 if opts.get('date'):
63 63 df = util.matchdate(opts['date'])
64 64
65 65 m = scmutil.match(repo[None], pats, opts)
66 66 def prep(ctx, fns):
67 67 rev = ctx.rev()
68 68 if df and not df(ctx.date()[0]): # doesn't match date format
69 69 return
70 70
71 71 key = getkey(ctx).strip()
72 72 key = amap.get(key, key) # alias remap
73 73 if opts.get('changesets'):
74 74 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
75 75 else:
76 76 parents = ctx.parents()
77 77 if len(parents) > 1:
78 78 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
79 79 return
80 80
81 81 ctx1 = parents[0]
82 82 lines = changedlines(ui, repo, ctx1, ctx, fns)
83 83 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
84 84
85 85 state['count'] += 1
86 86 ui.progress(_('analyzing'), state['count'], total=len(repo))
87 87
88 88 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
89 89 continue
90 90
91 91 ui.progress(_('analyzing'), None)
92 92
93 93 return rate
94 94
95 95
96 96 @command('churn',
97 97 [('r', 'rev', [],
98 98 _('count rate for the specified revision or revset'), _('REV')),
99 99 ('d', 'date', '',
100 100 _('count rate for revisions matching date spec'), _('DATE')),
101 101 ('t', 'oldtemplate', '',
102 102 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
103 103 ('T', 'template', '{author|email}',
104 104 _('template to group changesets'), _('TEMPLATE')),
105 105 ('f', 'dateformat', '',
106 106 _('strftime-compatible format for grouping by date'), _('FORMAT')),
107 107 ('c', 'changesets', False, _('count rate by number of changesets')),
108 108 ('s', 'sort', False, _('sort by key (default: sort by count)')),
109 109 ('', 'diffstat', False, _('display added/removed lines separately')),
110 110 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
111 111 ] + commands.walkopts,
112 112 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
113 113 inferrepo=True)
114 114 def churn(ui, repo, *pats, **opts):
115 115 '''histogram of changes to the repository
116 116
117 117 This command will display a histogram representing the number
118 118 of changed lines or revisions, grouped according to the given
119 119 template. The default template will group changes by author.
120 120 The --dateformat option may be used to group the results by
121 121 date instead.
122 122
123 123 Statistics are based on the number of changed lines, or
124 124 alternatively the number of matching revisions if the
125 125 --changesets option is specified.
126 126
127 127 Examples::
128 128
129 129 # display count of changed lines for every committer
130 130 hg churn -t "{author|email}"
131 131
132 132 # display daily activity graph
133 133 hg churn -f "%H" -s -c
134 134
135 135 # display activity of developers by month
136 136 hg churn -f "%Y-%m" -s -c
137 137
138 138 # display count of lines changed in every year
139 139 hg churn -f "%Y" -s
140 140
141 141 It is possible to map alternate email addresses to a main address
142 142 by providing a file using the following format::
143 143
144 144 <alias email> = <actual email>
145 145
146 146 Such a file may be specified with the --aliases option, otherwise
147 147 a .hgchurn file will be looked for in the working directory root.
148 148 Aliases will be split from the rightmost "=".
149 149 '''
150 150 def pad(s, l):
151 151 return s + " " * (l - encoding.colwidth(s))
152 152
153 153 amap = {}
154 154 aliases = opts.get('aliases')
155 155 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
156 156 aliases = repo.wjoin('.hgchurn')
157 157 if aliases:
158 158 for l in open(aliases, "r"):
159 159 try:
160 160 alias, actual = l.rsplit('=' in l and '=' or None, 1)
161 161 amap[alias.strip()] = actual.strip()
162 162 except ValueError:
163 163 l = l.strip()
164 164 if l:
165 165 ui.warn(_("skipping malformed alias: %s\n") % l)
166 166 continue
167 167
168 168 rate = countrate(ui, repo, amap, *pats, **opts).items()
169 169 if not rate:
170 170 return
171 171
172 172 if opts.get('sort'):
173 173 rate.sort()
174 174 else:
175 175 rate.sort(key=lambda x: (-sum(x[1]), x))
176 176
177 177 # Be careful not to have a zero maxcount (issue833)
178 178 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
179 179 maxname = max(len(k) for k, v in rate)
180 180
181 181 ttywidth = ui.termwidth()
182 182 ui.debug("assuming %i character terminal\n" % ttywidth)
183 183 width = ttywidth - maxname - 2 - 2 - 2
184 184
185 185 if opts.get('diffstat'):
186 186 width -= 15
187 187 def format(name, diffstat):
188 188 added, removed = diffstat
189 189 return "%s %15s %s%s\n" % (pad(name, maxname),
190 190 '+%d/-%d' % (added, removed),
191 191 ui.label('+' * charnum(added),
192 192 'diffstat.inserted'),
193 193 ui.label('-' * charnum(removed),
194 194 'diffstat.deleted'))
195 195 else:
196 196 width -= 6
197 197 def format(name, count):
198 198 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
199 199 '*' * charnum(sum(count)))
200 200
201 201 def charnum(count):
202 202 return int(round(count * width / maxcount))
203 203
204 204 for name, count in rate:
205 205 ui.write(format(name, count))
@@ -1,688 +1,688 b''
1 1 # color.py color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''colorize output from some commands
9 9
10 10 The color extension colorizes output from several Mercurial commands.
11 11 For example, the diff command shows additions in green and deletions
12 12 in red, while the status command shows modified files in magenta. Many
13 13 other commands have analogous colors. It is possible to customize
14 14 these colors.
15 15
16 16 Effects
17 17 -------
18 18
19 19 Other effects in addition to color, like bold and underlined text, are
20 20 also available. By default, the terminfo database is used to find the
21 21 terminal codes used to change color and effect. If terminfo is not
22 22 available, then effects are rendered with the ECMA-48 SGR control
23 23 function (aka ANSI escape codes).
24 24
25 25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 28 'underline'. How each is rendered depends on the terminal emulator.
29 29 Some may not be available for a given terminal type, and will be
30 30 silently ignored.
31 31
32 32 Labels
33 33 ------
34 34
35 35 Text receives color effects depending on the labels that it has. Many
36 36 default Mercurial commands emit labelled text. You can also define
37 37 your own labels in templates using the label function, see :hg:`help
38 38 templates`. A single portion of text may have more than one label. In
39 39 that case, effects given to the last label will override any other
40 40 effects. This includes the special "none" effect, which nullifies
41 41 other effects.
42 42
43 43 Labels are normally invisible. In order to see these labels and their
44 44 position in the text, use the global --color=debug option. The same
45 45 anchor text may be associated to multiple labels, e.g.
46 46
47 47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48 48
49 49 The following are the default effects for some default labels. Default
50 50 effects may be overridden from your configuration file::
51 51
52 52 [color]
53 53 status.modified = blue bold underline red_background
54 54 status.added = green bold
55 55 status.removed = red bold blue_background
56 56 status.deleted = cyan bold underline
57 57 status.unknown = magenta bold underline
58 58 status.ignored = black bold
59 59
60 60 # 'none' turns off all effects
61 61 status.clean = none
62 62 status.copied = none
63 63
64 64 qseries.applied = blue bold underline
65 65 qseries.unapplied = black bold
66 66 qseries.missing = red bold
67 67
68 68 diff.diffline = bold
69 69 diff.extended = cyan bold
70 70 diff.file_a = red bold
71 71 diff.file_b = green bold
72 72 diff.hunk = magenta
73 73 diff.deleted = red
74 74 diff.inserted = green
75 75 diff.changed = white
76 76 diff.tab =
77 77 diff.trailingwhitespace = bold red_background
78 78
79 79 # Blank so it inherits the style of the surrounding label
80 80 changeset.public =
81 81 changeset.draft =
82 82 changeset.secret =
83 83
84 84 resolve.unresolved = red bold
85 85 resolve.resolved = green bold
86 86
87 87 bookmarks.active = green
88 88
89 89 branches.active = none
90 90 branches.closed = black bold
91 91 branches.current = green
92 92 branches.inactive = none
93 93
94 94 tags.normal = green
95 95 tags.local = black bold
96 96
97 97 rebase.rebased = blue
98 98 rebase.remaining = red bold
99 99
100 100 shelve.age = cyan
101 101 shelve.newest = green bold
102 102 shelve.name = blue bold
103 103
104 104 histedit.remaining = red bold
105 105
106 106 Custom colors
107 107 -------------
108 108
109 109 Because there are only eight standard colors, this module allows you
110 110 to define color names for other color slots which might be available
111 111 for your terminal type, assuming terminfo mode. For instance::
112 112
113 113 color.brightblue = 12
114 114 color.pink = 207
115 115 color.orange = 202
116 116
117 117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 118 that have brighter colors defined in the upper eight) and, 'pink' and
119 119 'orange' to colors in 256-color xterm's default color cube. These
120 120 defined colors may then be used as any of the pre-defined eight,
121 121 including appending '_background' to set the background to that color.
122 122
123 123 Modes
124 124 -----
125 125
126 126 By default, the color extension will use ANSI mode (or win32 mode on
127 127 Windows) if it detects a terminal. To override auto mode (to enable
128 128 terminfo mode, for example), set the following configuration option::
129 129
130 130 [color]
131 131 mode = terminfo
132 132
133 133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 134 disable color.
135 135
136 136 Note that on some systems, terminfo mode may cause problems when using
137 137 color with the pager extension and less -R. less with the -R option
138 138 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 139 emit codes that less doesn't understand. You can work around this by
140 140 either using ansi mode (or auto mode), or by using less -r (which will
141 141 pass through all terminal control codes, not just color control
142 142 codes).
143 143
144 144 On some systems (such as MSYS in Windows), the terminal may support
145 145 a different color mode than the pager (activated via the "pager"
146 146 extension). It is possible to define separate modes depending on whether
147 147 the pager is active::
148 148
149 149 [color]
150 150 mode = auto
151 151 pagermode = ansi
152 152
153 153 If ``pagermode`` is not defined, the ``mode`` will be used.
154 154 '''
155 155
156 156 import os
157 157
158 158 from mercurial import cmdutil, commands, dispatch, extensions, subrepo, util
159 159 from mercurial import ui as uimod
160 160 from mercurial import templater, error
161 161 from mercurial.i18n import _
162 162
163 163 cmdtable = {}
164 164 command = cmdutil.command(cmdtable)
165 165 # Note for extension authors: ONLY specify testedwith = 'internal' for
166 166 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
167 167 # be specifying the version(s) of Mercurial they are tested with, or
168 168 # leave the attribute unspecified.
169 169 testedwith = 'internal'
170 170
171 171 # start and stop parameters for effects
172 172 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
173 173 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
174 174 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
175 175 'black_background': 40, 'red_background': 41,
176 176 'green_background': 42, 'yellow_background': 43,
177 177 'blue_background': 44, 'purple_background': 45,
178 178 'cyan_background': 46, 'white_background': 47}
179 179
180 180 def _terminfosetup(ui, mode):
181 181 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
182 182
183 183 global _terminfo_params
184 184 # If we failed to load curses, we go ahead and return.
185 185 if not _terminfo_params:
186 186 return
187 187 # Otherwise, see what the config file says.
188 188 if mode not in ('auto', 'terminfo'):
189 189 return
190 190
191 191 _terminfo_params.update((key[6:], (False, int(val)))
192 192 for key, val in ui.configitems('color')
193 193 if key.startswith('color.'))
194 194
195 195 try:
196 196 curses.setupterm()
197 except curses.error, e:
197 except curses.error as e:
198 198 _terminfo_params = {}
199 199 return
200 200
201 201 for key, (b, e) in _terminfo_params.items():
202 202 if not b:
203 203 continue
204 204 if not curses.tigetstr(e):
205 205 # Most terminals don't support dim, invis, etc, so don't be
206 206 # noisy and use ui.debug().
207 207 ui.debug("no terminfo entry for %s\n" % e)
208 208 del _terminfo_params[key]
209 209 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
210 210 # Only warn about missing terminfo entries if we explicitly asked for
211 211 # terminfo mode.
212 212 if mode == "terminfo":
213 213 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
214 214 "ECMA-48 color\n"))
215 215 _terminfo_params = {}
216 216
217 217 def _modesetup(ui, coloropt):
218 218 global _terminfo_params
219 219
220 220 if coloropt == 'debug':
221 221 return 'debug'
222 222
223 223 auto = (coloropt == 'auto')
224 224 always = not auto and util.parsebool(coloropt)
225 225 if not always and not auto:
226 226 return None
227 227
228 228 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
229 229
230 230 mode = ui.config('color', 'mode', 'auto')
231 231
232 232 # If pager is active, color.pagermode overrides color.mode.
233 233 if getattr(ui, 'pageractive', False):
234 234 mode = ui.config('color', 'pagermode', mode)
235 235
236 236 realmode = mode
237 237 if mode == 'auto':
238 238 if os.name == 'nt':
239 239 term = os.environ.get('TERM')
240 240 # TERM won't be defined in a vanilla cmd.exe environment.
241 241
242 242 # UNIX-like environments on Windows such as Cygwin and MSYS will
243 243 # set TERM. They appear to make a best effort attempt at setting it
244 244 # to something appropriate. However, not all environments with TERM
245 245 # defined support ANSI. Since "ansi" could result in terminal
246 246 # gibberish, we error on the side of selecting "win32". However, if
247 247 # w32effects is not defined, we almost certainly don't support
248 248 # "win32", so don't even try.
249 249 if (term and 'xterm' in term) or not w32effects:
250 250 realmode = 'ansi'
251 251 else:
252 252 realmode = 'win32'
253 253 else:
254 254 realmode = 'ansi'
255 255
256 256 def modewarn():
257 257 # only warn if color.mode was explicitly set and we're in
258 258 # an interactive terminal
259 259 if mode == realmode and ui.interactive():
260 260 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
261 261
262 262 if realmode == 'win32':
263 263 _terminfo_params = {}
264 264 if not w32effects:
265 265 modewarn()
266 266 return None
267 267 _effects.update(w32effects)
268 268 elif realmode == 'ansi':
269 269 _terminfo_params = {}
270 270 elif realmode == 'terminfo':
271 271 _terminfosetup(ui, mode)
272 272 if not _terminfo_params:
273 273 ## FIXME Shouldn't we return None in this case too?
274 274 modewarn()
275 275 realmode = 'ansi'
276 276 else:
277 277 return None
278 278
279 279 if always or (auto and formatted):
280 280 return realmode
281 281 return None
282 282
283 283 try:
284 284 import curses
285 285 # Mapping from effect name to terminfo attribute name or color number.
286 286 # This will also force-load the curses module.
287 287 _terminfo_params = {'none': (True, 'sgr0'),
288 288 'standout': (True, 'smso'),
289 289 'underline': (True, 'smul'),
290 290 'reverse': (True, 'rev'),
291 291 'inverse': (True, 'rev'),
292 292 'blink': (True, 'blink'),
293 293 'dim': (True, 'dim'),
294 294 'bold': (True, 'bold'),
295 295 'invisible': (True, 'invis'),
296 296 'italic': (True, 'sitm'),
297 297 'black': (False, curses.COLOR_BLACK),
298 298 'red': (False, curses.COLOR_RED),
299 299 'green': (False, curses.COLOR_GREEN),
300 300 'yellow': (False, curses.COLOR_YELLOW),
301 301 'blue': (False, curses.COLOR_BLUE),
302 302 'magenta': (False, curses.COLOR_MAGENTA),
303 303 'cyan': (False, curses.COLOR_CYAN),
304 304 'white': (False, curses.COLOR_WHITE)}
305 305 except ImportError:
306 306 _terminfo_params = {}
307 307
308 308 _styles = {'grep.match': 'red bold',
309 309 'grep.linenumber': 'green',
310 310 'grep.rev': 'green',
311 311 'grep.change': 'green',
312 312 'grep.sep': 'cyan',
313 313 'grep.filename': 'magenta',
314 314 'grep.user': 'magenta',
315 315 'grep.date': 'magenta',
316 316 'bookmarks.active': 'green',
317 317 'branches.active': 'none',
318 318 'branches.closed': 'black bold',
319 319 'branches.current': 'green',
320 320 'branches.inactive': 'none',
321 321 'diff.changed': 'white',
322 322 'diff.deleted': 'red',
323 323 'diff.diffline': 'bold',
324 324 'diff.extended': 'cyan bold',
325 325 'diff.file_a': 'red bold',
326 326 'diff.file_b': 'green bold',
327 327 'diff.hunk': 'magenta',
328 328 'diff.inserted': 'green',
329 329 'diff.tab': '',
330 330 'diff.trailingwhitespace': 'bold red_background',
331 331 'changeset.public' : '',
332 332 'changeset.draft' : '',
333 333 'changeset.secret' : '',
334 334 'diffstat.deleted': 'red',
335 335 'diffstat.inserted': 'green',
336 336 'histedit.remaining': 'red bold',
337 337 'ui.prompt': 'yellow',
338 338 'log.changeset': 'yellow',
339 339 'patchbomb.finalsummary': '',
340 340 'patchbomb.from': 'magenta',
341 341 'patchbomb.to': 'cyan',
342 342 'patchbomb.subject': 'green',
343 343 'patchbomb.diffstats': '',
344 344 'rebase.rebased': 'blue',
345 345 'rebase.remaining': 'red bold',
346 346 'resolve.resolved': 'green bold',
347 347 'resolve.unresolved': 'red bold',
348 348 'shelve.age': 'cyan',
349 349 'shelve.newest': 'green bold',
350 350 'shelve.name': 'blue bold',
351 351 'status.added': 'green bold',
352 352 'status.clean': 'none',
353 353 'status.copied': 'none',
354 354 'status.deleted': 'cyan bold underline',
355 355 'status.ignored': 'black bold',
356 356 'status.modified': 'blue bold',
357 357 'status.removed': 'red bold',
358 358 'status.unknown': 'magenta bold underline',
359 359 'tags.normal': 'green',
360 360 'tags.local': 'black bold'}
361 361
362 362
363 363 def _effect_str(effect):
364 364 '''Helper function for render_effects().'''
365 365
366 366 bg = False
367 367 if effect.endswith('_background'):
368 368 bg = True
369 369 effect = effect[:-11]
370 370 attr, val = _terminfo_params[effect]
371 371 if attr:
372 372 return curses.tigetstr(val)
373 373 elif bg:
374 374 return curses.tparm(curses.tigetstr('setab'), val)
375 375 else:
376 376 return curses.tparm(curses.tigetstr('setaf'), val)
377 377
378 378 def render_effects(text, effects):
379 379 'Wrap text in commands to turn on each effect.'
380 380 if not text:
381 381 return text
382 382 if not _terminfo_params:
383 383 start = [str(_effects[e]) for e in ['none'] + effects.split()]
384 384 start = '\033[' + ';'.join(start) + 'm'
385 385 stop = '\033[' + str(_effects['none']) + 'm'
386 386 else:
387 387 start = ''.join(_effect_str(effect)
388 388 for effect in ['none'] + effects.split())
389 389 stop = _effect_str('none')
390 390 return ''.join([start, text, stop])
391 391
392 392 def extstyles():
393 393 for name, ext in extensions.extensions():
394 394 _styles.update(getattr(ext, 'colortable', {}))
395 395
396 396 def valideffect(effect):
397 397 'Determine if the effect is valid or not.'
398 398 good = False
399 399 if not _terminfo_params and effect in _effects:
400 400 good = True
401 401 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
402 402 good = True
403 403 return good
404 404
405 405 def configstyles(ui):
406 406 for status, cfgeffects in ui.configitems('color'):
407 407 if '.' not in status or status.startswith('color.'):
408 408 continue
409 409 cfgeffects = ui.configlist('color', status)
410 410 if cfgeffects:
411 411 good = []
412 412 for e in cfgeffects:
413 413 if valideffect(e):
414 414 good.append(e)
415 415 else:
416 416 ui.warn(_("ignoring unknown color/effect %r "
417 417 "(configured in color.%s)\n")
418 418 % (e, status))
419 419 _styles[status] = ' '.join(good)
420 420
421 421 class colorui(uimod.ui):
422 422 def popbuffer(self, labeled=False):
423 423 if self._colormode is None:
424 424 return super(colorui, self).popbuffer(labeled)
425 425
426 426 self._bufferstates.pop()
427 427 if labeled:
428 428 return ''.join(self.label(a, label) for a, label
429 429 in self._buffers.pop())
430 430 return ''.join(a for a, label in self._buffers.pop())
431 431
432 432 _colormode = 'ansi'
433 433 def write(self, *args, **opts):
434 434 if self._colormode is None:
435 435 return super(colorui, self).write(*args, **opts)
436 436
437 437 label = opts.get('label', '')
438 438 if self._buffers:
439 439 self._buffers[-1].extend([(str(a), label) for a in args])
440 440 elif self._colormode == 'win32':
441 441 for a in args:
442 442 win32print(a, super(colorui, self).write, **opts)
443 443 else:
444 444 return super(colorui, self).write(
445 445 *[self.label(str(a), label) for a in args], **opts)
446 446
447 447 def write_err(self, *args, **opts):
448 448 if self._colormode is None:
449 449 return super(colorui, self).write_err(*args, **opts)
450 450
451 451 label = opts.get('label', '')
452 452 if self._bufferstates and self._bufferstates[-1][0]:
453 453 return self.write(*args, **opts)
454 454 if self._colormode == 'win32':
455 455 for a in args:
456 456 win32print(a, super(colorui, self).write_err, **opts)
457 457 else:
458 458 return super(colorui, self).write_err(
459 459 *[self.label(str(a), label) for a in args], **opts)
460 460
461 461 def showlabel(self, msg, label):
462 462 if label and msg:
463 463 if msg[-1] == '\n':
464 464 return "[%s|%s]\n" % (label, msg[:-1])
465 465 else:
466 466 return "[%s|%s]" % (label, msg)
467 467 else:
468 468 return msg
469 469
470 470 def label(self, msg, label):
471 471 if self._colormode is None:
472 472 return super(colorui, self).label(msg, label)
473 473
474 474 if self._colormode == 'debug':
475 475 return self.showlabel(msg, label)
476 476
477 477 effects = []
478 478 for l in label.split():
479 479 s = _styles.get(l, '')
480 480 if s:
481 481 effects.append(s)
482 482 elif valideffect(l):
483 483 effects.append(l)
484 484 effects = ' '.join(effects)
485 485 if effects:
486 486 return '\n'.join([render_effects(s, effects)
487 487 for s in msg.split('\n')])
488 488 return msg
489 489
490 490 def templatelabel(context, mapping, args):
491 491 if len(args) != 2:
492 492 # i18n: "label" is a keyword
493 493 raise error.ParseError(_("label expects two arguments"))
494 494
495 495 # add known effects to the mapping so symbols like 'red', 'bold',
496 496 # etc. don't need to be quoted
497 497 mapping.update(dict([(k, k) for k in _effects]))
498 498
499 499 thing = args[1][0](context, mapping, args[1][1])
500 500
501 501 # apparently, repo could be a string that is the favicon?
502 502 repo = mapping.get('repo', '')
503 503 if isinstance(repo, str):
504 504 return thing
505 505
506 506 label = args[0][0](context, mapping, args[0][1])
507 507
508 508 thing = templater.stringify(thing)
509 509 label = templater.stringify(label)
510 510
511 511 return repo.ui.label(thing, label)
512 512
513 513 def uisetup(ui):
514 514 if ui.plain():
515 515 return
516 516 if not isinstance(ui, colorui):
517 517 colorui.__bases__ = (ui.__class__,)
518 518 ui.__class__ = colorui
519 519 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
520 520 mode = _modesetup(ui_, opts['color'])
521 521 colorui._colormode = mode
522 522 if mode and mode != 'debug':
523 523 extstyles()
524 524 configstyles(ui_)
525 525 return orig(ui_, opts, cmd, cmdfunc)
526 526 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
527 527 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
528 528 # insert the argument in the front,
529 529 # the end of git diff arguments is used for paths
530 530 commands.insert(1, '--color')
531 531 return orig(gitsub, commands, env, stream, cwd)
532 532 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
533 533 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
534 534 templatelabel.__doc__ = templater.funcs['label'].__doc__
535 535 templater.funcs['label'] = templatelabel
536 536
537 537 def extsetup(ui):
538 538 commands.globalopts.append(
539 539 ('', 'color', 'auto',
540 540 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
541 541 # and should not be translated
542 542 _("when to colorize (boolean, always, auto, never, or debug)"),
543 543 _('TYPE')))
544 544
545 545 @command('debugcolor', [], 'hg debugcolor')
546 546 def debugcolor(ui, repo, **opts):
547 547 global _styles
548 548 _styles = {}
549 549 for effect in _effects.keys():
550 550 _styles[effect] = effect
551 551 ui.write(('color mode: %s\n') % ui._colormode)
552 552 ui.write(_('available colors:\n'))
553 553 for label, colors in _styles.items():
554 554 ui.write(('%s\n') % colors, label=label)
555 555
556 556 if os.name != 'nt':
557 557 w32effects = None
558 558 else:
559 559 import re, ctypes
560 560
561 561 _kernel32 = ctypes.windll.kernel32
562 562
563 563 _WORD = ctypes.c_ushort
564 564
565 565 _INVALID_HANDLE_VALUE = -1
566 566
567 567 class _COORD(ctypes.Structure):
568 568 _fields_ = [('X', ctypes.c_short),
569 569 ('Y', ctypes.c_short)]
570 570
571 571 class _SMALL_RECT(ctypes.Structure):
572 572 _fields_ = [('Left', ctypes.c_short),
573 573 ('Top', ctypes.c_short),
574 574 ('Right', ctypes.c_short),
575 575 ('Bottom', ctypes.c_short)]
576 576
577 577 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
578 578 _fields_ = [('dwSize', _COORD),
579 579 ('dwCursorPosition', _COORD),
580 580 ('wAttributes', _WORD),
581 581 ('srWindow', _SMALL_RECT),
582 582 ('dwMaximumWindowSize', _COORD)]
583 583
584 584 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
585 585 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
586 586
587 587 _FOREGROUND_BLUE = 0x0001
588 588 _FOREGROUND_GREEN = 0x0002
589 589 _FOREGROUND_RED = 0x0004
590 590 _FOREGROUND_INTENSITY = 0x0008
591 591
592 592 _BACKGROUND_BLUE = 0x0010
593 593 _BACKGROUND_GREEN = 0x0020
594 594 _BACKGROUND_RED = 0x0040
595 595 _BACKGROUND_INTENSITY = 0x0080
596 596
597 597 _COMMON_LVB_REVERSE_VIDEO = 0x4000
598 598 _COMMON_LVB_UNDERSCORE = 0x8000
599 599
600 600 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
601 601 w32effects = {
602 602 'none': -1,
603 603 'black': 0,
604 604 'red': _FOREGROUND_RED,
605 605 'green': _FOREGROUND_GREEN,
606 606 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
607 607 'blue': _FOREGROUND_BLUE,
608 608 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
609 609 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
610 610 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
611 611 'bold': _FOREGROUND_INTENSITY,
612 612 'black_background': 0x100, # unused value > 0x0f
613 613 'red_background': _BACKGROUND_RED,
614 614 'green_background': _BACKGROUND_GREEN,
615 615 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
616 616 'blue_background': _BACKGROUND_BLUE,
617 617 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
618 618 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
619 619 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
620 620 _BACKGROUND_BLUE),
621 621 'bold_background': _BACKGROUND_INTENSITY,
622 622 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
623 623 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
624 624 }
625 625
626 626 passthrough = set([_FOREGROUND_INTENSITY,
627 627 _BACKGROUND_INTENSITY,
628 628 _COMMON_LVB_UNDERSCORE,
629 629 _COMMON_LVB_REVERSE_VIDEO])
630 630
631 631 stdout = _kernel32.GetStdHandle(
632 632 _STD_OUTPUT_HANDLE) # don't close the handle returned
633 633 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
634 634 w32effects = None
635 635 else:
636 636 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
637 637 if not _kernel32.GetConsoleScreenBufferInfo(
638 638 stdout, ctypes.byref(csbi)):
639 639 # stdout may not support GetConsoleScreenBufferInfo()
640 640 # when called from subprocess or redirected
641 641 w32effects = None
642 642 else:
643 643 origattr = csbi.wAttributes
644 644 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
645 645 re.MULTILINE | re.DOTALL)
646 646
647 647 def win32print(text, orig, **opts):
648 648 label = opts.get('label', '')
649 649 attr = origattr
650 650
651 651 def mapcolor(val, attr):
652 652 if val == -1:
653 653 return origattr
654 654 elif val in passthrough:
655 655 return attr | val
656 656 elif val > 0x0f:
657 657 return (val & 0x70) | (attr & 0x8f)
658 658 else:
659 659 return (val & 0x07) | (attr & 0xf8)
660 660
661 661 # determine console attributes based on labels
662 662 for l in label.split():
663 663 style = _styles.get(l, '')
664 664 for effect in style.split():
665 665 try:
666 666 attr = mapcolor(w32effects[effect], attr)
667 667 except KeyError:
668 668 # w32effects could not have certain attributes so we skip
669 669 # them if not found
670 670 pass
671 671 # hack to ensure regexp finds data
672 672 if not text.startswith('\033['):
673 673 text = '\033[m' + text
674 674
675 675 # Look for ANSI-like codes embedded in text
676 676 m = re.match(ansire, text)
677 677
678 678 try:
679 679 while m:
680 680 for sattr in m.group(1).split(';'):
681 681 if sattr:
682 682 attr = mapcolor(int(sattr), attr)
683 683 _kernel32.SetConsoleTextAttribute(stdout, attr)
684 684 orig(m.group(2), **opts)
685 685 m = re.match(ansire, m.group(3))
686 686 finally:
687 687 # Explicitly reset original attributes
688 688 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,471 +1,471 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@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
8 8 import base64, errno, subprocess, os, datetime, re
9 9 import cPickle as pickle
10 10 from mercurial import phases, util
11 11 from mercurial.i18n import _
12 12
13 13 propertycache = util.propertycache
14 14
15 15 def encodeargs(args):
16 16 def encodearg(s):
17 17 lines = base64.encodestring(s)
18 18 lines = [l.splitlines()[0] for l in lines]
19 19 return ''.join(lines)
20 20
21 21 s = pickle.dumps(args)
22 22 return encodearg(s)
23 23
24 24 def decodeargs(s):
25 25 s = base64.decodestring(s)
26 26 return pickle.loads(s)
27 27
28 28 class MissingTool(Exception):
29 29 pass
30 30
31 31 def checktool(exe, name=None, abort=True):
32 32 name = name or exe
33 33 if not util.findexe(exe):
34 34 if abort:
35 35 exc = util.Abort
36 36 else:
37 37 exc = MissingTool
38 38 raise exc(_('cannot find required "%s" tool') % name)
39 39
40 40 class NoRepo(Exception):
41 41 pass
42 42
43 43 SKIPREV = 'SKIP'
44 44
45 45 class commit(object):
46 46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
47 47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
48 48 self.author = author or 'unknown'
49 49 self.date = date or '0 0'
50 50 self.desc = desc
51 51 self.parents = parents
52 52 self.branch = branch
53 53 self.rev = rev
54 54 self.extra = extra
55 55 self.sortkey = sortkey
56 56 self.saverev = saverev
57 57 self.phase = phase
58 58
59 59 class converter_source(object):
60 60 """Conversion source interface"""
61 61
62 62 def __init__(self, ui, path=None, rev=None):
63 63 """Initialize conversion source (or raise NoRepo("message")
64 64 exception if path is not a valid repository)"""
65 65 self.ui = ui
66 66 self.path = path
67 67 self.rev = rev
68 68
69 69 self.encoding = 'utf-8'
70 70
71 71 def checkhexformat(self, revstr, mapname='splicemap'):
72 72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
73 73 such format for their revision numbering
74 74 """
75 75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
76 76 raise util.Abort(_('%s entry %s is not a valid revision'
77 77 ' identifier') % (mapname, revstr))
78 78
79 79 def before(self):
80 80 pass
81 81
82 82 def after(self):
83 83 pass
84 84
85 85 def setrevmap(self, revmap):
86 86 """set the map of already-converted revisions"""
87 87 pass
88 88
89 89 def getheads(self):
90 90 """Return a list of this repository's heads"""
91 91 raise NotImplementedError
92 92
93 93 def getfile(self, name, rev):
94 94 """Return a pair (data, mode) where data is the file content
95 95 as a string and mode one of '', 'x' or 'l'. rev is the
96 96 identifier returned by a previous call to getchanges().
97 97 Data is None if file is missing/deleted in rev.
98 98 """
99 99 raise NotImplementedError
100 100
101 101 def getchanges(self, version, full):
102 102 """Returns a tuple of (files, copies, cleanp2).
103 103
104 104 files is a sorted list of (filename, id) tuples for all files
105 105 changed between version and its first parent returned by
106 106 getcommit(). If full, all files in that revision is returned.
107 107 id is the source revision id of the file.
108 108
109 109 copies is a dictionary of dest: source
110 110
111 111 cleanp2 is the set of files filenames that are clean against p2.
112 112 (Files that are clean against p1 are already not in files (unless
113 113 full). This makes it possible to handle p2 clean files similarly.)
114 114 """
115 115 raise NotImplementedError
116 116
117 117 def getcommit(self, version):
118 118 """Return the commit object for version"""
119 119 raise NotImplementedError
120 120
121 121 def numcommits(self):
122 122 """Return the number of commits in this source.
123 123
124 124 If unknown, return None.
125 125 """
126 126 return None
127 127
128 128 def gettags(self):
129 129 """Return the tags as a dictionary of name: revision
130 130
131 131 Tag names must be UTF-8 strings.
132 132 """
133 133 raise NotImplementedError
134 134
135 135 def recode(self, s, encoding=None):
136 136 if not encoding:
137 137 encoding = self.encoding or 'utf-8'
138 138
139 139 if isinstance(s, unicode):
140 140 return s.encode("utf-8")
141 141 try:
142 142 return s.decode(encoding).encode("utf-8")
143 143 except UnicodeError:
144 144 try:
145 145 return s.decode("latin-1").encode("utf-8")
146 146 except UnicodeError:
147 147 return s.decode(encoding, "replace").encode("utf-8")
148 148
149 149 def getchangedfiles(self, rev, i):
150 150 """Return the files changed by rev compared to parent[i].
151 151
152 152 i is an index selecting one of the parents of rev. The return
153 153 value should be the list of files that are different in rev and
154 154 this parent.
155 155
156 156 If rev has no parents, i is None.
157 157
158 158 This function is only needed to support --filemap
159 159 """
160 160 raise NotImplementedError
161 161
162 162 def converted(self, rev, sinkrev):
163 163 '''Notify the source that a revision has been converted.'''
164 164 pass
165 165
166 166 def hasnativeorder(self):
167 167 """Return true if this source has a meaningful, native revision
168 168 order. For instance, Mercurial revisions are store sequentially
169 169 while there is no such global ordering with Darcs.
170 170 """
171 171 return False
172 172
173 173 def hasnativeclose(self):
174 174 """Return true if this source has ability to close branch.
175 175 """
176 176 return False
177 177
178 178 def lookuprev(self, rev):
179 179 """If rev is a meaningful revision reference in source, return
180 180 the referenced identifier in the same format used by getcommit().
181 181 return None otherwise.
182 182 """
183 183 return None
184 184
185 185 def getbookmarks(self):
186 186 """Return the bookmarks as a dictionary of name: revision
187 187
188 188 Bookmark names are to be UTF-8 strings.
189 189 """
190 190 return {}
191 191
192 192 def checkrevformat(self, revstr, mapname='splicemap'):
193 193 """revstr is a string that describes a revision in the given
194 194 source control system. Return true if revstr has correct
195 195 format.
196 196 """
197 197 return True
198 198
199 199 class converter_sink(object):
200 200 """Conversion sink (target) interface"""
201 201
202 202 def __init__(self, ui, path):
203 203 """Initialize conversion sink (or raise NoRepo("message")
204 204 exception if path is not a valid repository)
205 205
206 206 created is a list of paths to remove if a fatal error occurs
207 207 later"""
208 208 self.ui = ui
209 209 self.path = path
210 210 self.created = []
211 211
212 212 def revmapfile(self):
213 213 """Path to a file that will contain lines
214 214 source_rev_id sink_rev_id
215 215 mapping equivalent revision identifiers for each system."""
216 216 raise NotImplementedError
217 217
218 218 def authorfile(self):
219 219 """Path to a file that will contain lines
220 220 srcauthor=dstauthor
221 221 mapping equivalent authors identifiers for each system."""
222 222 return None
223 223
224 224 def putcommit(self, files, copies, parents, commit, source, revmap, full,
225 225 cleanp2):
226 226 """Create a revision with all changed files listed in 'files'
227 227 and having listed parents. 'commit' is a commit object
228 228 containing at a minimum the author, date, and message for this
229 229 changeset. 'files' is a list of (path, version) tuples,
230 230 'copies' is a dictionary mapping destinations to sources,
231 231 'source' is the source repository, and 'revmap' is a mapfile
232 232 of source revisions to converted revisions. Only getfile() and
233 233 lookuprev() should be called on 'source'. 'full' means that 'files'
234 234 is complete and all other files should be removed.
235 235 'cleanp2' is a set of the filenames that are unchanged from p2
236 236 (only in the common merge case where there two parents).
237 237
238 238 Note that the sink repository is not told to update itself to
239 239 a particular revision (or even what that revision would be)
240 240 before it receives the file data.
241 241 """
242 242 raise NotImplementedError
243 243
244 244 def puttags(self, tags):
245 245 """Put tags into sink.
246 246
247 247 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
248 248 Return a pair (tag_revision, tag_parent_revision), or (None, None)
249 249 if nothing was changed.
250 250 """
251 251 raise NotImplementedError
252 252
253 253 def setbranch(self, branch, pbranches):
254 254 """Set the current branch name. Called before the first putcommit
255 255 on the branch.
256 256 branch: branch name for subsequent commits
257 257 pbranches: (converted parent revision, parent branch) tuples"""
258 258 pass
259 259
260 260 def setfilemapmode(self, active):
261 261 """Tell the destination that we're using a filemap
262 262
263 263 Some converter_sources (svn in particular) can claim that a file
264 264 was changed in a revision, even if there was no change. This method
265 265 tells the destination that we're using a filemap and that it should
266 266 filter empty revisions.
267 267 """
268 268 pass
269 269
270 270 def before(self):
271 271 pass
272 272
273 273 def after(self):
274 274 pass
275 275
276 276 def putbookmarks(self, bookmarks):
277 277 """Put bookmarks into sink.
278 278
279 279 bookmarks: {bookmarkname: sink_rev_id, ...}
280 280 where bookmarkname is an UTF-8 string.
281 281 """
282 282 pass
283 283
284 284 def hascommitfrommap(self, rev):
285 285 """Return False if a rev mentioned in a filemap is known to not be
286 286 present."""
287 287 raise NotImplementedError
288 288
289 289 def hascommitforsplicemap(self, rev):
290 290 """This method is for the special needs for splicemap handling and not
291 291 for general use. Returns True if the sink contains rev, aborts on some
292 292 special cases."""
293 293 raise NotImplementedError
294 294
295 295 class commandline(object):
296 296 def __init__(self, ui, command):
297 297 self.ui = ui
298 298 self.command = command
299 299
300 300 def prerun(self):
301 301 pass
302 302
303 303 def postrun(self):
304 304 pass
305 305
306 306 def _cmdline(self, cmd, *args, **kwargs):
307 307 cmdline = [self.command, cmd] + list(args)
308 308 for k, v in kwargs.iteritems():
309 309 if len(k) == 1:
310 310 cmdline.append('-' + k)
311 311 else:
312 312 cmdline.append('--' + k.replace('_', '-'))
313 313 try:
314 314 if len(k) == 1:
315 315 cmdline.append('' + v)
316 316 else:
317 317 cmdline[-1] += '=' + v
318 318 except TypeError:
319 319 pass
320 320 cmdline = [util.shellquote(arg) for arg in cmdline]
321 321 if not self.ui.debugflag:
322 322 cmdline += ['2>', os.devnull]
323 323 cmdline = ' '.join(cmdline)
324 324 return cmdline
325 325
326 326 def _run(self, cmd, *args, **kwargs):
327 327 def popen(cmdline):
328 328 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
329 329 close_fds=util.closefds,
330 330 stdout=subprocess.PIPE)
331 331 return p
332 332 return self._dorun(popen, cmd, *args, **kwargs)
333 333
334 334 def _run2(self, cmd, *args, **kwargs):
335 335 return self._dorun(util.popen2, cmd, *args, **kwargs)
336 336
337 337 def _dorun(self, openfunc, cmd, *args, **kwargs):
338 338 cmdline = self._cmdline(cmd, *args, **kwargs)
339 339 self.ui.debug('running: %s\n' % (cmdline,))
340 340 self.prerun()
341 341 try:
342 342 return openfunc(cmdline)
343 343 finally:
344 344 self.postrun()
345 345
346 346 def run(self, cmd, *args, **kwargs):
347 347 p = self._run(cmd, *args, **kwargs)
348 348 output = p.communicate()[0]
349 349 self.ui.debug(output)
350 350 return output, p.returncode
351 351
352 352 def runlines(self, cmd, *args, **kwargs):
353 353 p = self._run(cmd, *args, **kwargs)
354 354 output = p.stdout.readlines()
355 355 p.wait()
356 356 self.ui.debug(''.join(output))
357 357 return output, p.returncode
358 358
359 359 def checkexit(self, status, output=''):
360 360 if status:
361 361 if output:
362 362 self.ui.warn(_('%s error:\n') % self.command)
363 363 self.ui.warn(output)
364 364 msg = util.explainexit(status)[0]
365 365 raise util.Abort('%s %s' % (self.command, msg))
366 366
367 367 def run0(self, cmd, *args, **kwargs):
368 368 output, status = self.run(cmd, *args, **kwargs)
369 369 self.checkexit(status, output)
370 370 return output
371 371
372 372 def runlines0(self, cmd, *args, **kwargs):
373 373 output, status = self.runlines(cmd, *args, **kwargs)
374 374 self.checkexit(status, ''.join(output))
375 375 return output
376 376
377 377 @propertycache
378 378 def argmax(self):
379 379 # POSIX requires at least 4096 bytes for ARG_MAX
380 380 argmax = 4096
381 381 try:
382 382 argmax = os.sysconf("SC_ARG_MAX")
383 383 except (AttributeError, ValueError):
384 384 pass
385 385
386 386 # Windows shells impose their own limits on command line length,
387 387 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
388 388 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
389 389 # details about cmd.exe limitations.
390 390
391 391 # Since ARG_MAX is for command line _and_ environment, lower our limit
392 392 # (and make happy Windows shells while doing this).
393 393 return argmax // 2 - 1
394 394
395 395 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
396 396 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
397 397 limit = self.argmax - cmdlen
398 398 bytes = 0
399 399 fl = []
400 400 for fn in arglist:
401 401 b = len(fn) + 3
402 402 if bytes + b < limit or len(fl) == 0:
403 403 fl.append(fn)
404 404 bytes += b
405 405 else:
406 406 yield fl
407 407 fl = [fn]
408 408 bytes = b
409 409 if fl:
410 410 yield fl
411 411
412 412 def xargs(self, arglist, cmd, *args, **kwargs):
413 413 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
414 414 self.run0(cmd, *(list(args) + l), **kwargs)
415 415
416 416 class mapfile(dict):
417 417 def __init__(self, ui, path):
418 418 super(mapfile, self).__init__()
419 419 self.ui = ui
420 420 self.path = path
421 421 self.fp = None
422 422 self.order = []
423 423 self._read()
424 424
425 425 def _read(self):
426 426 if not self.path:
427 427 return
428 428 try:
429 429 fp = open(self.path, 'r')
430 except IOError, err:
430 except IOError as err:
431 431 if err.errno != errno.ENOENT:
432 432 raise
433 433 return
434 434 for i, line in enumerate(fp):
435 435 line = line.splitlines()[0].rstrip()
436 436 if not line:
437 437 # Ignore blank lines
438 438 continue
439 439 try:
440 440 key, value = line.rsplit(' ', 1)
441 441 except ValueError:
442 442 raise util.Abort(
443 443 _('syntax error in %s(%d): key/value pair expected')
444 444 % (self.path, i + 1))
445 445 if key not in self:
446 446 self.order.append(key)
447 447 super(mapfile, self).__setitem__(key, value)
448 448 fp.close()
449 449
450 450 def __setitem__(self, key, value):
451 451 if self.fp is None:
452 452 try:
453 453 self.fp = open(self.path, 'a')
454 except IOError, err:
454 except IOError as err:
455 455 raise util.Abort(_('could not open map file %r: %s') %
456 456 (self.path, err.strerror))
457 457 self.fp.write('%s %s\n' % (key, value))
458 458 self.fp.flush()
459 459 super(mapfile, self).__setitem__(key, value)
460 460
461 461 def close(self):
462 462 if self.fp:
463 463 self.fp.close()
464 464 self.fp = None
465 465
466 466 def makedatetimestamp(t):
467 467 """Like util.makedate() but for time t instead of current time"""
468 468 delta = (datetime.datetime.utcfromtimestamp(t) -
469 469 datetime.datetime.fromtimestamp(t))
470 470 tz = delta.days * 86400 + delta.seconds
471 471 return t, tz
@@ -1,547 +1,547 b''
1 1 # convcmd - convert extension commands definition
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@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 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 from bzr import bzr_source
17 17 from p4 import p4_source
18 18 import filemap
19 19
20 20 import os, shutil, shlex
21 21 from mercurial import hg, util, encoding
22 22 from mercurial.i18n import _
23 23
24 24 orig_encoding = 'ascii'
25 25
26 26 def recode(s):
27 27 if isinstance(s, unicode):
28 28 return s.encode(orig_encoding, 'replace')
29 29 else:
30 30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31 31
32 32 source_converters = [
33 33 ('cvs', convert_cvs, 'branchsort'),
34 34 ('git', convert_git, 'branchsort'),
35 35 ('svn', svn_source, 'branchsort'),
36 36 ('hg', mercurial_source, 'sourcesort'),
37 37 ('darcs', darcs_source, 'branchsort'),
38 38 ('mtn', monotone_source, 'branchsort'),
39 39 ('gnuarch', gnuarch_source, 'branchsort'),
40 40 ('bzr', bzr_source, 'branchsort'),
41 41 ('p4', p4_source, 'branchsort'),
42 42 ]
43 43
44 44 sink_converters = [
45 45 ('hg', mercurial_sink),
46 46 ('svn', svn_sink),
47 47 ]
48 48
49 49 def convertsource(ui, path, type, rev):
50 50 exceptions = []
51 51 if type and type not in [s[0] for s in source_converters]:
52 52 raise util.Abort(_('%s: invalid source repository type') % type)
53 53 for name, source, sortmode in source_converters:
54 54 try:
55 55 if not type or name == type:
56 56 return source(ui, path, rev), sortmode
57 except (NoRepo, MissingTool), inst:
57 except (NoRepo, MissingTool) as inst:
58 58 exceptions.append(inst)
59 59 if not ui.quiet:
60 60 for inst in exceptions:
61 61 ui.write("%s\n" % inst)
62 62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63 63
64 64 def convertsink(ui, path, type):
65 65 if type and type not in [s[0] for s in sink_converters]:
66 66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 67 for name, sink in sink_converters:
68 68 try:
69 69 if not type or name == type:
70 70 return sink(ui, path)
71 except NoRepo, inst:
71 except NoRepo as inst:
72 72 ui.note(_("convert: %s\n") % inst)
73 except MissingTool, inst:
73 except MissingTool as inst:
74 74 raise util.Abort('%s\n' % inst)
75 75 raise util.Abort(_('%s: unknown repository type') % path)
76 76
77 77 class progresssource(object):
78 78 def __init__(self, ui, source, filecount):
79 79 self.ui = ui
80 80 self.source = source
81 81 self.filecount = filecount
82 82 self.retrieved = 0
83 83
84 84 def getfile(self, file, rev):
85 85 self.retrieved += 1
86 86 self.ui.progress(_('getting files'), self.retrieved,
87 87 item=file, total=self.filecount)
88 88 return self.source.getfile(file, rev)
89 89
90 90 def lookuprev(self, rev):
91 91 return self.source.lookuprev(rev)
92 92
93 93 def close(self):
94 94 self.ui.progress(_('getting files'), None)
95 95
96 96 class converter(object):
97 97 def __init__(self, ui, source, dest, revmapfile, opts):
98 98
99 99 self.source = source
100 100 self.dest = dest
101 101 self.ui = ui
102 102 self.opts = opts
103 103 self.commitcache = {}
104 104 self.authors = {}
105 105 self.authorfile = None
106 106
107 107 # Record converted revisions persistently: maps source revision
108 108 # ID to target revision ID (both strings). (This is how
109 109 # incremental conversions work.)
110 110 self.map = mapfile(ui, revmapfile)
111 111
112 112 # Read first the dst author map if any
113 113 authorfile = self.dest.authorfile()
114 114 if authorfile and os.path.exists(authorfile):
115 115 self.readauthormap(authorfile)
116 116 # Extend/Override with new author map if necessary
117 117 if opts.get('authormap'):
118 118 self.readauthormap(opts.get('authormap'))
119 119 self.authorfile = self.dest.authorfile()
120 120
121 121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123 123
124 124 def parsesplicemap(self, path):
125 125 """ check and validate the splicemap format and
126 126 return a child/parents dictionary.
127 127 Format checking has two parts.
128 128 1. generic format which is same across all source types
129 129 2. specific format checking which may be different for
130 130 different source type. This logic is implemented in
131 131 checkrevformat function in source files like
132 132 hg.py, subversion.py etc.
133 133 """
134 134
135 135 if not path:
136 136 return {}
137 137 m = {}
138 138 try:
139 139 fp = open(path, 'r')
140 140 for i, line in enumerate(fp):
141 141 line = line.splitlines()[0].rstrip()
142 142 if not line:
143 143 # Ignore blank lines
144 144 continue
145 145 # split line
146 146 lex = shlex.shlex(line, posix=True)
147 147 lex.whitespace_split = True
148 148 lex.whitespace += ','
149 149 line = list(lex)
150 150 # check number of parents
151 151 if not (2 <= len(line) <= 3):
152 152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 153 '[,parent2] expected') % (path, i + 1))
154 154 for part in line:
155 155 self.source.checkrevformat(part)
156 156 child, p1, p2 = line[0], line[1:2], line[2:]
157 157 if p1 == p2:
158 158 m[child] = p1
159 159 else:
160 160 m[child] = p1 + p2
161 161 # if file does not exist or error reading, exit
162 162 except IOError:
163 163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 164 % path)
165 165 return m
166 166
167 167
168 168 def walktree(self, heads):
169 169 '''Return a mapping that identifies the uncommitted parents of every
170 170 uncommitted changeset.'''
171 171 visit = heads
172 172 known = set()
173 173 parents = {}
174 174 numcommits = self.source.numcommits()
175 175 while visit:
176 176 n = visit.pop(0)
177 177 if n in known:
178 178 continue
179 179 if n in self.map:
180 180 m = self.map[n]
181 181 if m == SKIPREV or self.dest.hascommitfrommap(m):
182 182 continue
183 183 known.add(n)
184 184 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
185 185 total=numcommits)
186 186 commit = self.cachecommit(n)
187 187 parents[n] = []
188 188 for p in commit.parents:
189 189 parents[n].append(p)
190 190 visit.append(p)
191 191 self.ui.progress(_('scanning'), None)
192 192
193 193 return parents
194 194
195 195 def mergesplicemap(self, parents, splicemap):
196 196 """A splicemap redefines child/parent relationships. Check the
197 197 map contains valid revision identifiers and merge the new
198 198 links in the source graph.
199 199 """
200 200 for c in sorted(splicemap):
201 201 if c not in parents:
202 202 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
203 203 # Could be in source but not converted during this run
204 204 self.ui.warn(_('splice map revision %s is not being '
205 205 'converted, ignoring\n') % c)
206 206 continue
207 207 pc = []
208 208 for p in splicemap[c]:
209 209 # We do not have to wait for nodes already in dest.
210 210 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
211 211 continue
212 212 # Parent is not in dest and not being converted, not good
213 213 if p not in parents:
214 214 raise util.Abort(_('unknown splice map parent: %s') % p)
215 215 pc.append(p)
216 216 parents[c] = pc
217 217
218 218 def toposort(self, parents, sortmode):
219 219 '''Return an ordering such that every uncommitted changeset is
220 220 preceded by all its uncommitted ancestors.'''
221 221
222 222 def mapchildren(parents):
223 223 """Return a (children, roots) tuple where 'children' maps parent
224 224 revision identifiers to children ones, and 'roots' is the list of
225 225 revisions without parents. 'parents' must be a mapping of revision
226 226 identifier to its parents ones.
227 227 """
228 228 visit = sorted(parents)
229 229 seen = set()
230 230 children = {}
231 231 roots = []
232 232
233 233 while visit:
234 234 n = visit.pop(0)
235 235 if n in seen:
236 236 continue
237 237 seen.add(n)
238 238 # Ensure that nodes without parents are present in the
239 239 # 'children' mapping.
240 240 children.setdefault(n, [])
241 241 hasparent = False
242 242 for p in parents[n]:
243 243 if p not in self.map:
244 244 visit.append(p)
245 245 hasparent = True
246 246 children.setdefault(p, []).append(n)
247 247 if not hasparent:
248 248 roots.append(n)
249 249
250 250 return children, roots
251 251
252 252 # Sort functions are supposed to take a list of revisions which
253 253 # can be converted immediately and pick one
254 254
255 255 def makebranchsorter():
256 256 """If the previously converted revision has a child in the
257 257 eligible revisions list, pick it. Return the list head
258 258 otherwise. Branch sort attempts to minimize branch
259 259 switching, which is harmful for Mercurial backend
260 260 compression.
261 261 """
262 262 prev = [None]
263 263 def picknext(nodes):
264 264 next = nodes[0]
265 265 for n in nodes:
266 266 if prev[0] in parents[n]:
267 267 next = n
268 268 break
269 269 prev[0] = next
270 270 return next
271 271 return picknext
272 272
273 273 def makesourcesorter():
274 274 """Source specific sort."""
275 275 keyfn = lambda n: self.commitcache[n].sortkey
276 276 def picknext(nodes):
277 277 return sorted(nodes, key=keyfn)[0]
278 278 return picknext
279 279
280 280 def makeclosesorter():
281 281 """Close order sort."""
282 282 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
283 283 self.commitcache[n].sortkey)
284 284 def picknext(nodes):
285 285 return sorted(nodes, key=keyfn)[0]
286 286 return picknext
287 287
288 288 def makedatesorter():
289 289 """Sort revisions by date."""
290 290 dates = {}
291 291 def getdate(n):
292 292 if n not in dates:
293 293 dates[n] = util.parsedate(self.commitcache[n].date)
294 294 return dates[n]
295 295
296 296 def picknext(nodes):
297 297 return min([(getdate(n), n) for n in nodes])[1]
298 298
299 299 return picknext
300 300
301 301 if sortmode == 'branchsort':
302 302 picknext = makebranchsorter()
303 303 elif sortmode == 'datesort':
304 304 picknext = makedatesorter()
305 305 elif sortmode == 'sourcesort':
306 306 picknext = makesourcesorter()
307 307 elif sortmode == 'closesort':
308 308 picknext = makeclosesorter()
309 309 else:
310 310 raise util.Abort(_('unknown sort mode: %s') % sortmode)
311 311
312 312 children, actives = mapchildren(parents)
313 313
314 314 s = []
315 315 pendings = {}
316 316 while actives:
317 317 n = picknext(actives)
318 318 actives.remove(n)
319 319 s.append(n)
320 320
321 321 # Update dependents list
322 322 for c in children.get(n, []):
323 323 if c not in pendings:
324 324 pendings[c] = [p for p in parents[c] if p not in self.map]
325 325 try:
326 326 pendings[c].remove(n)
327 327 except ValueError:
328 328 raise util.Abort(_('cycle detected between %s and %s')
329 329 % (recode(c), recode(n)))
330 330 if not pendings[c]:
331 331 # Parents are converted, node is eligible
332 332 actives.insert(0, c)
333 333 pendings[c] = None
334 334
335 335 if len(s) != len(parents):
336 336 raise util.Abort(_("not all revisions were sorted"))
337 337
338 338 return s
339 339
340 340 def writeauthormap(self):
341 341 authorfile = self.authorfile
342 342 if authorfile:
343 343 self.ui.status(_('writing author map file %s\n') % authorfile)
344 344 ofile = open(authorfile, 'w+')
345 345 for author in self.authors:
346 346 ofile.write("%s=%s\n" % (author, self.authors[author]))
347 347 ofile.close()
348 348
349 349 def readauthormap(self, authorfile):
350 350 afile = open(authorfile, 'r')
351 351 for line in afile:
352 352
353 353 line = line.strip()
354 354 if not line or line.startswith('#'):
355 355 continue
356 356
357 357 try:
358 358 srcauthor, dstauthor = line.split('=', 1)
359 359 except ValueError:
360 360 msg = _('ignoring bad line in author map file %s: %s\n')
361 361 self.ui.warn(msg % (authorfile, line.rstrip()))
362 362 continue
363 363
364 364 srcauthor = srcauthor.strip()
365 365 dstauthor = dstauthor.strip()
366 366 if self.authors.get(srcauthor) in (None, dstauthor):
367 367 msg = _('mapping author %s to %s\n')
368 368 self.ui.debug(msg % (srcauthor, dstauthor))
369 369 self.authors[srcauthor] = dstauthor
370 370 continue
371 371
372 372 m = _('overriding mapping for author %s, was %s, will be %s\n')
373 373 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
374 374
375 375 afile.close()
376 376
377 377 def cachecommit(self, rev):
378 378 commit = self.source.getcommit(rev)
379 379 commit.author = self.authors.get(commit.author, commit.author)
380 380 # If commit.branch is None, this commit is coming from the source
381 381 # repository's default branch and destined for the default branch in the
382 382 # destination repository. For such commits, passing a literal "None"
383 383 # string to branchmap.get() below allows the user to map "None" to an
384 384 # alternate default branch in the destination repository.
385 385 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
386 386 self.commitcache[rev] = commit
387 387 return commit
388 388
389 389 def copy(self, rev):
390 390 commit = self.commitcache[rev]
391 391 full = self.opts.get('full')
392 392 changes = self.source.getchanges(rev, full)
393 393 if isinstance(changes, basestring):
394 394 if changes == SKIPREV:
395 395 dest = SKIPREV
396 396 else:
397 397 dest = self.map[changes]
398 398 self.map[rev] = dest
399 399 return
400 400 files, copies, cleanp2 = changes
401 401 pbranches = []
402 402 if commit.parents:
403 403 for prev in commit.parents:
404 404 if prev not in self.commitcache:
405 405 self.cachecommit(prev)
406 406 pbranches.append((self.map[prev],
407 407 self.commitcache[prev].branch))
408 408 self.dest.setbranch(commit.branch, pbranches)
409 409 try:
410 410 parents = self.splicemap[rev]
411 411 self.ui.status(_('spliced in %s as parents of %s\n') %
412 412 (parents, rev))
413 413 parents = [self.map.get(p, p) for p in parents]
414 414 except KeyError:
415 415 parents = [b[0] for b in pbranches]
416 416 if len(pbranches) != 2:
417 417 cleanp2 = set()
418 418 if len(parents) < 3:
419 419 source = progresssource(self.ui, self.source, len(files))
420 420 else:
421 421 # For an octopus merge, we end up traversing the list of
422 422 # changed files N-1 times. This tweak to the number of
423 423 # files makes it so the progress bar doesn't overflow
424 424 # itself.
425 425 source = progresssource(self.ui, self.source,
426 426 len(files) * (len(parents) - 1))
427 427 newnode = self.dest.putcommit(files, copies, parents, commit,
428 428 source, self.map, full, cleanp2)
429 429 source.close()
430 430 self.source.converted(rev, newnode)
431 431 self.map[rev] = newnode
432 432
433 433 def convert(self, sortmode):
434 434 try:
435 435 self.source.before()
436 436 self.dest.before()
437 437 self.source.setrevmap(self.map)
438 438 self.ui.status(_("scanning source...\n"))
439 439 heads = self.source.getheads()
440 440 parents = self.walktree(heads)
441 441 self.mergesplicemap(parents, self.splicemap)
442 442 self.ui.status(_("sorting...\n"))
443 443 t = self.toposort(parents, sortmode)
444 444 num = len(t)
445 445 c = None
446 446
447 447 self.ui.status(_("converting...\n"))
448 448 for i, c in enumerate(t):
449 449 num -= 1
450 450 desc = self.commitcache[c].desc
451 451 if "\n" in desc:
452 452 desc = desc.splitlines()[0]
453 453 # convert log message to local encoding without using
454 454 # tolocal() because the encoding.encoding convert()
455 455 # uses is 'utf-8'
456 456 self.ui.status("%d %s\n" % (num, recode(desc)))
457 457 self.ui.note(_("source: %s\n") % recode(c))
458 458 self.ui.progress(_('converting'), i, unit=_('revisions'),
459 459 total=len(t))
460 460 self.copy(c)
461 461 self.ui.progress(_('converting'), None)
462 462
463 463 tags = self.source.gettags()
464 464 ctags = {}
465 465 for k in tags:
466 466 v = tags[k]
467 467 if self.map.get(v, SKIPREV) != SKIPREV:
468 468 ctags[k] = self.map[v]
469 469
470 470 if c and ctags:
471 471 nrev, tagsparent = self.dest.puttags(ctags)
472 472 if nrev and tagsparent:
473 473 # write another hash correspondence to override the previous
474 474 # one so we don't end up with extra tag heads
475 475 tagsparents = [e for e in self.map.iteritems()
476 476 if e[1] == tagsparent]
477 477 if tagsparents:
478 478 self.map[tagsparents[0][0]] = nrev
479 479
480 480 bookmarks = self.source.getbookmarks()
481 481 cbookmarks = {}
482 482 for k in bookmarks:
483 483 v = bookmarks[k]
484 484 if self.map.get(v, SKIPREV) != SKIPREV:
485 485 cbookmarks[k] = self.map[v]
486 486
487 487 if c and cbookmarks:
488 488 self.dest.putbookmarks(cbookmarks)
489 489
490 490 self.writeauthormap()
491 491 finally:
492 492 self.cleanup()
493 493
494 494 def cleanup(self):
495 495 try:
496 496 self.dest.after()
497 497 finally:
498 498 self.source.after()
499 499 self.map.close()
500 500
501 501 def convert(ui, src, dest=None, revmapfile=None, **opts):
502 502 global orig_encoding
503 503 orig_encoding = encoding.encoding
504 504 encoding.encoding = 'UTF-8'
505 505
506 506 # support --authors as an alias for --authormap
507 507 if not opts.get('authormap'):
508 508 opts['authormap'] = opts.get('authors')
509 509
510 510 if not dest:
511 511 dest = hg.defaultdest(src) + "-hg"
512 512 ui.status(_("assuming destination %s\n") % dest)
513 513
514 514 destc = convertsink(ui, dest, opts.get('dest_type'))
515 515
516 516 try:
517 517 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
518 518 opts.get('rev'))
519 519 except Exception:
520 520 for path in destc.created:
521 521 shutil.rmtree(path, True)
522 522 raise
523 523
524 524 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
525 525 sortmode = [m for m in sortmodes if opts.get(m)]
526 526 if len(sortmode) > 1:
527 527 raise util.Abort(_('more than one sort mode specified'))
528 528 if sortmode:
529 529 sortmode = sortmode[0]
530 530 else:
531 531 sortmode = defaultsort
532 532
533 533 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
534 534 raise util.Abort(_('--sourcesort is not supported by this data source'))
535 535 if sortmode == 'closesort' and not srcc.hasnativeclose():
536 536 raise util.Abort(_('--closesort is not supported by this data source'))
537 537
538 538 fmap = opts.get('filemap')
539 539 if fmap:
540 540 srcc = filemap.filemap_source(ui, srcc, fmap)
541 541 destc.setfilemapmode(True)
542 542
543 543 if not revmapfile:
544 544 revmapfile = destc.revmapfile()
545 545
546 546 c = converter(ui, srcc, destc, revmapfile, opts)
547 547 c.convert(sortmode)
@@ -1,277 +1,277 b''
1 1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@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
8 8 import os, re, socket, errno
9 9 from cStringIO import StringIO
10 10 from mercurial import encoding, util
11 11 from mercurial.i18n import _
12 12
13 13 from common import NoRepo, commit, converter_source, checktool
14 14 from common import makedatetimestamp
15 15 import cvsps
16 16
17 17 class convert_cvs(converter_source):
18 18 def __init__(self, ui, path, rev=None):
19 19 super(convert_cvs, self).__init__(ui, path, rev=rev)
20 20
21 21 cvs = os.path.join(path, "CVS")
22 22 if not os.path.exists(cvs):
23 23 raise NoRepo(_("%s does not look like a CVS checkout") % path)
24 24
25 25 checktool('cvs')
26 26
27 27 self.changeset = None
28 28 self.files = {}
29 29 self.tags = {}
30 30 self.lastbranch = {}
31 31 self.socket = None
32 32 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
33 33 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
34 34 self.encoding = encoding.encoding
35 35
36 36 self._connect()
37 37
38 38 def _parse(self):
39 39 if self.changeset is not None:
40 40 return
41 41 self.changeset = {}
42 42
43 43 maxrev = 0
44 44 if self.rev:
45 45 # TODO: handle tags
46 46 try:
47 47 # patchset number?
48 48 maxrev = int(self.rev)
49 49 except ValueError:
50 50 raise util.Abort(_('revision %s is not a patchset number')
51 51 % self.rev)
52 52
53 53 d = os.getcwd()
54 54 try:
55 55 os.chdir(self.path)
56 56 id = None
57 57
58 58 cache = 'update'
59 59 if not self.ui.configbool('convert', 'cvsps.cache', True):
60 60 cache = None
61 61 db = cvsps.createlog(self.ui, cache=cache)
62 62 db = cvsps.createchangeset(self.ui, db,
63 63 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
64 64 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
65 65 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
66 66
67 67 for cs in db:
68 68 if maxrev and cs.id > maxrev:
69 69 break
70 70 id = str(cs.id)
71 71 cs.author = self.recode(cs.author)
72 72 self.lastbranch[cs.branch] = id
73 73 cs.comment = self.recode(cs.comment)
74 74 if self.ui.configbool('convert', 'localtimezone'):
75 75 cs.date = makedatetimestamp(cs.date[0])
76 76 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
77 77 self.tags.update(dict.fromkeys(cs.tags, id))
78 78
79 79 files = {}
80 80 for f in cs.entries:
81 81 files[f.file] = "%s%s" % ('.'.join([str(x)
82 82 for x in f.revision]),
83 83 ['', '(DEAD)'][f.dead])
84 84
85 85 # add current commit to set
86 86 c = commit(author=cs.author, date=date,
87 87 parents=[str(p.id) for p in cs.parents],
88 88 desc=cs.comment, branch=cs.branch or '')
89 89 self.changeset[id] = c
90 90 self.files[id] = files
91 91
92 92 self.heads = self.lastbranch.values()
93 93 finally:
94 94 os.chdir(d)
95 95
96 96 def _connect(self):
97 97 root = self.cvsroot
98 98 conntype = None
99 99 user, host = None, None
100 100 cmd = ['cvs', 'server']
101 101
102 102 self.ui.status(_("connecting to %s\n") % root)
103 103
104 104 if root.startswith(":pserver:"):
105 105 root = root[9:]
106 106 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
107 107 root)
108 108 if m:
109 109 conntype = "pserver"
110 110 user, passw, serv, port, root = m.groups()
111 111 if not user:
112 112 user = "anonymous"
113 113 if not port:
114 114 port = 2401
115 115 else:
116 116 port = int(port)
117 117 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
118 118 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
119 119
120 120 if not passw:
121 121 passw = "A"
122 122 cvspass = os.path.expanduser("~/.cvspass")
123 123 try:
124 124 pf = open(cvspass)
125 125 for line in pf.read().splitlines():
126 126 part1, part2 = line.split(' ', 1)
127 127 # /1 :pserver:user@example.com:2401/cvsroot/foo
128 128 # Ah<Z
129 129 if part1 == '/1':
130 130 part1, part2 = part2.split(' ', 1)
131 131 format = format1
132 132 # :pserver:user@example.com:/cvsroot/foo Ah<Z
133 133 else:
134 134 format = format0
135 135 if part1 == format:
136 136 passw = part2
137 137 break
138 138 pf.close()
139 except IOError, inst:
139 except IOError as inst:
140 140 if inst.errno != errno.ENOENT:
141 141 if not getattr(inst, 'filename', None):
142 142 inst.filename = cvspass
143 143 raise
144 144
145 145 sck = socket.socket()
146 146 sck.connect((serv, port))
147 147 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
148 148 "END AUTH REQUEST", ""]))
149 149 if sck.recv(128) != "I LOVE YOU\n":
150 150 raise util.Abort(_("CVS pserver authentication failed"))
151 151
152 152 self.writep = self.readp = sck.makefile('r+')
153 153
154 154 if not conntype and root.startswith(":local:"):
155 155 conntype = "local"
156 156 root = root[7:]
157 157
158 158 if not conntype:
159 159 # :ext:user@host/home/user/path/to/cvsroot
160 160 if root.startswith(":ext:"):
161 161 root = root[5:]
162 162 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
163 163 # Do not take Windows path "c:\foo\bar" for a connection strings
164 164 if os.path.isdir(root) or not m:
165 165 conntype = "local"
166 166 else:
167 167 conntype = "rsh"
168 168 user, host, root = m.group(1), m.group(2), m.group(3)
169 169
170 170 if conntype != "pserver":
171 171 if conntype == "rsh":
172 172 rsh = os.environ.get("CVS_RSH") or "ssh"
173 173 if user:
174 174 cmd = [rsh, '-l', user, host] + cmd
175 175 else:
176 176 cmd = [rsh, host] + cmd
177 177
178 178 # popen2 does not support argument lists under Windows
179 179 cmd = [util.shellquote(arg) for arg in cmd]
180 180 cmd = util.quotecommand(' '.join(cmd))
181 181 self.writep, self.readp = util.popen2(cmd)
182 182
183 183 self.realroot = root
184 184
185 185 self.writep.write("Root %s\n" % root)
186 186 self.writep.write("Valid-responses ok error Valid-requests Mode"
187 187 " M Mbinary E Checked-in Created Updated"
188 188 " Merged Removed\n")
189 189 self.writep.write("valid-requests\n")
190 190 self.writep.flush()
191 191 r = self.readp.readline()
192 192 if not r.startswith("Valid-requests"):
193 193 raise util.Abort(_('unexpected response from CVS server '
194 194 '(expected "Valid-requests", but got %r)')
195 195 % r)
196 196 if "UseUnchanged" in r:
197 197 self.writep.write("UseUnchanged\n")
198 198 self.writep.flush()
199 199 r = self.readp.readline()
200 200
201 201 def getheads(self):
202 202 self._parse()
203 203 return self.heads
204 204
205 205 def getfile(self, name, rev):
206 206
207 207 def chunkedread(fp, count):
208 208 # file-objects returned by socket.makefile() do not handle
209 209 # large read() requests very well.
210 210 chunksize = 65536
211 211 output = StringIO()
212 212 while count > 0:
213 213 data = fp.read(min(count, chunksize))
214 214 if not data:
215 215 raise util.Abort(_("%d bytes missing from remote file")
216 216 % count)
217 217 count -= len(data)
218 218 output.write(data)
219 219 return output.getvalue()
220 220
221 221 self._parse()
222 222 if rev.endswith("(DEAD)"):
223 223 return None, None
224 224
225 225 args = ("-N -P -kk -r %s --" % rev).split()
226 226 args.append(self.cvsrepo + '/' + name)
227 227 for x in args:
228 228 self.writep.write("Argument %s\n" % x)
229 229 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
230 230 self.writep.flush()
231 231
232 232 data = ""
233 233 mode = None
234 234 while True:
235 235 line = self.readp.readline()
236 236 if line.startswith("Created ") or line.startswith("Updated "):
237 237 self.readp.readline() # path
238 238 self.readp.readline() # entries
239 239 mode = self.readp.readline()[:-1]
240 240 count = int(self.readp.readline()[:-1])
241 241 data = chunkedread(self.readp, count)
242 242 elif line.startswith(" "):
243 243 data += line[1:]
244 244 elif line.startswith("M "):
245 245 pass
246 246 elif line.startswith("Mbinary "):
247 247 count = int(self.readp.readline()[:-1])
248 248 data = chunkedread(self.readp, count)
249 249 else:
250 250 if line == "ok\n":
251 251 if mode is None:
252 252 raise util.Abort(_('malformed response from CVS'))
253 253 return (data, "x" in mode and "x" or "")
254 254 elif line.startswith("E "):
255 255 self.ui.warn(_("cvs server: %s\n") % line[2:])
256 256 elif line.startswith("Remove"):
257 257 self.readp.readline()
258 258 else:
259 259 raise util.Abort(_("unknown CVS response: %s") % line)
260 260
261 261 def getchanges(self, rev, full):
262 262 if full:
263 263 raise util.Abort(_("convert from cvs do not support --full"))
264 264 self._parse()
265 265 return sorted(self.files[rev].iteritems()), {}, set()
266 266
267 267 def getcommit(self, rev):
268 268 self._parse()
269 269 return self.changeset[rev]
270 270
271 271 def gettags(self):
272 272 self._parse()
273 273 return self.tags
274 274
275 275 def getchangedfiles(self, rev, i):
276 276 self._parse()
277 277 return sorted(self.files[rev])
@@ -1,904 +1,904 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
8 8 import os
9 9 import re
10 10 import cPickle as pickle
11 11 from mercurial.i18n import _
12 12 from mercurial import hook
13 13 from mercurial import util
14 14
15 15 class logentry(object):
16 16 '''Class logentry has the following attributes:
17 17 .author - author name as CVS knows it
18 18 .branch - name of branch this revision is on
19 19 .branches - revision tuple of branches starting at this revision
20 20 .comment - commit message
21 21 .commitid - CVS commitid or None
22 22 .date - the commit date as a (time, tz) tuple
23 23 .dead - true if file revision is dead
24 24 .file - Name of file
25 25 .lines - a tuple (+lines, -lines) or None
26 26 .parent - Previous revision of this entry
27 27 .rcs - name of file as returned from CVS
28 28 .revision - revision number as tuple
29 29 .tags - list of tags on the file
30 30 .synthetic - is this a synthetic "file ... added on ..." revision?
31 31 .mergepoint - the branch that has been merged from (if present in
32 32 rlog output) or None
33 33 .branchpoints - the branches that start at the current entry or empty
34 34 '''
35 35 def __init__(self, **entries):
36 36 self.synthetic = False
37 37 self.__dict__.update(entries)
38 38
39 39 def __repr__(self):
40 40 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
41 41 return "%s(%s)"%(type(self).__name__, ", ".join(items))
42 42
43 43 class logerror(Exception):
44 44 pass
45 45
46 46 def getrepopath(cvspath):
47 47 """Return the repository path from a CVS path.
48 48
49 49 >>> getrepopath('/foo/bar')
50 50 '/foo/bar'
51 51 >>> getrepopath('c:/foo/bar')
52 52 '/foo/bar'
53 53 >>> getrepopath(':pserver:10/foo/bar')
54 54 '/foo/bar'
55 55 >>> getrepopath(':pserver:10c:/foo/bar')
56 56 '/foo/bar'
57 57 >>> getrepopath(':pserver:/foo/bar')
58 58 '/foo/bar'
59 59 >>> getrepopath(':pserver:c:/foo/bar')
60 60 '/foo/bar'
61 61 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
62 62 '/foo/bar'
63 63 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
64 64 '/foo/bar'
65 65 >>> getrepopath('user@server/path/to/repository')
66 66 '/path/to/repository'
67 67 """
68 68 # According to CVS manual, CVS paths are expressed like:
69 69 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
70 70 #
71 71 # CVSpath is splitted into parts and then position of the first occurrence
72 72 # of the '/' char after the '@' is located. The solution is the rest of the
73 73 # string after that '/' sign including it
74 74
75 75 parts = cvspath.split(':')
76 76 atposition = parts[-1].find('@')
77 77 start = 0
78 78
79 79 if atposition != -1:
80 80 start = atposition
81 81
82 82 repopath = parts[-1][parts[-1].find('/', start):]
83 83 return repopath
84 84
85 85 def createlog(ui, directory=None, root="", rlog=True, cache=None):
86 86 '''Collect the CVS rlog'''
87 87
88 88 # Because we store many duplicate commit log messages, reusing strings
89 89 # saves a lot of memory and pickle storage space.
90 90 _scache = {}
91 91 def scache(s):
92 92 "return a shared version of a string"
93 93 return _scache.setdefault(s, s)
94 94
95 95 ui.status(_('collecting CVS rlog\n'))
96 96
97 97 log = [] # list of logentry objects containing the CVS state
98 98
99 99 # patterns to match in CVS (r)log output, by state of use
100 100 re_00 = re.compile('RCS file: (.+)$')
101 101 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
102 102 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
103 103 re_03 = re.compile("(Cannot access.+CVSROOT)|"
104 104 "(can't create temporary directory.+)$")
105 105 re_10 = re.compile('Working file: (.+)$')
106 106 re_20 = re.compile('symbolic names:')
107 107 re_30 = re.compile('\t(.+): ([\\d.]+)$')
108 108 re_31 = re.compile('----------------------------$')
109 109 re_32 = re.compile('======================================='
110 110 '======================================$')
111 111 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
112 112 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
113 113 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
114 114 r'(\s+commitid:\s+([^;]+);)?'
115 115 r'(.*mergepoint:\s+([^;]+);)?')
116 116 re_70 = re.compile('branches: (.+);$')
117 117
118 118 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
119 119
120 120 prefix = '' # leading path to strip of what we get from CVS
121 121
122 122 if directory is None:
123 123 # Current working directory
124 124
125 125 # Get the real directory in the repository
126 126 try:
127 127 prefix = open(os.path.join('CVS','Repository')).read().strip()
128 128 directory = prefix
129 129 if prefix == ".":
130 130 prefix = ""
131 131 except IOError:
132 132 raise logerror(_('not a CVS sandbox'))
133 133
134 134 if prefix and not prefix.endswith(os.sep):
135 135 prefix += os.sep
136 136
137 137 # Use the Root file in the sandbox, if it exists
138 138 try:
139 139 root = open(os.path.join('CVS','Root')).read().strip()
140 140 except IOError:
141 141 pass
142 142
143 143 if not root:
144 144 root = os.environ.get('CVSROOT', '')
145 145
146 146 # read log cache if one exists
147 147 oldlog = []
148 148 date = None
149 149
150 150 if cache:
151 151 cachedir = os.path.expanduser('~/.hg.cvsps')
152 152 if not os.path.exists(cachedir):
153 153 os.mkdir(cachedir)
154 154
155 155 # The cvsps cache pickle needs a uniquified name, based on the
156 156 # repository location. The address may have all sort of nasties
157 157 # in it, slashes, colons and such. So here we take just the
158 158 # alphanumeric characters, concatenated in a way that does not
159 159 # mix up the various components, so that
160 160 # :pserver:user@server:/path
161 161 # and
162 162 # /pserver/user/server/path
163 163 # are mapped to different cache file names.
164 164 cachefile = root.split(":") + [directory, "cache"]
165 165 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
166 166 cachefile = os.path.join(cachedir,
167 167 '.'.join([s for s in cachefile if s]))
168 168
169 169 if cache == 'update':
170 170 try:
171 171 ui.note(_('reading cvs log cache %s\n') % cachefile)
172 172 oldlog = pickle.load(open(cachefile))
173 173 for e in oldlog:
174 174 if not (util.safehasattr(e, 'branchpoints') and
175 175 util.safehasattr(e, 'commitid') and
176 176 util.safehasattr(e, 'mergepoint')):
177 177 ui.status(_('ignoring old cache\n'))
178 178 oldlog = []
179 179 break
180 180
181 181 ui.note(_('cache has %d log entries\n') % len(oldlog))
182 except Exception, e:
182 except Exception as e:
183 183 ui.note(_('error reading cache: %r\n') % e)
184 184
185 185 if oldlog:
186 186 date = oldlog[-1].date # last commit date as a (time,tz) tuple
187 187 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
188 188
189 189 # build the CVS commandline
190 190 cmd = ['cvs', '-q']
191 191 if root:
192 192 cmd.append('-d%s' % root)
193 193 p = util.normpath(getrepopath(root))
194 194 if not p.endswith('/'):
195 195 p += '/'
196 196 if prefix:
197 197 # looks like normpath replaces "" by "."
198 198 prefix = p + util.normpath(prefix)
199 199 else:
200 200 prefix = p
201 201 cmd.append(['log', 'rlog'][rlog])
202 202 if date:
203 203 # no space between option and date string
204 204 cmd.append('-d>%s' % date)
205 205 cmd.append(directory)
206 206
207 207 # state machine begins here
208 208 tags = {} # dictionary of revisions on current file with their tags
209 209 branchmap = {} # mapping between branch names and revision numbers
210 210 state = 0
211 211 store = False # set when a new record can be appended
212 212
213 213 cmd = [util.shellquote(arg) for arg in cmd]
214 214 ui.note(_("running %s\n") % (' '.join(cmd)))
215 215 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
216 216
217 217 pfp = util.popen(' '.join(cmd))
218 218 peek = pfp.readline()
219 219 while True:
220 220 line = peek
221 221 if line == '':
222 222 break
223 223 peek = pfp.readline()
224 224 if line.endswith('\n'):
225 225 line = line[:-1]
226 226 #ui.debug('state=%d line=%r\n' % (state, line))
227 227
228 228 if state == 0:
229 229 # initial state, consume input until we see 'RCS file'
230 230 match = re_00.match(line)
231 231 if match:
232 232 rcs = match.group(1)
233 233 tags = {}
234 234 if rlog:
235 235 filename = util.normpath(rcs[:-2])
236 236 if filename.startswith(prefix):
237 237 filename = filename[len(prefix):]
238 238 if filename.startswith('/'):
239 239 filename = filename[1:]
240 240 if filename.startswith('Attic/'):
241 241 filename = filename[6:]
242 242 else:
243 243 filename = filename.replace('/Attic/', '/')
244 244 state = 2
245 245 continue
246 246 state = 1
247 247 continue
248 248 match = re_01.match(line)
249 249 if match:
250 250 raise logerror(match.group(1))
251 251 match = re_02.match(line)
252 252 if match:
253 253 raise logerror(match.group(2))
254 254 if re_03.match(line):
255 255 raise logerror(line)
256 256
257 257 elif state == 1:
258 258 # expect 'Working file' (only when using log instead of rlog)
259 259 match = re_10.match(line)
260 260 assert match, _('RCS file must be followed by working file')
261 261 filename = util.normpath(match.group(1))
262 262 state = 2
263 263
264 264 elif state == 2:
265 265 # expect 'symbolic names'
266 266 if re_20.match(line):
267 267 branchmap = {}
268 268 state = 3
269 269
270 270 elif state == 3:
271 271 # read the symbolic names and store as tags
272 272 match = re_30.match(line)
273 273 if match:
274 274 rev = [int(x) for x in match.group(2).split('.')]
275 275
276 276 # Convert magic branch number to an odd-numbered one
277 277 revn = len(rev)
278 278 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
279 279 rev = rev[:-2] + rev[-1:]
280 280 rev = tuple(rev)
281 281
282 282 if rev not in tags:
283 283 tags[rev] = []
284 284 tags[rev].append(match.group(1))
285 285 branchmap[match.group(1)] = match.group(2)
286 286
287 287 elif re_31.match(line):
288 288 state = 5
289 289 elif re_32.match(line):
290 290 state = 0
291 291
292 292 elif state == 4:
293 293 # expecting '------' separator before first revision
294 294 if re_31.match(line):
295 295 state = 5
296 296 else:
297 297 assert not re_32.match(line), _('must have at least '
298 298 'some revisions')
299 299
300 300 elif state == 5:
301 301 # expecting revision number and possibly (ignored) lock indication
302 302 # we create the logentry here from values stored in states 0 to 4,
303 303 # as this state is re-entered for subsequent revisions of a file.
304 304 match = re_50.match(line)
305 305 assert match, _('expected revision number')
306 306 e = logentry(rcs=scache(rcs),
307 307 file=scache(filename),
308 308 revision=tuple([int(x) for x in
309 309 match.group(1).split('.')]),
310 310 branches=[],
311 311 parent=None,
312 312 commitid=None,
313 313 mergepoint=None,
314 314 branchpoints=set())
315 315
316 316 state = 6
317 317
318 318 elif state == 6:
319 319 # expecting date, author, state, lines changed
320 320 match = re_60.match(line)
321 321 assert match, _('revision must be followed by date line')
322 322 d = match.group(1)
323 323 if d[2] == '/':
324 324 # Y2K
325 325 d = '19' + d
326 326
327 327 if len(d.split()) != 3:
328 328 # cvs log dates always in GMT
329 329 d = d + ' UTC'
330 330 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
331 331 '%Y/%m/%d %H:%M:%S',
332 332 '%Y-%m-%d %H:%M:%S'])
333 333 e.author = scache(match.group(2))
334 334 e.dead = match.group(3).lower() == 'dead'
335 335
336 336 if match.group(5):
337 337 if match.group(6):
338 338 e.lines = (int(match.group(5)), int(match.group(6)))
339 339 else:
340 340 e.lines = (int(match.group(5)), 0)
341 341 elif match.group(6):
342 342 e.lines = (0, int(match.group(6)))
343 343 else:
344 344 e.lines = None
345 345
346 346 if match.group(7): # cvs 1.12 commitid
347 347 e.commitid = match.group(8)
348 348
349 349 if match.group(9): # cvsnt mergepoint
350 350 myrev = match.group(10).split('.')
351 351 if len(myrev) == 2: # head
352 352 e.mergepoint = 'HEAD'
353 353 else:
354 354 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
355 355 branches = [b for b in branchmap if branchmap[b] == myrev]
356 356 assert len(branches) == 1, ('unknown branch: %s'
357 357 % e.mergepoint)
358 358 e.mergepoint = branches[0]
359 359
360 360 e.comment = []
361 361 state = 7
362 362
363 363 elif state == 7:
364 364 # read the revision numbers of branches that start at this revision
365 365 # or store the commit log message otherwise
366 366 m = re_70.match(line)
367 367 if m:
368 368 e.branches = [tuple([int(y) for y in x.strip().split('.')])
369 369 for x in m.group(1).split(';')]
370 370 state = 8
371 371 elif re_31.match(line) and re_50.match(peek):
372 372 state = 5
373 373 store = True
374 374 elif re_32.match(line):
375 375 state = 0
376 376 store = True
377 377 else:
378 378 e.comment.append(line)
379 379
380 380 elif state == 8:
381 381 # store commit log message
382 382 if re_31.match(line):
383 383 cpeek = peek
384 384 if cpeek.endswith('\n'):
385 385 cpeek = cpeek[:-1]
386 386 if re_50.match(cpeek):
387 387 state = 5
388 388 store = True
389 389 else:
390 390 e.comment.append(line)
391 391 elif re_32.match(line):
392 392 state = 0
393 393 store = True
394 394 else:
395 395 e.comment.append(line)
396 396
397 397 # When a file is added on a branch B1, CVS creates a synthetic
398 398 # dead trunk revision 1.1 so that the branch has a root.
399 399 # Likewise, if you merge such a file to a later branch B2 (one
400 400 # that already existed when the file was added on B1), CVS
401 401 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
402 402 # these revisions now, but mark them synthetic so
403 403 # createchangeset() can take care of them.
404 404 if (store and
405 405 e.dead and
406 406 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
407 407 len(e.comment) == 1 and
408 408 file_added_re.match(e.comment[0])):
409 409 ui.debug('found synthetic revision in %s: %r\n'
410 410 % (e.rcs, e.comment[0]))
411 411 e.synthetic = True
412 412
413 413 if store:
414 414 # clean up the results and save in the log.
415 415 store = False
416 416 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
417 417 e.comment = scache('\n'.join(e.comment))
418 418
419 419 revn = len(e.revision)
420 420 if revn > 3 and (revn % 2) == 0:
421 421 e.branch = tags.get(e.revision[:-1], [None])[0]
422 422 else:
423 423 e.branch = None
424 424
425 425 # find the branches starting from this revision
426 426 branchpoints = set()
427 427 for branch, revision in branchmap.iteritems():
428 428 revparts = tuple([int(i) for i in revision.split('.')])
429 429 if len(revparts) < 2: # bad tags
430 430 continue
431 431 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
432 432 # normal branch
433 433 if revparts[:-2] == e.revision:
434 434 branchpoints.add(branch)
435 435 elif revparts == (1, 1, 1): # vendor branch
436 436 if revparts in e.branches:
437 437 branchpoints.add(branch)
438 438 e.branchpoints = branchpoints
439 439
440 440 log.append(e)
441 441
442 442 if len(log) % 100 == 0:
443 443 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
444 444
445 445 log.sort(key=lambda x: (x.rcs, x.revision))
446 446
447 447 # find parent revisions of individual files
448 448 versions = {}
449 449 for e in log:
450 450 branch = e.revision[:-1]
451 451 p = versions.get((e.rcs, branch), None)
452 452 if p is None:
453 453 p = e.revision[:-2]
454 454 e.parent = p
455 455 versions[(e.rcs, branch)] = e.revision
456 456
457 457 # update the log cache
458 458 if cache:
459 459 if log:
460 460 # join up the old and new logs
461 461 log.sort(key=lambda x: x.date)
462 462
463 463 if oldlog and oldlog[-1].date >= log[0].date:
464 464 raise logerror(_('log cache overlaps with new log entries,'
465 465 ' re-run without cache.'))
466 466
467 467 log = oldlog + log
468 468
469 469 # write the new cachefile
470 470 ui.note(_('writing cvs log cache %s\n') % cachefile)
471 471 pickle.dump(log, open(cachefile, 'w'))
472 472 else:
473 473 log = oldlog
474 474
475 475 ui.status(_('%d log entries\n') % len(log))
476 476
477 477 hook.hook(ui, None, "cvslog", True, log=log)
478 478
479 479 return log
480 480
481 481
482 482 class changeset(object):
483 483 '''Class changeset has the following attributes:
484 484 .id - integer identifying this changeset (list index)
485 485 .author - author name as CVS knows it
486 486 .branch - name of branch this changeset is on, or None
487 487 .comment - commit message
488 488 .commitid - CVS commitid or None
489 489 .date - the commit date as a (time,tz) tuple
490 490 .entries - list of logentry objects in this changeset
491 491 .parents - list of one or two parent changesets
492 492 .tags - list of tags on this changeset
493 493 .synthetic - from synthetic revision "file ... added on branch ..."
494 494 .mergepoint- the branch that has been merged from or None
495 495 .branchpoints- the branches that start at the current entry or empty
496 496 '''
497 497 def __init__(self, **entries):
498 498 self.id = None
499 499 self.synthetic = False
500 500 self.__dict__.update(entries)
501 501
502 502 def __repr__(self):
503 503 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
504 504 return "%s(%s)"%(type(self).__name__, ", ".join(items))
505 505
506 506 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
507 507 '''Convert log into changesets.'''
508 508
509 509 ui.status(_('creating changesets\n'))
510 510
511 511 # try to order commitids by date
512 512 mindate = {}
513 513 for e in log:
514 514 if e.commitid:
515 515 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
516 516
517 517 # Merge changesets
518 518 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
519 519 x.author, x.branch, x.date, x.branchpoints))
520 520
521 521 changesets = []
522 522 files = set()
523 523 c = None
524 524 for i, e in enumerate(log):
525 525
526 526 # Check if log entry belongs to the current changeset or not.
527 527
528 528 # Since CVS is file-centric, two different file revisions with
529 529 # different branchpoints should be treated as belonging to two
530 530 # different changesets (and the ordering is important and not
531 531 # honoured by cvsps at this point).
532 532 #
533 533 # Consider the following case:
534 534 # foo 1.1 branchpoints: [MYBRANCH]
535 535 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
536 536 #
537 537 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
538 538 # later version of foo may be in MYBRANCH2, so foo should be the
539 539 # first changeset and bar the next and MYBRANCH and MYBRANCH2
540 540 # should both start off of the bar changeset. No provisions are
541 541 # made to ensure that this is, in fact, what happens.
542 542 if not (c and e.branchpoints == c.branchpoints and
543 543 (# cvs commitids
544 544 (e.commitid is not None and e.commitid == c.commitid) or
545 545 (# no commitids, use fuzzy commit detection
546 546 (e.commitid is None or c.commitid is None) and
547 547 e.comment == c.comment and
548 548 e.author == c.author and
549 549 e.branch == c.branch and
550 550 ((c.date[0] + c.date[1]) <=
551 551 (e.date[0] + e.date[1]) <=
552 552 (c.date[0] + c.date[1]) + fuzz) and
553 553 e.file not in files))):
554 554 c = changeset(comment=e.comment, author=e.author,
555 555 branch=e.branch, date=e.date,
556 556 entries=[], mergepoint=e.mergepoint,
557 557 branchpoints=e.branchpoints, commitid=e.commitid)
558 558 changesets.append(c)
559 559
560 560 files = set()
561 561 if len(changesets) % 100 == 0:
562 562 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
563 563 ui.status(util.ellipsis(t, 80) + '\n')
564 564
565 565 c.entries.append(e)
566 566 files.add(e.file)
567 567 c.date = e.date # changeset date is date of latest commit in it
568 568
569 569 # Mark synthetic changesets
570 570
571 571 for c in changesets:
572 572 # Synthetic revisions always get their own changeset, because
573 573 # the log message includes the filename. E.g. if you add file3
574 574 # and file4 on a branch, you get four log entries and three
575 575 # changesets:
576 576 # "File file3 was added on branch ..." (synthetic, 1 entry)
577 577 # "File file4 was added on branch ..." (synthetic, 1 entry)
578 578 # "Add file3 and file4 to fix ..." (real, 2 entries)
579 579 # Hence the check for 1 entry here.
580 580 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
581 581
582 582 # Sort files in each changeset
583 583
584 584 def entitycompare(l, r):
585 585 'Mimic cvsps sorting order'
586 586 l = l.file.split('/')
587 587 r = r.file.split('/')
588 588 nl = len(l)
589 589 nr = len(r)
590 590 n = min(nl, nr)
591 591 for i in range(n):
592 592 if i + 1 == nl and nl < nr:
593 593 return -1
594 594 elif i + 1 == nr and nl > nr:
595 595 return +1
596 596 elif l[i] < r[i]:
597 597 return -1
598 598 elif l[i] > r[i]:
599 599 return +1
600 600 return 0
601 601
602 602 for c in changesets:
603 603 c.entries.sort(entitycompare)
604 604
605 605 # Sort changesets by date
606 606
607 607 odd = set()
608 608 def cscmp(l, r, odd=odd):
609 609 d = sum(l.date) - sum(r.date)
610 610 if d:
611 611 return d
612 612
613 613 # detect vendor branches and initial commits on a branch
614 614 le = {}
615 615 for e in l.entries:
616 616 le[e.rcs] = e.revision
617 617 re = {}
618 618 for e in r.entries:
619 619 re[e.rcs] = e.revision
620 620
621 621 d = 0
622 622 for e in l.entries:
623 623 if re.get(e.rcs, None) == e.parent:
624 624 assert not d
625 625 d = 1
626 626 break
627 627
628 628 for e in r.entries:
629 629 if le.get(e.rcs, None) == e.parent:
630 630 if d:
631 631 odd.add((l, r))
632 632 d = -1
633 633 break
634 634 # By this point, the changesets are sufficiently compared that
635 635 # we don't really care about ordering. However, this leaves
636 636 # some race conditions in the tests, so we compare on the
637 637 # number of files modified, the files contained in each
638 638 # changeset, and the branchpoints in the change to ensure test
639 639 # output remains stable.
640 640
641 641 # recommended replacement for cmp from
642 642 # https://docs.python.org/3.0/whatsnew/3.0.html
643 643 c = lambda x, y: (x > y) - (x < y)
644 644 # Sort bigger changes first.
645 645 if not d:
646 646 d = c(len(l.entries), len(r.entries))
647 647 # Try sorting by filename in the change.
648 648 if not d:
649 649 d = c([e.file for e in l.entries], [e.file for e in r.entries])
650 650 # Try and put changes without a branch point before ones with
651 651 # a branch point.
652 652 if not d:
653 653 d = c(len(l.branchpoints), len(r.branchpoints))
654 654 return d
655 655
656 656 changesets.sort(cscmp)
657 657
658 658 # Collect tags
659 659
660 660 globaltags = {}
661 661 for c in changesets:
662 662 for e in c.entries:
663 663 for tag in e.tags:
664 664 # remember which is the latest changeset to have this tag
665 665 globaltags[tag] = c
666 666
667 667 for c in changesets:
668 668 tags = set()
669 669 for e in c.entries:
670 670 tags.update(e.tags)
671 671 # remember tags only if this is the latest changeset to have it
672 672 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
673 673
674 674 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
675 675 # by inserting dummy changesets with two parents, and handle
676 676 # {{mergefrombranch BRANCHNAME}} by setting two parents.
677 677
678 678 if mergeto is None:
679 679 mergeto = r'{{mergetobranch ([-\w]+)}}'
680 680 if mergeto:
681 681 mergeto = re.compile(mergeto)
682 682
683 683 if mergefrom is None:
684 684 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
685 685 if mergefrom:
686 686 mergefrom = re.compile(mergefrom)
687 687
688 688 versions = {} # changeset index where we saw any particular file version
689 689 branches = {} # changeset index where we saw a branch
690 690 n = len(changesets)
691 691 i = 0
692 692 while i < n:
693 693 c = changesets[i]
694 694
695 695 for f in c.entries:
696 696 versions[(f.rcs, f.revision)] = i
697 697
698 698 p = None
699 699 if c.branch in branches:
700 700 p = branches[c.branch]
701 701 else:
702 702 # first changeset on a new branch
703 703 # the parent is a changeset with the branch in its
704 704 # branchpoints such that it is the latest possible
705 705 # commit without any intervening, unrelated commits.
706 706
707 707 for candidate in xrange(i):
708 708 if c.branch not in changesets[candidate].branchpoints:
709 709 if p is not None:
710 710 break
711 711 continue
712 712 p = candidate
713 713
714 714 c.parents = []
715 715 if p is not None:
716 716 p = changesets[p]
717 717
718 718 # Ensure no changeset has a synthetic changeset as a parent.
719 719 while p.synthetic:
720 720 assert len(p.parents) <= 1, \
721 721 _('synthetic changeset cannot have multiple parents')
722 722 if p.parents:
723 723 p = p.parents[0]
724 724 else:
725 725 p = None
726 726 break
727 727
728 728 if p is not None:
729 729 c.parents.append(p)
730 730
731 731 if c.mergepoint:
732 732 if c.mergepoint == 'HEAD':
733 733 c.mergepoint = None
734 734 c.parents.append(changesets[branches[c.mergepoint]])
735 735
736 736 if mergefrom:
737 737 m = mergefrom.search(c.comment)
738 738 if m:
739 739 m = m.group(1)
740 740 if m == 'HEAD':
741 741 m = None
742 742 try:
743 743 candidate = changesets[branches[m]]
744 744 except KeyError:
745 745 ui.warn(_("warning: CVS commit message references "
746 746 "non-existent branch %r:\n%s\n")
747 747 % (m, c.comment))
748 748 if m in branches and c.branch != m and not candidate.synthetic:
749 749 c.parents.append(candidate)
750 750
751 751 if mergeto:
752 752 m = mergeto.search(c.comment)
753 753 if m:
754 754 if m.groups():
755 755 m = m.group(1)
756 756 if m == 'HEAD':
757 757 m = None
758 758 else:
759 759 m = None # if no group found then merge to HEAD
760 760 if m in branches and c.branch != m:
761 761 # insert empty changeset for merge
762 762 cc = changeset(
763 763 author=c.author, branch=m, date=c.date,
764 764 comment='convert-repo: CVS merge from branch %s'
765 765 % c.branch,
766 766 entries=[], tags=[],
767 767 parents=[changesets[branches[m]], c])
768 768 changesets.insert(i + 1, cc)
769 769 branches[m] = i + 1
770 770
771 771 # adjust our loop counters now we have inserted a new entry
772 772 n += 1
773 773 i += 2
774 774 continue
775 775
776 776 branches[c.branch] = i
777 777 i += 1
778 778
779 779 # Drop synthetic changesets (safe now that we have ensured no other
780 780 # changesets can have them as parents).
781 781 i = 0
782 782 while i < len(changesets):
783 783 if changesets[i].synthetic:
784 784 del changesets[i]
785 785 else:
786 786 i += 1
787 787
788 788 # Number changesets
789 789
790 790 for i, c in enumerate(changesets):
791 791 c.id = i + 1
792 792
793 793 if odd:
794 794 for l, r in odd:
795 795 if l.id is not None and r.id is not None:
796 796 ui.warn(_('changeset %d is both before and after %d\n')
797 797 % (l.id, r.id))
798 798
799 799 ui.status(_('%d changeset entries\n') % len(changesets))
800 800
801 801 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
802 802
803 803 return changesets
804 804
805 805
806 806 def debugcvsps(ui, *args, **opts):
807 807 '''Read CVS rlog for current directory or named path in
808 808 repository, and convert the log to changesets based on matching
809 809 commit log entries and dates.
810 810 '''
811 811 if opts["new_cache"]:
812 812 cache = "write"
813 813 elif opts["update_cache"]:
814 814 cache = "update"
815 815 else:
816 816 cache = None
817 817
818 818 revisions = opts["revisions"]
819 819
820 820 try:
821 821 if args:
822 822 log = []
823 823 for d in args:
824 824 log += createlog(ui, d, root=opts["root"], cache=cache)
825 825 else:
826 826 log = createlog(ui, root=opts["root"], cache=cache)
827 except logerror, e:
827 except logerror as e:
828 828 ui.write("%r\n"%e)
829 829 return
830 830
831 831 changesets = createchangeset(ui, log, opts["fuzz"])
832 832 del log
833 833
834 834 # Print changesets (optionally filtered)
835 835
836 836 off = len(revisions)
837 837 branches = {} # latest version number in each branch
838 838 ancestors = {} # parent branch
839 839 for cs in changesets:
840 840
841 841 if opts["ancestors"]:
842 842 if cs.branch not in branches and cs.parents and cs.parents[0].id:
843 843 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
844 844 cs.parents[0].id)
845 845 branches[cs.branch] = cs.id
846 846
847 847 # limit by branches
848 848 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
849 849 continue
850 850
851 851 if not off:
852 852 # Note: trailing spaces on several lines here are needed to have
853 853 # bug-for-bug compatibility with cvsps.
854 854 ui.write('---------------------\n')
855 855 ui.write(('PatchSet %d \n' % cs.id))
856 856 ui.write(('Date: %s\n' % util.datestr(cs.date,
857 857 '%Y/%m/%d %H:%M:%S %1%2')))
858 858 ui.write(('Author: %s\n' % cs.author))
859 859 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
860 860 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
861 861 ','.join(cs.tags) or '(none)')))
862 862 if cs.branchpoints:
863 863 ui.write(('Branchpoints: %s \n') %
864 864 ', '.join(sorted(cs.branchpoints)))
865 865 if opts["parents"] and cs.parents:
866 866 if len(cs.parents) > 1:
867 867 ui.write(('Parents: %s\n' %
868 868 (','.join([str(p.id) for p in cs.parents]))))
869 869 else:
870 870 ui.write(('Parent: %d\n' % cs.parents[0].id))
871 871
872 872 if opts["ancestors"]:
873 873 b = cs.branch
874 874 r = []
875 875 while b:
876 876 b, c = ancestors[b]
877 877 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
878 878 if r:
879 879 ui.write(('Ancestors: %s\n' % (','.join(r))))
880 880
881 881 ui.write(('Log:\n'))
882 882 ui.write('%s\n\n' % cs.comment)
883 883 ui.write(('Members: \n'))
884 884 for f in cs.entries:
885 885 fn = f.file
886 886 if fn.startswith(opts["prefix"]):
887 887 fn = fn[len(opts["prefix"]):]
888 888 ui.write('\t%s:%s->%s%s \n' % (
889 889 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
890 890 '.'.join([str(x) for x in f.revision]),
891 891 ['', '(DEAD)'][f.dead]))
892 892 ui.write('\n')
893 893
894 894 # have we seen the start tag?
895 895 if revisions and off:
896 896 if revisions[0] == str(cs.id) or \
897 897 revisions[0] in cs.tags:
898 898 off = False
899 899
900 900 # see if we reached the end tag
901 901 if len(revisions) > 1 and not off:
902 902 if revisions[1] == str(cs.id) or \
903 903 revisions[1] in cs.tags:
904 904 break
@@ -1,208 +1,208 b''
1 1 # darcs.py - darcs support for the convert extension
2 2 #
3 3 # Copyright 2007-2009 Matt Mackall <mpm@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
8 8 from common import NoRepo, checktool, commandline, commit, converter_source
9 9 from mercurial.i18n import _
10 10 from mercurial import util
11 11 import os, shutil, tempfile, re, errno
12 12
13 13 # The naming drift of ElementTree is fun!
14 14
15 15 try:
16 16 from xml.etree.cElementTree import ElementTree, XMLParser
17 17 except ImportError:
18 18 try:
19 19 from xml.etree.ElementTree import ElementTree, XMLParser
20 20 except ImportError:
21 21 try:
22 22 from elementtree.cElementTree import ElementTree, XMLParser
23 23 except ImportError:
24 24 try:
25 25 from elementtree.ElementTree import ElementTree, XMLParser
26 26 except ImportError:
27 27 pass
28 28
29 29 class darcs_source(converter_source, commandline):
30 30 def __init__(self, ui, path, rev=None):
31 31 converter_source.__init__(self, ui, path, rev=rev)
32 32 commandline.__init__(self, ui, 'darcs')
33 33
34 34 # check for _darcs, ElementTree so that we can easily skip
35 35 # test-convert-darcs if ElementTree is not around
36 36 if not os.path.exists(os.path.join(path, '_darcs')):
37 37 raise NoRepo(_("%s does not look like a darcs repository") % path)
38 38
39 39 checktool('darcs')
40 40 version = self.run0('--version').splitlines()[0].strip()
41 41 if version < '2.1':
42 42 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
43 43 version)
44 44
45 45 if "ElementTree" not in globals():
46 46 raise util.Abort(_("Python ElementTree module is not available"))
47 47
48 48 self.path = os.path.realpath(path)
49 49
50 50 self.lastrev = None
51 51 self.changes = {}
52 52 self.parents = {}
53 53 self.tags = {}
54 54
55 55 # Check darcs repository format
56 56 format = self.format()
57 57 if format:
58 58 if format in ('darcs-1.0', 'hashed'):
59 59 raise NoRepo(_("%s repository format is unsupported, "
60 60 "please upgrade") % format)
61 61 else:
62 62 self.ui.warn(_('failed to detect repository format!'))
63 63
64 64 def before(self):
65 65 self.tmppath = tempfile.mkdtemp(
66 66 prefix='convert-' + os.path.basename(self.path) + '-')
67 67 output, status = self.run('init', repodir=self.tmppath)
68 68 self.checkexit(status)
69 69
70 70 tree = self.xml('changes', xml_output=True, summary=True,
71 71 repodir=self.path)
72 72 tagname = None
73 73 child = None
74 74 for elt in tree.findall('patch'):
75 75 node = elt.get('hash')
76 76 name = elt.findtext('name', '')
77 77 if name.startswith('TAG '):
78 78 tagname = name[4:].strip()
79 79 elif tagname is not None:
80 80 self.tags[tagname] = node
81 81 tagname = None
82 82 self.changes[node] = elt
83 83 self.parents[child] = [node]
84 84 child = node
85 85 self.parents[child] = []
86 86
87 87 def after(self):
88 88 self.ui.debug('cleaning up %s\n' % self.tmppath)
89 89 shutil.rmtree(self.tmppath, ignore_errors=True)
90 90
91 91 def recode(self, s, encoding=None):
92 92 if isinstance(s, unicode):
93 93 # XMLParser returns unicode objects for anything it can't
94 94 # encode into ASCII. We convert them back to str to get
95 95 # recode's normal conversion behavior.
96 96 s = s.encode('latin-1')
97 97 return super(darcs_source, self).recode(s, encoding)
98 98
99 99 def xml(self, cmd, **kwargs):
100 100 # NOTE: darcs is currently encoding agnostic and will print
101 101 # patch metadata byte-for-byte, even in the XML changelog.
102 102 etree = ElementTree()
103 103 # While we are decoding the XML as latin-1 to be as liberal as
104 104 # possible, etree will still raise an exception if any
105 105 # non-printable characters are in the XML changelog.
106 106 parser = XMLParser(encoding='latin-1')
107 107 p = self._run(cmd, **kwargs)
108 108 etree.parse(p.stdout, parser=parser)
109 109 p.wait()
110 110 self.checkexit(p.returncode)
111 111 return etree.getroot()
112 112
113 113 def format(self):
114 114 output, status = self.run('show', 'repo', no_files=True,
115 115 repodir=self.path)
116 116 self.checkexit(status)
117 117 m = re.search(r'^\s*Format:\s*(.*)$', output, re.MULTILINE)
118 118 if not m:
119 119 return None
120 120 return ','.join(sorted(f.strip() for f in m.group(1).split(',')))
121 121
122 122 def manifest(self):
123 123 man = []
124 124 output, status = self.run('show', 'files', no_directories=True,
125 125 repodir=self.tmppath)
126 126 self.checkexit(status)
127 127 for line in output.split('\n'):
128 128 path = line[2:]
129 129 if path:
130 130 man.append(path)
131 131 return man
132 132
133 133 def getheads(self):
134 134 return self.parents[None]
135 135
136 136 def getcommit(self, rev):
137 137 elt = self.changes[rev]
138 138 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
139 139 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
140 140 # etree can return unicode objects for name, comment, and author,
141 141 # so recode() is used to ensure str objects are emitted.
142 142 return commit(author=self.recode(elt.get('author')),
143 143 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
144 144 desc=self.recode(desc).strip(),
145 145 parents=self.parents[rev])
146 146
147 147 def pull(self, rev):
148 148 output, status = self.run('pull', self.path, all=True,
149 149 match='hash %s' % rev,
150 150 no_test=True, no_posthook=True,
151 151 external_merge='/bin/false',
152 152 repodir=self.tmppath)
153 153 if status:
154 154 if output.find('We have conflicts in') == -1:
155 155 self.checkexit(status, output)
156 156 output, status = self.run('revert', all=True, repodir=self.tmppath)
157 157 self.checkexit(status, output)
158 158
159 159 def getchanges(self, rev, full):
160 160 if full:
161 161 raise util.Abort(_("convert from darcs do not support --full"))
162 162 copies = {}
163 163 changes = []
164 164 man = None
165 165 for elt in self.changes[rev].find('summary').getchildren():
166 166 if elt.tag in ('add_directory', 'remove_directory'):
167 167 continue
168 168 if elt.tag == 'move':
169 169 if man is None:
170 170 man = self.manifest()
171 171 source, dest = elt.get('from'), elt.get('to')
172 172 if source in man:
173 173 # File move
174 174 changes.append((source, rev))
175 175 changes.append((dest, rev))
176 176 copies[dest] = source
177 177 else:
178 178 # Directory move, deduce file moves from manifest
179 179 source = source + '/'
180 180 for f in man:
181 181 if not f.startswith(source):
182 182 continue
183 183 fdest = dest + '/' + f[len(source):]
184 184 changes.append((f, rev))
185 185 changes.append((fdest, rev))
186 186 copies[fdest] = f
187 187 else:
188 188 changes.append((elt.text.strip(), rev))
189 189 self.pull(rev)
190 190 self.lastrev = rev
191 191 return sorted(changes), copies, set()
192 192
193 193 def getfile(self, name, rev):
194 194 if rev != self.lastrev:
195 195 raise util.Abort(_('internal calling inconsistency'))
196 196 path = os.path.join(self.tmppath, name)
197 197 try:
198 198 data = util.readfile(path)
199 199 mode = os.lstat(path).st_mode
200 except IOError, inst:
200 except IOError as inst:
201 201 if inst.errno == errno.ENOENT:
202 202 return None, None
203 203 raise
204 204 mode = (mode & 0o111) and 'x' or ''
205 205 return data, mode
206 206
207 207 def gettags(self):
208 208 return self.tags
@@ -1,561 +1,561 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@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
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, bookmarks, error, scmutil, exchange
25 25 from mercurial import phases
26 26
27 27 from common import NoRepo, commit, converter_source, converter_sink, mapfile
28 28
29 29 import re
30 30 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
31 31
32 32 class mercurial_sink(converter_sink):
33 33 def __init__(self, ui, path):
34 34 converter_sink.__init__(self, ui, path)
35 35 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
36 36 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
37 37 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
38 38 self.lastbranch = None
39 39 if os.path.isdir(path) and len(os.listdir(path)) > 0:
40 40 try:
41 41 self.repo = hg.repository(self.ui, path)
42 42 if not self.repo.local():
43 43 raise NoRepo(_('%s is not a local Mercurial repository')
44 44 % path)
45 except error.RepoError, err:
45 except error.RepoError as err:
46 46 ui.traceback()
47 47 raise NoRepo(err.args[0])
48 48 else:
49 49 try:
50 50 ui.status(_('initializing destination %s repository\n') % path)
51 51 self.repo = hg.repository(self.ui, path, create=True)
52 52 if not self.repo.local():
53 53 raise NoRepo(_('%s is not a local Mercurial repository')
54 54 % path)
55 55 self.created.append(path)
56 56 except error.RepoError:
57 57 ui.traceback()
58 58 raise NoRepo(_("could not create hg repository %s as sink")
59 59 % path)
60 60 self.lock = None
61 61 self.wlock = None
62 62 self.filemapmode = False
63 63 self.subrevmaps = {}
64 64
65 65 def before(self):
66 66 self.ui.debug('run hg sink pre-conversion action\n')
67 67 self.wlock = self.repo.wlock()
68 68 self.lock = self.repo.lock()
69 69
70 70 def after(self):
71 71 self.ui.debug('run hg sink post-conversion action\n')
72 72 if self.lock:
73 73 self.lock.release()
74 74 if self.wlock:
75 75 self.wlock.release()
76 76
77 77 def revmapfile(self):
78 78 return self.repo.join("shamap")
79 79
80 80 def authorfile(self):
81 81 return self.repo.join("authormap")
82 82
83 83 def setbranch(self, branch, pbranches):
84 84 if not self.clonebranches:
85 85 return
86 86
87 87 setbranch = (branch != self.lastbranch)
88 88 self.lastbranch = branch
89 89 if not branch:
90 90 branch = 'default'
91 91 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
92 92 if pbranches:
93 93 pbranch = pbranches[0][1]
94 94 else:
95 95 pbranch = 'default'
96 96
97 97 branchpath = os.path.join(self.path, branch)
98 98 if setbranch:
99 99 self.after()
100 100 try:
101 101 self.repo = hg.repository(self.ui, branchpath)
102 102 except Exception:
103 103 self.repo = hg.repository(self.ui, branchpath, create=True)
104 104 self.before()
105 105
106 106 # pbranches may bring revisions from other branches (merge parents)
107 107 # Make sure we have them, or pull them.
108 108 missings = {}
109 109 for b in pbranches:
110 110 try:
111 111 self.repo.lookup(b[0])
112 112 except Exception:
113 113 missings.setdefault(b[1], []).append(b[0])
114 114
115 115 if missings:
116 116 self.after()
117 117 for pbranch, heads in sorted(missings.iteritems()):
118 118 pbranchpath = os.path.join(self.path, pbranch)
119 119 prepo = hg.peer(self.ui, {}, pbranchpath)
120 120 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
121 121 exchange.pull(self.repo, prepo,
122 122 [prepo.lookup(h) for h in heads])
123 123 self.before()
124 124
125 125 def _rewritetags(self, source, revmap, data):
126 126 fp = cStringIO.StringIO()
127 127 for line in data.splitlines():
128 128 s = line.split(' ', 1)
129 129 if len(s) != 2:
130 130 continue
131 131 revid = revmap.get(source.lookuprev(s[0]))
132 132 if not revid:
133 133 if s[0] == hex(nullid):
134 134 revid = s[0]
135 135 else:
136 136 continue
137 137 fp.write('%s %s\n' % (revid, s[1]))
138 138 return fp.getvalue()
139 139
140 140 def _rewritesubstate(self, source, data):
141 141 fp = cStringIO.StringIO()
142 142 for line in data.splitlines():
143 143 s = line.split(' ', 1)
144 144 if len(s) != 2:
145 145 continue
146 146
147 147 revid = s[0]
148 148 subpath = s[1]
149 149 if revid != hex(nullid):
150 150 revmap = self.subrevmaps.get(subpath)
151 151 if revmap is None:
152 152 revmap = mapfile(self.ui,
153 153 self.repo.wjoin(subpath, '.hg/shamap'))
154 154 self.subrevmaps[subpath] = revmap
155 155
156 156 # It is reasonable that one or more of the subrepos don't
157 157 # need to be converted, in which case they can be cloned
158 158 # into place instead of converted. Therefore, only warn
159 159 # once.
160 160 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
161 161 if len(revmap) == 0:
162 162 sub = self.repo.wvfs.reljoin(subpath, '.hg')
163 163
164 164 if self.repo.wvfs.exists(sub):
165 165 self.ui.warn(msg % subpath)
166 166
167 167 newid = revmap.get(revid)
168 168 if not newid:
169 169 if len(revmap) > 0:
170 170 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
171 171 (revid, subpath))
172 172 else:
173 173 revid = newid
174 174
175 175 fp.write('%s %s\n' % (revid, subpath))
176 176
177 177 return fp.getvalue()
178 178
179 179 def putcommit(self, files, copies, parents, commit, source, revmap, full,
180 180 cleanp2):
181 181 files = dict(files)
182 182
183 183 def getfilectx(repo, memctx, f):
184 184 if p2ctx and f in cleanp2 and f not in copies:
185 185 self.ui.debug('reusing %s from p2\n' % f)
186 186 return p2ctx[f]
187 187 try:
188 188 v = files[f]
189 189 except KeyError:
190 190 return None
191 191 data, mode = source.getfile(f, v)
192 192 if data is None:
193 193 return None
194 194 if f == '.hgtags':
195 195 data = self._rewritetags(source, revmap, data)
196 196 if f == '.hgsubstate':
197 197 data = self._rewritesubstate(source, data)
198 198 return context.memfilectx(self.repo, f, data, 'l' in mode,
199 199 'x' in mode, copies.get(f))
200 200
201 201 pl = []
202 202 for p in parents:
203 203 if p not in pl:
204 204 pl.append(p)
205 205 parents = pl
206 206 nparents = len(parents)
207 207 if self.filemapmode and nparents == 1:
208 208 m1node = self.repo.changelog.read(bin(parents[0]))[0]
209 209 parent = parents[0]
210 210
211 211 if len(parents) < 2:
212 212 parents.append(nullid)
213 213 if len(parents) < 2:
214 214 parents.append(nullid)
215 215 p2 = parents.pop(0)
216 216
217 217 text = commit.desc
218 218
219 219 sha1s = re.findall(sha1re, text)
220 220 for sha1 in sha1s:
221 221 oldrev = source.lookuprev(sha1)
222 222 newrev = revmap.get(oldrev)
223 223 if newrev is not None:
224 224 text = text.replace(sha1, newrev[:len(sha1)])
225 225
226 226 extra = commit.extra.copy()
227 227
228 228 for label in ('source', 'transplant_source', 'rebase_source',
229 229 'intermediate-source'):
230 230 node = extra.get(label)
231 231
232 232 if node is None:
233 233 continue
234 234
235 235 # Only transplant stores its reference in binary
236 236 if label == 'transplant_source':
237 237 node = hex(node)
238 238
239 239 newrev = revmap.get(node)
240 240 if newrev is not None:
241 241 if label == 'transplant_source':
242 242 newrev = bin(newrev)
243 243
244 244 extra[label] = newrev
245 245
246 246 if self.branchnames and commit.branch:
247 247 extra['branch'] = commit.branch
248 248 if commit.rev and commit.saverev:
249 249 extra['convert_revision'] = commit.rev
250 250
251 251 while parents:
252 252 p1 = p2
253 253 p2 = parents.pop(0)
254 254 p2ctx = None
255 255 if p2 != nullid:
256 256 p2ctx = self.repo[p2]
257 257 fileset = set(files)
258 258 if full:
259 259 fileset.update(self.repo[p1])
260 260 fileset.update(self.repo[p2])
261 261 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
262 262 getfilectx, commit.author, commit.date, extra)
263 263
264 264 # We won't know if the conversion changes the node until after the
265 265 # commit, so copy the source's phase for now.
266 266 self.repo.ui.setconfig('phases', 'new-commit',
267 267 phases.phasenames[commit.phase], 'convert')
268 268
269 269 tr = self.repo.transaction("convert")
270 270
271 271 try:
272 272 node = hex(self.repo.commitctx(ctx))
273 273
274 274 # If the node value has changed, but the phase is lower than
275 275 # draft, set it back to draft since it hasn't been exposed
276 276 # anywhere.
277 277 if commit.rev != node:
278 278 ctx = self.repo[node]
279 279 if ctx.phase() < phases.draft:
280 280 phases.retractboundary(self.repo, tr, phases.draft,
281 281 [ctx.node()])
282 282 tr.close()
283 283 finally:
284 284 tr.release()
285 285
286 286 text = "(octopus merge fixup)\n"
287 287 p2 = hex(self.repo.changelog.tip())
288 288
289 289 if self.filemapmode and nparents == 1:
290 290 man = self.repo.manifest
291 291 mnode = self.repo.changelog.read(bin(p2))[0]
292 292 closed = 'close' in commit.extra
293 293 if not closed and not man.cmp(m1node, man.revision(mnode)):
294 294 self.ui.status(_("filtering out empty revision\n"))
295 295 self.repo.rollback(force=True)
296 296 return parent
297 297 return p2
298 298
299 299 def puttags(self, tags):
300 300 try:
301 301 parentctx = self.repo[self.tagsbranch]
302 302 tagparent = parentctx.node()
303 303 except error.RepoError:
304 304 parentctx = None
305 305 tagparent = nullid
306 306
307 307 oldlines = set()
308 308 for branch, heads in self.repo.branchmap().iteritems():
309 309 for h in heads:
310 310 if '.hgtags' in self.repo[h]:
311 311 oldlines.update(
312 312 set(self.repo[h]['.hgtags'].data().splitlines(True)))
313 313 oldlines = sorted(list(oldlines))
314 314
315 315 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
316 316 if newlines == oldlines:
317 317 return None, None
318 318
319 319 # if the old and new tags match, then there is nothing to update
320 320 oldtags = set()
321 321 newtags = set()
322 322 for line in oldlines:
323 323 s = line.strip().split(' ', 1)
324 324 if len(s) != 2:
325 325 continue
326 326 oldtags.add(s[1])
327 327 for line in newlines:
328 328 s = line.strip().split(' ', 1)
329 329 if len(s) != 2:
330 330 continue
331 331 if s[1] not in oldtags:
332 332 newtags.add(s[1].strip())
333 333
334 334 if not newtags:
335 335 return None, None
336 336
337 337 data = "".join(newlines)
338 338 def getfilectx(repo, memctx, f):
339 339 return context.memfilectx(repo, f, data, False, False, None)
340 340
341 341 self.ui.status(_("updating tags\n"))
342 342 date = "%s 0" % int(time.mktime(time.gmtime()))
343 343 extra = {'branch': self.tagsbranch}
344 344 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
345 345 [".hgtags"], getfilectx, "convert-repo", date,
346 346 extra)
347 347 self.repo.commitctx(ctx)
348 348 return hex(self.repo.changelog.tip()), hex(tagparent)
349 349
350 350 def setfilemapmode(self, active):
351 351 self.filemapmode = active
352 352
353 353 def putbookmarks(self, updatedbookmark):
354 354 if not len(updatedbookmark):
355 355 return
356 356
357 357 self.ui.status(_("updating bookmarks\n"))
358 358 destmarks = self.repo._bookmarks
359 359 for bookmark in updatedbookmark:
360 360 destmarks[bookmark] = bin(updatedbookmark[bookmark])
361 361 destmarks.write()
362 362
363 363 def hascommitfrommap(self, rev):
364 364 # the exact semantics of clonebranches is unclear so we can't say no
365 365 return rev in self.repo or self.clonebranches
366 366
367 367 def hascommitforsplicemap(self, rev):
368 368 if rev not in self.repo and self.clonebranches:
369 369 raise util.Abort(_('revision %s not found in destination '
370 370 'repository (lookups with clonebranches=true '
371 371 'are not implemented)') % rev)
372 372 return rev in self.repo
373 373
374 374 class mercurial_source(converter_source):
375 375 def __init__(self, ui, path, rev=None):
376 376 converter_source.__init__(self, ui, path, rev)
377 377 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
378 378 self.ignored = set()
379 379 self.saverev = ui.configbool('convert', 'hg.saverev', False)
380 380 try:
381 381 self.repo = hg.repository(self.ui, path)
382 382 # try to provoke an exception if this isn't really a hg
383 383 # repo, but some other bogus compatible-looking url
384 384 if not self.repo.local():
385 385 raise error.RepoError
386 386 except error.RepoError:
387 387 ui.traceback()
388 388 raise NoRepo(_("%s is not a local Mercurial repository") % path)
389 389 self.lastrev = None
390 390 self.lastctx = None
391 391 self._changescache = None, None
392 392 self.convertfp = None
393 393 # Restrict converted revisions to startrev descendants
394 394 startnode = ui.config('convert', 'hg.startrev')
395 395 hgrevs = ui.config('convert', 'hg.revs')
396 396 if hgrevs is None:
397 397 if startnode is not None:
398 398 try:
399 399 startnode = self.repo.lookup(startnode)
400 400 except error.RepoError:
401 401 raise util.Abort(_('%s is not a valid start revision')
402 402 % startnode)
403 403 startrev = self.repo.changelog.rev(startnode)
404 404 children = {startnode: 1}
405 405 for r in self.repo.changelog.descendants([startrev]):
406 406 children[self.repo.changelog.node(r)] = 1
407 407 self.keep = children.__contains__
408 408 else:
409 409 self.keep = util.always
410 410 if rev:
411 411 self._heads = [self.repo[rev].node()]
412 412 else:
413 413 self._heads = self.repo.heads()
414 414 else:
415 415 if rev or startnode is not None:
416 416 raise util.Abort(_('hg.revs cannot be combined with '
417 417 'hg.startrev or --rev'))
418 418 nodes = set()
419 419 parents = set()
420 420 for r in scmutil.revrange(self.repo, [hgrevs]):
421 421 ctx = self.repo[r]
422 422 nodes.add(ctx.node())
423 423 parents.update(p.node() for p in ctx.parents())
424 424 self.keep = nodes.__contains__
425 425 self._heads = nodes - parents
426 426
427 427 def changectx(self, rev):
428 428 if self.lastrev != rev:
429 429 self.lastctx = self.repo[rev]
430 430 self.lastrev = rev
431 431 return self.lastctx
432 432
433 433 def parents(self, ctx):
434 434 return [p for p in ctx.parents() if p and self.keep(p.node())]
435 435
436 436 def getheads(self):
437 437 return [hex(h) for h in self._heads if self.keep(h)]
438 438
439 439 def getfile(self, name, rev):
440 440 try:
441 441 fctx = self.changectx(rev)[name]
442 442 return fctx.data(), fctx.flags()
443 443 except error.LookupError:
444 444 return None, None
445 445
446 446 def getchanges(self, rev, full):
447 447 ctx = self.changectx(rev)
448 448 parents = self.parents(ctx)
449 449 if full or not parents:
450 450 files = copyfiles = ctx.manifest()
451 451 if parents:
452 452 if self._changescache[0] == rev:
453 453 m, a, r = self._changescache[1]
454 454 else:
455 455 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
456 456 if not full:
457 457 files = m + a + r
458 458 copyfiles = m + a
459 459 # getcopies() is also run for roots and before filtering so missing
460 460 # revlogs are detected early
461 461 copies = self.getcopies(ctx, parents, copyfiles)
462 462 cleanp2 = set()
463 463 if len(parents) == 2:
464 464 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
465 465 clean=True).clean)
466 466 changes = [(f, rev) for f in files if f not in self.ignored]
467 467 changes.sort()
468 468 return changes, copies, cleanp2
469 469
470 470 def getcopies(self, ctx, parents, files):
471 471 copies = {}
472 472 for name in files:
473 473 if name in self.ignored:
474 474 continue
475 475 try:
476 476 copysource, _copynode = ctx.filectx(name).renamed()
477 477 if copysource in self.ignored:
478 478 continue
479 479 # Ignore copy sources not in parent revisions
480 480 found = False
481 481 for p in parents:
482 482 if copysource in p:
483 483 found = True
484 484 break
485 485 if not found:
486 486 continue
487 487 copies[name] = copysource
488 488 except TypeError:
489 489 pass
490 except error.LookupError, e:
490 except error.LookupError as e:
491 491 if not self.ignoreerrors:
492 492 raise
493 493 self.ignored.add(name)
494 494 self.ui.warn(_('ignoring: %s\n') % e)
495 495 return copies
496 496
497 497 def getcommit(self, rev):
498 498 ctx = self.changectx(rev)
499 499 parents = [p.hex() for p in self.parents(ctx)]
500 500 crev = rev
501 501
502 502 return commit(author=ctx.user(),
503 503 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
504 504 desc=ctx.description(), rev=crev, parents=parents,
505 505 branch=ctx.branch(), extra=ctx.extra(),
506 506 sortkey=ctx.rev(), saverev=self.saverev,
507 507 phase=ctx.phase())
508 508
509 509 def gettags(self):
510 510 # This will get written to .hgtags, filter non global tags out.
511 511 tags = [t for t in self.repo.tagslist()
512 512 if self.repo.tagtype(t[0]) == 'global']
513 513 return dict([(name, hex(node)) for name, node in tags
514 514 if self.keep(node)])
515 515
516 516 def getchangedfiles(self, rev, i):
517 517 ctx = self.changectx(rev)
518 518 parents = self.parents(ctx)
519 519 if not parents and i is None:
520 520 i = 0
521 521 changes = [], ctx.manifest().keys(), []
522 522 else:
523 523 i = i or 0
524 524 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
525 525 changes = [[f for f in l if f not in self.ignored] for l in changes]
526 526
527 527 if i == 0:
528 528 self._changescache = (rev, changes)
529 529
530 530 return changes[0] + changes[1] + changes[2]
531 531
532 532 def converted(self, rev, destrev):
533 533 if self.convertfp is None:
534 534 self.convertfp = open(self.repo.join('shamap'), 'a')
535 535 self.convertfp.write('%s %s\n' % (destrev, rev))
536 536 self.convertfp.flush()
537 537
538 538 def before(self):
539 539 self.ui.debug('run hg source pre-conversion action\n')
540 540
541 541 def after(self):
542 542 self.ui.debug('run hg source post-conversion action\n')
543 543
544 544 def hasnativeorder(self):
545 545 return True
546 546
547 547 def hasnativeclose(self):
548 548 return True
549 549
550 550 def lookuprev(self, rev):
551 551 try:
552 552 return hex(self.repo.lookup(rev))
553 553 except (error.RepoError, error.LookupError):
554 554 return None
555 555
556 556 def getbookmarks(self):
557 557 return bookmarks.listbookmarks(self.repo)
558 558
559 559 def checkrevformat(self, revstr, mapname='splicemap'):
560 560 """ Mercurial, revision string is a 40 byte hex """
561 561 self.checkhexformat(revstr, mapname)
@@ -1,1329 +1,1330 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4
5 5 import os, re, sys, tempfile, urllib, urllib2
6 6 import xml.dom.minidom
7 7 import cPickle as pickle
8 8
9 9 from mercurial import strutil, scmutil, util, encoding
10 10 from mercurial.i18n import _
11 11
12 12 propertycache = util.propertycache
13 13
14 14 # Subversion stuff. Works best with very recent Python SVN bindings
15 15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 16 # these bindings.
17 17
18 18 from cStringIO import StringIO
19 19
20 20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 21 from common import commandline, converter_source, converter_sink, mapfile
22 22 from common import makedatetimestamp
23 23
24 24 try:
25 25 from svn.core import SubversionException, Pool
26 26 import svn
27 27 import svn.client
28 28 import svn.core
29 29 import svn.ra
30 30 import svn.delta
31 31 import transport
32 32 import warnings
33 33 warnings.filterwarnings('ignore',
34 34 module='svn.core',
35 35 category=DeprecationWarning)
36 36
37 37 except ImportError:
38 38 svn = None
39 39
40 40 class SvnPathNotFound(Exception):
41 41 pass
42 42
43 43 def revsplit(rev):
44 44 """Parse a revision string and return (uuid, path, revnum).
45 45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
46 46 ... '/proj%20B/mytrunk/mytrunk@1')
47 47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
48 48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
49 49 ('', '', 1)
50 50 >>> revsplit('@7')
51 51 ('', '', 7)
52 52 >>> revsplit('7')
53 53 ('', '', 0)
54 54 >>> revsplit('bad')
55 55 ('', '', 0)
56 56 """
57 57 parts = rev.rsplit('@', 1)
58 58 revnum = 0
59 59 if len(parts) > 1:
60 60 revnum = int(parts[1])
61 61 parts = parts[0].split('/', 1)
62 62 uuid = ''
63 63 mod = ''
64 64 if len(parts) > 1 and parts[0].startswith('svn:'):
65 65 uuid = parts[0][4:]
66 66 mod = '/' + parts[1]
67 67 return uuid, mod, revnum
68 68
69 69 def quote(s):
70 70 # As of svn 1.7, many svn calls expect "canonical" paths. In
71 71 # theory, we should call svn.core.*canonicalize() on all paths
72 72 # before passing them to the API. Instead, we assume the base url
73 73 # is canonical and copy the behaviour of svn URL encoding function
74 74 # so we can extend it safely with new components. The "safe"
75 75 # characters were taken from the "svn_uri__char_validity" table in
76 76 # libsvn_subr/path.c.
77 77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
78 78
79 79 def geturl(path):
80 80 try:
81 81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
82 82 except SubversionException:
83 83 # svn.client.url_from_path() fails with local repositories
84 84 pass
85 85 if os.path.isdir(path):
86 86 path = os.path.normpath(os.path.abspath(path))
87 87 if os.name == 'nt':
88 88 path = '/' + util.normpath(path)
89 89 # Module URL is later compared with the repository URL returned
90 90 # by svn API, which is UTF-8.
91 91 path = encoding.tolocal(path)
92 92 path = 'file://%s' % quote(path)
93 93 return svn.core.svn_path_canonicalize(path)
94 94
95 95 def optrev(number):
96 96 optrev = svn.core.svn_opt_revision_t()
97 97 optrev.kind = svn.core.svn_opt_revision_number
98 98 optrev.value.number = number
99 99 return optrev
100 100
101 101 class changedpath(object):
102 102 def __init__(self, p):
103 103 self.copyfrom_path = p.copyfrom_path
104 104 self.copyfrom_rev = p.copyfrom_rev
105 105 self.action = p.action
106 106
107 107 def get_log_child(fp, url, paths, start, end, limit=0,
108 108 discover_changed_paths=True, strict_node_history=False):
109 109 protocol = -1
110 110 def receiver(orig_paths, revnum, author, date, message, pool):
111 111 paths = {}
112 112 if orig_paths is not None:
113 113 for k, v in orig_paths.iteritems():
114 114 paths[k] = changedpath(v)
115 115 pickle.dump((paths, revnum, author, date, message),
116 116 fp, protocol)
117 117
118 118 try:
119 119 # Use an ra of our own so that our parent can consume
120 120 # our results without confusing the server.
121 121 t = transport.SvnRaTransport(url=url)
122 122 svn.ra.get_log(t.ra, paths, start, end, limit,
123 123 discover_changed_paths,
124 124 strict_node_history,
125 125 receiver)
126 126 except IOError:
127 127 # Caller may interrupt the iteration
128 128 pickle.dump(None, fp, protocol)
129 except Exception, inst:
129 except Exception as inst:
130 130 pickle.dump(str(inst), fp, protocol)
131 131 else:
132 132 pickle.dump(None, fp, protocol)
133 133 fp.close()
134 134 # With large history, cleanup process goes crazy and suddenly
135 135 # consumes *huge* amount of memory. The output file being closed,
136 136 # there is no need for clean termination.
137 137 os._exit(0)
138 138
139 139 def debugsvnlog(ui, **opts):
140 140 """Fetch SVN log in a subprocess and channel them back to parent to
141 141 avoid memory collection issues.
142 142 """
143 143 if svn is None:
144 144 raise util.Abort(_('debugsvnlog could not load Subversion python '
145 145 'bindings'))
146 146
147 147 util.setbinary(sys.stdin)
148 148 util.setbinary(sys.stdout)
149 149 args = decodeargs(sys.stdin.read())
150 150 get_log_child(sys.stdout, *args)
151 151
152 152 class logstream(object):
153 153 """Interruptible revision log iterator."""
154 154 def __init__(self, stdout):
155 155 self._stdout = stdout
156 156
157 157 def __iter__(self):
158 158 while True:
159 159 try:
160 160 entry = pickle.load(self._stdout)
161 161 except EOFError:
162 162 raise util.Abort(_('Mercurial failed to run itself, check'
163 163 ' hg executable is in PATH'))
164 164 try:
165 165 orig_paths, revnum, author, date, message = entry
166 166 except (TypeError, ValueError):
167 167 if entry is None:
168 168 break
169 169 raise util.Abort(_("log stream exception '%s'") % entry)
170 170 yield entry
171 171
172 172 def close(self):
173 173 if self._stdout:
174 174 self._stdout.close()
175 175 self._stdout = None
176 176
177 177 class directlogstream(list):
178 178 """Direct revision log iterator.
179 179 This can be used for debugging and development but it will probably leak
180 180 memory and is not suitable for real conversions."""
181 181 def __init__(self, url, paths, start, end, limit=0,
182 182 discover_changed_paths=True, strict_node_history=False):
183 183
184 184 def receiver(orig_paths, revnum, author, date, message, pool):
185 185 paths = {}
186 186 if orig_paths is not None:
187 187 for k, v in orig_paths.iteritems():
188 188 paths[k] = changedpath(v)
189 189 self.append((paths, revnum, author, date, message))
190 190
191 191 # Use an ra of our own so that our parent can consume
192 192 # our results without confusing the server.
193 193 t = transport.SvnRaTransport(url=url)
194 194 svn.ra.get_log(t.ra, paths, start, end, limit,
195 195 discover_changed_paths,
196 196 strict_node_history,
197 197 receiver)
198 198
199 199 def close(self):
200 200 pass
201 201
202 202 # Check to see if the given path is a local Subversion repo. Verify this by
203 203 # looking for several svn-specific files and directories in the given
204 204 # directory.
205 205 def filecheck(ui, path, proto):
206 206 for x in ('locks', 'hooks', 'format', 'db'):
207 207 if not os.path.exists(os.path.join(path, x)):
208 208 return False
209 209 return True
210 210
211 211 # Check to see if a given path is the root of an svn repo over http. We verify
212 212 # this by requesting a version-controlled URL we know can't exist and looking
213 213 # for the svn-specific "not found" XML.
214 214 def httpcheck(ui, path, proto):
215 215 try:
216 216 opener = urllib2.build_opener()
217 217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
218 218 data = rsp.read()
219 except urllib2.HTTPError, inst:
219 except urllib2.HTTPError as inst:
220 220 if inst.code != 404:
221 221 # Except for 404 we cannot know for sure this is not an svn repo
222 222 ui.warn(_('svn: cannot probe remote repository, assume it could '
223 223 'be a subversion repository. Use --source-type if you '
224 224 'know better.\n'))
225 225 return True
226 226 data = inst.fp.read()
227 227 except Exception:
228 228 # Could be urllib2.URLError if the URL is invalid or anything else.
229 229 return False
230 230 return '<m:human-readable errcode="160013">' in data
231 231
232 232 protomap = {'http': httpcheck,
233 233 'https': httpcheck,
234 234 'file': filecheck,
235 235 }
236 236 def issvnurl(ui, url):
237 237 try:
238 238 proto, path = url.split('://', 1)
239 239 if proto == 'file':
240 240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
241 241 and path[2:6].lower() == '%3a/'):
242 242 path = path[:2] + ':/' + path[6:]
243 243 path = urllib.url2pathname(path)
244 244 except ValueError:
245 245 proto = 'file'
246 246 path = os.path.abspath(url)
247 247 if proto == 'file':
248 248 path = util.pconvert(path)
249 249 check = protomap.get(proto, lambda *args: False)
250 250 while '/' in path:
251 251 if check(ui, path, proto):
252 252 return True
253 253 path = path.rsplit('/', 1)[0]
254 254 return False
255 255
256 256 # SVN conversion code stolen from bzr-svn and tailor
257 257 #
258 258 # Subversion looks like a versioned filesystem, branches structures
259 259 # are defined by conventions and not enforced by the tool. First,
260 260 # we define the potential branches (modules) as "trunk" and "branches"
261 261 # children directories. Revisions are then identified by their
262 262 # module and revision number (and a repository identifier).
263 263 #
264 264 # The revision graph is really a tree (or a forest). By default, a
265 265 # revision parent is the previous revision in the same module. If the
266 266 # module directory is copied/moved from another module then the
267 267 # revision is the module root and its parent the source revision in
268 268 # the parent module. A revision has at most one parent.
269 269 #
270 270 class svn_source(converter_source):
271 271 def __init__(self, ui, url, rev=None):
272 272 super(svn_source, self).__init__(ui, url, rev=rev)
273 273
274 274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
275 275 (os.path.exists(url) and
276 276 os.path.exists(os.path.join(url, '.svn'))) or
277 277 issvnurl(ui, url)):
278 278 raise NoRepo(_("%s does not look like a Subversion repository")
279 279 % url)
280 280 if svn is None:
281 281 raise MissingTool(_('could not load Subversion python bindings'))
282 282
283 283 try:
284 284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
285 285 if version < (1, 4):
286 286 raise MissingTool(_('Subversion python bindings %d.%d found, '
287 287 '1.4 or later required') % version)
288 288 except AttributeError:
289 289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
290 290 'or later required'))
291 291
292 292 self.lastrevs = {}
293 293
294 294 latest = None
295 295 try:
296 296 # Support file://path@rev syntax. Useful e.g. to convert
297 297 # deleted branches.
298 298 at = url.rfind('@')
299 299 if at >= 0:
300 300 latest = int(url[at + 1:])
301 301 url = url[:at]
302 302 except ValueError:
303 303 pass
304 304 self.url = geturl(url)
305 305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
306 306 try:
307 307 self.transport = transport.SvnRaTransport(url=self.url)
308 308 self.ra = self.transport.ra
309 309 self.ctx = self.transport.client
310 310 self.baseurl = svn.ra.get_repos_root(self.ra)
311 311 # Module is either empty or a repository path starting with
312 312 # a slash and not ending with a slash.
313 313 self.module = urllib.unquote(self.url[len(self.baseurl):])
314 314 self.prevmodule = None
315 315 self.rootmodule = self.module
316 316 self.commits = {}
317 317 self.paths = {}
318 318 self.uuid = svn.ra.get_uuid(self.ra)
319 319 except SubversionException:
320 320 ui.traceback()
321 321 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
322 322 svn.core.SVN_VER_MINOR,
323 323 svn.core.SVN_VER_MICRO)
324 324 raise NoRepo(_("%s does not look like a Subversion repository "
325 325 "to libsvn version %s")
326 326 % (self.url, svnversion))
327 327
328 328 if rev:
329 329 try:
330 330 latest = int(rev)
331 331 except ValueError:
332 332 raise util.Abort(_('svn: revision %s is not an integer') % rev)
333 333
334 334 self.trunkname = self.ui.config('convert', 'svn.trunk',
335 335 'trunk').strip('/')
336 336 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
337 337 try:
338 338 self.startrev = int(self.startrev)
339 339 if self.startrev < 0:
340 340 self.startrev = 0
341 341 except ValueError:
342 342 raise util.Abort(_('svn: start revision %s is not an integer')
343 343 % self.startrev)
344 344
345 345 try:
346 346 self.head = self.latest(self.module, latest)
347 347 except SvnPathNotFound:
348 348 self.head = None
349 349 if not self.head:
350 350 raise util.Abort(_('no revision found in module %s')
351 351 % self.module)
352 352 self.last_changed = self.revnum(self.head)
353 353
354 354 self._changescache = (None, None)
355 355
356 356 if os.path.exists(os.path.join(url, '.svn/entries')):
357 357 self.wc = url
358 358 else:
359 359 self.wc = None
360 360 self.convertfp = None
361 361
362 362 def setrevmap(self, revmap):
363 363 lastrevs = {}
364 364 for revid in revmap.iterkeys():
365 365 uuid, module, revnum = revsplit(revid)
366 366 lastrevnum = lastrevs.setdefault(module, revnum)
367 367 if revnum > lastrevnum:
368 368 lastrevs[module] = revnum
369 369 self.lastrevs = lastrevs
370 370
371 371 def exists(self, path, optrev):
372 372 try:
373 373 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
374 374 optrev, False, self.ctx)
375 375 return True
376 376 except SubversionException:
377 377 return False
378 378
379 379 def getheads(self):
380 380
381 381 def isdir(path, revnum):
382 382 kind = self._checkpath(path, revnum)
383 383 return kind == svn.core.svn_node_dir
384 384
385 385 def getcfgpath(name, rev):
386 386 cfgpath = self.ui.config('convert', 'svn.' + name)
387 387 if cfgpath is not None and cfgpath.strip() == '':
388 388 return None
389 389 path = (cfgpath or name).strip('/')
390 390 if not self.exists(path, rev):
391 391 if self.module.endswith(path) and name == 'trunk':
392 392 # we are converting from inside this directory
393 393 return None
394 394 if cfgpath:
395 395 raise util.Abort(_('expected %s to be at %r, but not found')
396 396 % (name, path))
397 397 return None
398 398 self.ui.note(_('found %s at %r\n') % (name, path))
399 399 return path
400 400
401 401 rev = optrev(self.last_changed)
402 402 oldmodule = ''
403 403 trunk = getcfgpath('trunk', rev)
404 404 self.tags = getcfgpath('tags', rev)
405 405 branches = getcfgpath('branches', rev)
406 406
407 407 # If the project has a trunk or branches, we will extract heads
408 408 # from them. We keep the project root otherwise.
409 409 if trunk:
410 410 oldmodule = self.module or ''
411 411 self.module += '/' + trunk
412 412 self.head = self.latest(self.module, self.last_changed)
413 413 if not self.head:
414 414 raise util.Abort(_('no revision found in module %s')
415 415 % self.module)
416 416
417 417 # First head in the list is the module's head
418 418 self.heads = [self.head]
419 419 if self.tags is not None:
420 420 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
421 421
422 422 # Check if branches bring a few more heads to the list
423 423 if branches:
424 424 rpath = self.url.strip('/')
425 425 branchnames = svn.client.ls(rpath + '/' + quote(branches),
426 426 rev, False, self.ctx)
427 427 for branch in sorted(branchnames):
428 428 module = '%s/%s/%s' % (oldmodule, branches, branch)
429 429 if not isdir(module, self.last_changed):
430 430 continue
431 431 brevid = self.latest(module, self.last_changed)
432 432 if not brevid:
433 433 self.ui.note(_('ignoring empty branch %s\n') % branch)
434 434 continue
435 435 self.ui.note(_('found branch %s at %d\n') %
436 436 (branch, self.revnum(brevid)))
437 437 self.heads.append(brevid)
438 438
439 439 if self.startrev and self.heads:
440 440 if len(self.heads) > 1:
441 441 raise util.Abort(_('svn: start revision is not supported '
442 442 'with more than one branch'))
443 443 revnum = self.revnum(self.heads[0])
444 444 if revnum < self.startrev:
445 445 raise util.Abort(
446 446 _('svn: no revision found after start revision %d')
447 447 % self.startrev)
448 448
449 449 return self.heads
450 450
451 451 def _getchanges(self, rev, full):
452 452 (paths, parents) = self.paths[rev]
453 453 copies = {}
454 454 if parents:
455 455 files, self.removed, copies = self.expandpaths(rev, paths, parents)
456 456 if full or not parents:
457 457 # Perform a full checkout on roots
458 458 uuid, module, revnum = revsplit(rev)
459 459 entries = svn.client.ls(self.baseurl + quote(module),
460 460 optrev(revnum), True, self.ctx)
461 461 files = [n for n, e in entries.iteritems()
462 462 if e.kind == svn.core.svn_node_file]
463 463 self.removed = set()
464 464
465 465 files.sort()
466 466 files = zip(files, [rev] * len(files))
467 467 return (files, copies)
468 468
469 469 def getchanges(self, rev, full):
470 470 # reuse cache from getchangedfiles
471 471 if self._changescache[0] == rev and not full:
472 472 (files, copies) = self._changescache[1]
473 473 else:
474 474 (files, copies) = self._getchanges(rev, full)
475 475 # caller caches the result, so free it here to release memory
476 476 del self.paths[rev]
477 477 return (files, copies, set())
478 478
479 479 def getchangedfiles(self, rev, i):
480 480 # called from filemap - cache computed values for reuse in getchanges
481 481 (files, copies) = self._getchanges(rev, False)
482 482 self._changescache = (rev, (files, copies))
483 483 return [f[0] for f in files]
484 484
485 485 def getcommit(self, rev):
486 486 if rev not in self.commits:
487 487 uuid, module, revnum = revsplit(rev)
488 488 self.module = module
489 489 self.reparent(module)
490 490 # We assume that:
491 491 # - requests for revisions after "stop" come from the
492 492 # revision graph backward traversal. Cache all of them
493 493 # down to stop, they will be used eventually.
494 494 # - requests for revisions before "stop" come to get
495 495 # isolated branches parents. Just fetch what is needed.
496 496 stop = self.lastrevs.get(module, 0)
497 497 if revnum < stop:
498 498 stop = revnum + 1
499 499 self._fetch_revisions(revnum, stop)
500 500 if rev not in self.commits:
501 501 raise util.Abort(_('svn: revision %s not found') % revnum)
502 502 revcommit = self.commits[rev]
503 503 # caller caches the result, so free it here to release memory
504 504 del self.commits[rev]
505 505 return revcommit
506 506
507 507 def checkrevformat(self, revstr, mapname='splicemap'):
508 508 """ fails if revision format does not match the correct format"""
509 509 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
510 510 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
511 511 '{12,12}(.*)\@[0-9]+$',revstr):
512 512 raise util.Abort(_('%s entry %s is not a valid revision'
513 513 ' identifier') % (mapname, revstr))
514 514
515 515 def numcommits(self):
516 516 return int(self.head.rsplit('@', 1)[1]) - self.startrev
517 517
518 518 def gettags(self):
519 519 tags = {}
520 520 if self.tags is None:
521 521 return tags
522 522
523 523 # svn tags are just a convention, project branches left in a
524 524 # 'tags' directory. There is no other relationship than
525 525 # ancestry, which is expensive to discover and makes them hard
526 526 # to update incrementally. Worse, past revisions may be
527 527 # referenced by tags far away in the future, requiring a deep
528 528 # history traversal on every calculation. Current code
529 529 # performs a single backward traversal, tracking moves within
530 530 # the tags directory (tag renaming) and recording a new tag
531 531 # everytime a project is copied from outside the tags
532 532 # directory. It also lists deleted tags, this behaviour may
533 533 # change in the future.
534 534 pendings = []
535 535 tagspath = self.tags
536 536 start = svn.ra.get_latest_revnum(self.ra)
537 537 stream = self._getlog([self.tags], start, self.startrev)
538 538 try:
539 539 for entry in stream:
540 540 origpaths, revnum, author, date, message = entry
541 541 if not origpaths:
542 542 origpaths = []
543 543 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
544 544 in origpaths.iteritems() if e.copyfrom_path]
545 545 # Apply moves/copies from more specific to general
546 546 copies.sort(reverse=True)
547 547
548 548 srctagspath = tagspath
549 549 if copies and copies[-1][2] == tagspath:
550 550 # Track tags directory moves
551 551 srctagspath = copies.pop()[0]
552 552
553 553 for source, sourcerev, dest in copies:
554 554 if not dest.startswith(tagspath + '/'):
555 555 continue
556 556 for tag in pendings:
557 557 if tag[0].startswith(dest):
558 558 tagpath = source + tag[0][len(dest):]
559 559 tag[:2] = [tagpath, sourcerev]
560 560 break
561 561 else:
562 562 pendings.append([source, sourcerev, dest])
563 563
564 564 # Filter out tags with children coming from different
565 565 # parts of the repository like:
566 566 # /tags/tag.1 (from /trunk:10)
567 567 # /tags/tag.1/foo (from /branches/foo:12)
568 568 # Here/tags/tag.1 discarded as well as its children.
569 569 # It happens with tools like cvs2svn. Such tags cannot
570 570 # be represented in mercurial.
571 571 addeds = dict((p, e.copyfrom_path) for p, e
572 572 in origpaths.iteritems()
573 573 if e.action == 'A' and e.copyfrom_path)
574 574 badroots = set()
575 575 for destroot in addeds:
576 576 for source, sourcerev, dest in pendings:
577 577 if (not dest.startswith(destroot + '/')
578 578 or source.startswith(addeds[destroot] + '/')):
579 579 continue
580 580 badroots.add(destroot)
581 581 break
582 582
583 583 for badroot in badroots:
584 584 pendings = [p for p in pendings if p[2] != badroot
585 585 and not p[2].startswith(badroot + '/')]
586 586
587 587 # Tell tag renamings from tag creations
588 588 renamings = []
589 589 for source, sourcerev, dest in pendings:
590 590 tagname = dest.split('/')[-1]
591 591 if source.startswith(srctagspath):
592 592 renamings.append([source, sourcerev, tagname])
593 593 continue
594 594 if tagname in tags:
595 595 # Keep the latest tag value
596 596 continue
597 597 # From revision may be fake, get one with changes
598 598 try:
599 599 tagid = self.latest(source, sourcerev)
600 600 if tagid and tagname not in tags:
601 601 tags[tagname] = tagid
602 602 except SvnPathNotFound:
603 603 # It happens when we are following directories
604 604 # we assumed were copied with their parents
605 605 # but were really created in the tag
606 606 # directory.
607 607 pass
608 608 pendings = renamings
609 609 tagspath = srctagspath
610 610 finally:
611 611 stream.close()
612 612 return tags
613 613
614 614 def converted(self, rev, destrev):
615 615 if not self.wc:
616 616 return
617 617 if self.convertfp is None:
618 618 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
619 619 'a')
620 620 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
621 621 self.convertfp.flush()
622 622
623 623 def revid(self, revnum, module=None):
624 624 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
625 625
626 626 def revnum(self, rev):
627 627 return int(rev.split('@')[-1])
628 628
629 629 def latest(self, path, stop=None):
630 630 """Find the latest revid affecting path, up to stop revision
631 631 number. If stop is None, default to repository latest
632 632 revision. It may return a revision in a different module,
633 633 since a branch may be moved without a change being
634 634 reported. Return None if computed module does not belong to
635 635 rootmodule subtree.
636 636 """
637 637 def findchanges(path, start, stop=None):
638 638 stream = self._getlog([path], start, stop or 1)
639 639 try:
640 640 for entry in stream:
641 641 paths, revnum, author, date, message = entry
642 642 if stop is None and paths:
643 643 # We do not know the latest changed revision,
644 644 # keep the first one with changed paths.
645 645 break
646 646 if revnum <= stop:
647 647 break
648 648
649 649 for p in paths:
650 650 if (not path.startswith(p) or
651 651 not paths[p].copyfrom_path):
652 652 continue
653 653 newpath = paths[p].copyfrom_path + path[len(p):]
654 654 self.ui.debug("branch renamed from %s to %s at %d\n" %
655 655 (path, newpath, revnum))
656 656 path = newpath
657 657 break
658 658 if not paths:
659 659 revnum = None
660 660 return revnum, path
661 661 finally:
662 662 stream.close()
663 663
664 664 if not path.startswith(self.rootmodule):
665 665 # Requests on foreign branches may be forbidden at server level
666 666 self.ui.debug('ignoring foreign branch %r\n' % path)
667 667 return None
668 668
669 669 if stop is None:
670 670 stop = svn.ra.get_latest_revnum(self.ra)
671 671 try:
672 672 prevmodule = self.reparent('')
673 673 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
674 674 self.reparent(prevmodule)
675 675 except SubversionException:
676 676 dirent = None
677 677 if not dirent:
678 678 raise SvnPathNotFound(_('%s not found up to revision %d')
679 679 % (path, stop))
680 680
681 681 # stat() gives us the previous revision on this line of
682 682 # development, but it might be in *another module*. Fetch the
683 683 # log and detect renames down to the latest revision.
684 684 revnum, realpath = findchanges(path, stop, dirent.created_rev)
685 685 if revnum is None:
686 686 # Tools like svnsync can create empty revision, when
687 687 # synchronizing only a subtree for instance. These empty
688 688 # revisions created_rev still have their original values
689 689 # despite all changes having disappeared and can be
690 690 # returned by ra.stat(), at least when stating the root
691 691 # module. In that case, do not trust created_rev and scan
692 692 # the whole history.
693 693 revnum, realpath = findchanges(path, stop)
694 694 if revnum is None:
695 695 self.ui.debug('ignoring empty branch %r\n' % realpath)
696 696 return None
697 697
698 698 if not realpath.startswith(self.rootmodule):
699 699 self.ui.debug('ignoring foreign branch %r\n' % realpath)
700 700 return None
701 701 return self.revid(revnum, realpath)
702 702
703 703 def reparent(self, module):
704 704 """Reparent the svn transport and return the previous parent."""
705 705 if self.prevmodule == module:
706 706 return module
707 707 svnurl = self.baseurl + quote(module)
708 708 prevmodule = self.prevmodule
709 709 if prevmodule is None:
710 710 prevmodule = ''
711 711 self.ui.debug("reparent to %s\n" % svnurl)
712 712 svn.ra.reparent(self.ra, svnurl)
713 713 self.prevmodule = module
714 714 return prevmodule
715 715
716 716 def expandpaths(self, rev, paths, parents):
717 717 changed, removed = set(), set()
718 718 copies = {}
719 719
720 720 new_module, revnum = revsplit(rev)[1:]
721 721 if new_module != self.module:
722 722 self.module = new_module
723 723 self.reparent(self.module)
724 724
725 725 for i, (path, ent) in enumerate(paths):
726 726 self.ui.progress(_('scanning paths'), i, item=path,
727 727 total=len(paths))
728 728 entrypath = self.getrelpath(path)
729 729
730 730 kind = self._checkpath(entrypath, revnum)
731 731 if kind == svn.core.svn_node_file:
732 732 changed.add(self.recode(entrypath))
733 733 if not ent.copyfrom_path or not parents:
734 734 continue
735 735 # Copy sources not in parent revisions cannot be
736 736 # represented, ignore their origin for now
737 737 pmodule, prevnum = revsplit(parents[0])[1:]
738 738 if ent.copyfrom_rev < prevnum:
739 739 continue
740 740 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
741 741 if not copyfrom_path:
742 742 continue
743 743 self.ui.debug("copied to %s from %s@%s\n" %
744 744 (entrypath, copyfrom_path, ent.copyfrom_rev))
745 745 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
746 746 elif kind == 0: # gone, but had better be a deleted *file*
747 747 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
748 748 pmodule, prevnum = revsplit(parents[0])[1:]
749 749 parentpath = pmodule + "/" + entrypath
750 750 fromkind = self._checkpath(entrypath, prevnum, pmodule)
751 751
752 752 if fromkind == svn.core.svn_node_file:
753 753 removed.add(self.recode(entrypath))
754 754 elif fromkind == svn.core.svn_node_dir:
755 755 oroot = parentpath.strip('/')
756 756 nroot = path.strip('/')
757 757 children = self._iterfiles(oroot, prevnum)
758 758 for childpath in children:
759 759 childpath = childpath.replace(oroot, nroot)
760 760 childpath = self.getrelpath("/" + childpath, pmodule)
761 761 if childpath:
762 762 removed.add(self.recode(childpath))
763 763 else:
764 764 self.ui.debug('unknown path in revision %d: %s\n' % \
765 765 (revnum, path))
766 766 elif kind == svn.core.svn_node_dir:
767 767 if ent.action == 'M':
768 768 # If the directory just had a prop change,
769 769 # then we shouldn't need to look for its children.
770 770 continue
771 771 if ent.action == 'R' and parents:
772 772 # If a directory is replacing a file, mark the previous
773 773 # file as deleted
774 774 pmodule, prevnum = revsplit(parents[0])[1:]
775 775 pkind = self._checkpath(entrypath, prevnum, pmodule)
776 776 if pkind == svn.core.svn_node_file:
777 777 removed.add(self.recode(entrypath))
778 778 elif pkind == svn.core.svn_node_dir:
779 779 # We do not know what files were kept or removed,
780 780 # mark them all as changed.
781 781 for childpath in self._iterfiles(pmodule, prevnum):
782 782 childpath = self.getrelpath("/" + childpath)
783 783 if childpath:
784 784 changed.add(self.recode(childpath))
785 785
786 786 for childpath in self._iterfiles(path, revnum):
787 787 childpath = self.getrelpath("/" + childpath)
788 788 if childpath:
789 789 changed.add(self.recode(childpath))
790 790
791 791 # Handle directory copies
792 792 if not ent.copyfrom_path or not parents:
793 793 continue
794 794 # Copy sources not in parent revisions cannot be
795 795 # represented, ignore their origin for now
796 796 pmodule, prevnum = revsplit(parents[0])[1:]
797 797 if ent.copyfrom_rev < prevnum:
798 798 continue
799 799 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
800 800 if not copyfrompath:
801 801 continue
802 802 self.ui.debug("mark %s came from %s:%d\n"
803 803 % (path, copyfrompath, ent.copyfrom_rev))
804 804 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
805 805 for childpath in children:
806 806 childpath = self.getrelpath("/" + childpath, pmodule)
807 807 if not childpath:
808 808 continue
809 809 copytopath = path + childpath[len(copyfrompath):]
810 810 copytopath = self.getrelpath(copytopath)
811 811 copies[self.recode(copytopath)] = self.recode(childpath)
812 812
813 813 self.ui.progress(_('scanning paths'), None)
814 814 changed.update(removed)
815 815 return (list(changed), removed, copies)
816 816
817 817 def _fetch_revisions(self, from_revnum, to_revnum):
818 818 if from_revnum < to_revnum:
819 819 from_revnum, to_revnum = to_revnum, from_revnum
820 820
821 821 self.child_cset = None
822 822
823 823 def parselogentry(orig_paths, revnum, author, date, message):
824 824 """Return the parsed commit object or None, and True if
825 825 the revision is a branch root.
826 826 """
827 827 self.ui.debug("parsing revision %d (%d changes)\n" %
828 828 (revnum, len(orig_paths)))
829 829
830 830 branched = False
831 831 rev = self.revid(revnum)
832 832 # branch log might return entries for a parent we already have
833 833
834 834 if rev in self.commits or revnum < to_revnum:
835 835 return None, branched
836 836
837 837 parents = []
838 838 # check whether this revision is the start of a branch or part
839 839 # of a branch renaming
840 840 orig_paths = sorted(orig_paths.iteritems())
841 841 root_paths = [(p, e) for p, e in orig_paths
842 842 if self.module.startswith(p)]
843 843 if root_paths:
844 844 path, ent = root_paths[-1]
845 845 if ent.copyfrom_path:
846 846 branched = True
847 847 newpath = ent.copyfrom_path + self.module[len(path):]
848 848 # ent.copyfrom_rev may not be the actual last revision
849 849 previd = self.latest(newpath, ent.copyfrom_rev)
850 850 if previd is not None:
851 851 prevmodule, prevnum = revsplit(previd)[1:]
852 852 if prevnum >= self.startrev:
853 853 parents = [previd]
854 854 self.ui.note(
855 855 _('found parent of branch %s at %d: %s\n') %
856 856 (self.module, prevnum, prevmodule))
857 857 else:
858 858 self.ui.debug("no copyfrom path, don't know what to do.\n")
859 859
860 860 paths = []
861 861 # filter out unrelated paths
862 862 for path, ent in orig_paths:
863 863 if self.getrelpath(path) is None:
864 864 continue
865 865 paths.append((path, ent))
866 866
867 867 # Example SVN datetime. Includes microseconds.
868 868 # ISO-8601 conformant
869 869 # '2007-01-04T17:35:00.902377Z'
870 870 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
871 871 if self.ui.configbool('convert', 'localtimezone'):
872 872 date = makedatetimestamp(date[0])
873 873
874 874 if message:
875 875 log = self.recode(message)
876 876 else:
877 877 log = ''
878 878
879 879 if author:
880 880 author = self.recode(author)
881 881 else:
882 882 author = ''
883 883
884 884 try:
885 885 branch = self.module.split("/")[-1]
886 886 if branch == self.trunkname:
887 887 branch = None
888 888 except IndexError:
889 889 branch = None
890 890
891 891 cset = commit(author=author,
892 892 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
893 893 desc=log,
894 894 parents=parents,
895 895 branch=branch,
896 896 rev=rev)
897 897
898 898 self.commits[rev] = cset
899 899 # The parents list is *shared* among self.paths and the
900 900 # commit object. Both will be updated below.
901 901 self.paths[rev] = (paths, cset.parents)
902 902 if self.child_cset and not self.child_cset.parents:
903 903 self.child_cset.parents[:] = [rev]
904 904 self.child_cset = cset
905 905 return cset, branched
906 906
907 907 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
908 908 (self.module, from_revnum, to_revnum))
909 909
910 910 try:
911 911 firstcset = None
912 912 lastonbranch = False
913 913 stream = self._getlog([self.module], from_revnum, to_revnum)
914 914 try:
915 915 for entry in stream:
916 916 paths, revnum, author, date, message = entry
917 917 if revnum < self.startrev:
918 918 lastonbranch = True
919 919 break
920 920 if not paths:
921 921 self.ui.debug('revision %d has no entries\n' % revnum)
922 922 # If we ever leave the loop on an empty
923 923 # revision, do not try to get a parent branch
924 924 lastonbranch = lastonbranch or revnum == 0
925 925 continue
926 926 cset, lastonbranch = parselogentry(paths, revnum, author,
927 927 date, message)
928 928 if cset:
929 929 firstcset = cset
930 930 if lastonbranch:
931 931 break
932 932 finally:
933 933 stream.close()
934 934
935 935 if not lastonbranch and firstcset and not firstcset.parents:
936 936 # The first revision of the sequence (the last fetched one)
937 937 # has invalid parents if not a branch root. Find the parent
938 938 # revision now, if any.
939 939 try:
940 940 firstrevnum = self.revnum(firstcset.rev)
941 941 if firstrevnum > 1:
942 942 latest = self.latest(self.module, firstrevnum - 1)
943 943 if latest:
944 944 firstcset.parents.append(latest)
945 945 except SvnPathNotFound:
946 946 pass
947 except SubversionException, (inst, num):
947 except SubversionException as xxx_todo_changeme:
948 (inst, num) = xxx_todo_changeme.args
948 949 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
949 950 raise util.Abort(_('svn: branch has no revision %s')
950 951 % to_revnum)
951 952 raise
952 953
953 954 def getfile(self, file, rev):
954 955 # TODO: ra.get_file transmits the whole file instead of diffs.
955 956 if file in self.removed:
956 957 return None, None
957 958 mode = ''
958 959 try:
959 960 new_module, revnum = revsplit(rev)[1:]
960 961 if self.module != new_module:
961 962 self.module = new_module
962 963 self.reparent(self.module)
963 964 io = StringIO()
964 965 info = svn.ra.get_file(self.ra, file, revnum, io)
965 966 data = io.getvalue()
966 967 # ra.get_file() seems to keep a reference on the input buffer
967 968 # preventing collection. Release it explicitly.
968 969 io.close()
969 970 if isinstance(info, list):
970 971 info = info[-1]
971 972 mode = ("svn:executable" in info) and 'x' or ''
972 973 mode = ("svn:special" in info) and 'l' or mode
973 except SubversionException, e:
974 except SubversionException as e:
974 975 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
975 976 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
976 977 if e.apr_err in notfound: # File not found
977 978 return None, None
978 979 raise
979 980 if mode == 'l':
980 981 link_prefix = "link "
981 982 if data.startswith(link_prefix):
982 983 data = data[len(link_prefix):]
983 984 return data, mode
984 985
985 986 def _iterfiles(self, path, revnum):
986 987 """Enumerate all files in path at revnum, recursively."""
987 988 path = path.strip('/')
988 989 pool = Pool()
989 990 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
990 991 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
991 992 if path:
992 993 path += '/'
993 994 return ((path + p) for p, e in entries.iteritems()
994 995 if e.kind == svn.core.svn_node_file)
995 996
996 997 def getrelpath(self, path, module=None):
997 998 if module is None:
998 999 module = self.module
999 1000 # Given the repository url of this wc, say
1000 1001 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1001 1002 # extract the "entry" portion (a relative path) from what
1002 1003 # svn log --xml says, i.e.
1003 1004 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1004 1005 # that is to say "tests/PloneTestCase.py"
1005 1006 if path.startswith(module):
1006 1007 relative = path.rstrip('/')[len(module):]
1007 1008 if relative.startswith('/'):
1008 1009 return relative[1:]
1009 1010 elif relative == '':
1010 1011 return relative
1011 1012
1012 1013 # The path is outside our tracked tree...
1013 1014 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1014 1015 return None
1015 1016
1016 1017 def _checkpath(self, path, revnum, module=None):
1017 1018 if module is not None:
1018 1019 prevmodule = self.reparent('')
1019 1020 path = module + '/' + path
1020 1021 try:
1021 1022 # ra.check_path does not like leading slashes very much, it leads
1022 1023 # to PROPFIND subversion errors
1023 1024 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1024 1025 finally:
1025 1026 if module is not None:
1026 1027 self.reparent(prevmodule)
1027 1028
1028 1029 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1029 1030 strict_node_history=False):
1030 1031 # Normalize path names, svn >= 1.5 only wants paths relative to
1031 1032 # supplied URL
1032 1033 relpaths = []
1033 1034 for p in paths:
1034 1035 if not p.startswith('/'):
1035 1036 p = self.module + '/' + p
1036 1037 relpaths.append(p.strip('/'))
1037 1038 args = [self.baseurl, relpaths, start, end, limit,
1038 1039 discover_changed_paths, strict_node_history]
1039 1040 # undocumented feature: debugsvnlog can be disabled
1040 1041 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1041 1042 return directlogstream(*args)
1042 1043 arg = encodeargs(args)
1043 1044 hgexe = util.hgexecutable()
1044 1045 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1045 1046 stdin, stdout = util.popen2(util.quotecommand(cmd))
1046 1047 stdin.write(arg)
1047 1048 try:
1048 1049 stdin.close()
1049 1050 except IOError:
1050 1051 raise util.Abort(_('Mercurial failed to run itself, check'
1051 1052 ' hg executable is in PATH'))
1052 1053 return logstream(stdout)
1053 1054
1054 1055 pre_revprop_change = '''#!/bin/sh
1055 1056
1056 1057 REPOS="$1"
1057 1058 REV="$2"
1058 1059 USER="$3"
1059 1060 PROPNAME="$4"
1060 1061 ACTION="$5"
1061 1062
1062 1063 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1063 1064 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1064 1065 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1065 1066
1066 1067 echo "Changing prohibited revision property" >&2
1067 1068 exit 1
1068 1069 '''
1069 1070
1070 1071 class svn_sink(converter_sink, commandline):
1071 1072 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1072 1073 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1073 1074
1074 1075 def prerun(self):
1075 1076 if self.wc:
1076 1077 os.chdir(self.wc)
1077 1078
1078 1079 def postrun(self):
1079 1080 if self.wc:
1080 1081 os.chdir(self.cwd)
1081 1082
1082 1083 def join(self, name):
1083 1084 return os.path.join(self.wc, '.svn', name)
1084 1085
1085 1086 def revmapfile(self):
1086 1087 return self.join('hg-shamap')
1087 1088
1088 1089 def authorfile(self):
1089 1090 return self.join('hg-authormap')
1090 1091
1091 1092 def __init__(self, ui, path):
1092 1093
1093 1094 converter_sink.__init__(self, ui, path)
1094 1095 commandline.__init__(self, ui, 'svn')
1095 1096 self.delete = []
1096 1097 self.setexec = []
1097 1098 self.delexec = []
1098 1099 self.copies = []
1099 1100 self.wc = None
1100 1101 self.cwd = os.getcwd()
1101 1102
1102 1103 created = False
1103 1104 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1104 1105 self.wc = os.path.realpath(path)
1105 1106 self.run0('update')
1106 1107 else:
1107 1108 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1108 1109 path = os.path.realpath(path)
1109 1110 if os.path.isdir(os.path.dirname(path)):
1110 1111 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1111 1112 ui.status(_('initializing svn repository %r\n') %
1112 1113 os.path.basename(path))
1113 1114 commandline(ui, 'svnadmin').run0('create', path)
1114 1115 created = path
1115 1116 path = util.normpath(path)
1116 1117 if not path.startswith('/'):
1117 1118 path = '/' + path
1118 1119 path = 'file://' + path
1119 1120
1120 1121 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1121 1122 ui.status(_('initializing svn working copy %r\n')
1122 1123 % os.path.basename(wcpath))
1123 1124 self.run0('checkout', path, wcpath)
1124 1125
1125 1126 self.wc = wcpath
1126 1127 self.opener = scmutil.opener(self.wc)
1127 1128 self.wopener = scmutil.opener(self.wc)
1128 1129 self.childmap = mapfile(ui, self.join('hg-childmap'))
1129 1130 if util.checkexec(self.wc):
1130 1131 self.is_exec = util.isexec
1131 1132 else:
1132 1133 self.is_exec = None
1133 1134
1134 1135 if created:
1135 1136 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1136 1137 fp = open(hook, 'w')
1137 1138 fp.write(pre_revprop_change)
1138 1139 fp.close()
1139 1140 util.setflags(hook, False, True)
1140 1141
1141 1142 output = self.run0('info')
1142 1143 self.uuid = self.uuid_re.search(output).group(1).strip()
1143 1144
1144 1145 def wjoin(self, *names):
1145 1146 return os.path.join(self.wc, *names)
1146 1147
1147 1148 @propertycache
1148 1149 def manifest(self):
1149 1150 # As of svn 1.7, the "add" command fails when receiving
1150 1151 # already tracked entries, so we have to track and filter them
1151 1152 # ourselves.
1152 1153 m = set()
1153 1154 output = self.run0('ls', recursive=True, xml=True)
1154 1155 doc = xml.dom.minidom.parseString(output)
1155 1156 for e in doc.getElementsByTagName('entry'):
1156 1157 for n in e.childNodes:
1157 1158 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1158 1159 continue
1159 1160 name = ''.join(c.data for c in n.childNodes
1160 1161 if c.nodeType == c.TEXT_NODE)
1161 1162 # Entries are compared with names coming from
1162 1163 # mercurial, so bytes with undefined encoding. Our
1163 1164 # best bet is to assume they are in local
1164 1165 # encoding. They will be passed to command line calls
1165 1166 # later anyway, so they better be.
1166 1167 m.add(encoding.tolocal(name.encode('utf-8')))
1167 1168 break
1168 1169 return m
1169 1170
1170 1171 def putfile(self, filename, flags, data):
1171 1172 if 'l' in flags:
1172 1173 self.wopener.symlink(data, filename)
1173 1174 else:
1174 1175 try:
1175 1176 if os.path.islink(self.wjoin(filename)):
1176 1177 os.unlink(filename)
1177 1178 except OSError:
1178 1179 pass
1179 1180 self.wopener.write(filename, data)
1180 1181
1181 1182 if self.is_exec:
1182 1183 if self.is_exec(self.wjoin(filename)):
1183 1184 if 'x' not in flags:
1184 1185 self.delexec.append(filename)
1185 1186 else:
1186 1187 if 'x' in flags:
1187 1188 self.setexec.append(filename)
1188 1189 util.setflags(self.wjoin(filename), False, 'x' in flags)
1189 1190
1190 1191 def _copyfile(self, source, dest):
1191 1192 # SVN's copy command pukes if the destination file exists, but
1192 1193 # our copyfile method expects to record a copy that has
1193 1194 # already occurred. Cross the semantic gap.
1194 1195 wdest = self.wjoin(dest)
1195 1196 exists = os.path.lexists(wdest)
1196 1197 if exists:
1197 1198 fd, tempname = tempfile.mkstemp(
1198 1199 prefix='hg-copy-', dir=os.path.dirname(wdest))
1199 1200 os.close(fd)
1200 1201 os.unlink(tempname)
1201 1202 os.rename(wdest, tempname)
1202 1203 try:
1203 1204 self.run0('copy', source, dest)
1204 1205 finally:
1205 1206 self.manifest.add(dest)
1206 1207 if exists:
1207 1208 try:
1208 1209 os.unlink(wdest)
1209 1210 except OSError:
1210 1211 pass
1211 1212 os.rename(tempname, wdest)
1212 1213
1213 1214 def dirs_of(self, files):
1214 1215 dirs = set()
1215 1216 for f in files:
1216 1217 if os.path.isdir(self.wjoin(f)):
1217 1218 dirs.add(f)
1218 1219 for i in strutil.rfindall(f, '/'):
1219 1220 dirs.add(f[:i])
1220 1221 return dirs
1221 1222
1222 1223 def add_dirs(self, files):
1223 1224 add_dirs = [d for d in sorted(self.dirs_of(files))
1224 1225 if d not in self.manifest]
1225 1226 if add_dirs:
1226 1227 self.manifest.update(add_dirs)
1227 1228 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1228 1229 return add_dirs
1229 1230
1230 1231 def add_files(self, files):
1231 1232 files = [f for f in files if f not in self.manifest]
1232 1233 if files:
1233 1234 self.manifest.update(files)
1234 1235 self.xargs(files, 'add', quiet=True)
1235 1236 return files
1236 1237
1237 1238 def addchild(self, parent, child):
1238 1239 self.childmap[parent] = child
1239 1240
1240 1241 def revid(self, rev):
1241 1242 return u"svn:%s@%s" % (self.uuid, rev)
1242 1243
1243 1244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1244 1245 cleanp2):
1245 1246 for parent in parents:
1246 1247 try:
1247 1248 return self.revid(self.childmap[parent])
1248 1249 except KeyError:
1249 1250 pass
1250 1251
1251 1252 # Apply changes to working copy
1252 1253 for f, v in files:
1253 1254 data, mode = source.getfile(f, v)
1254 1255 if data is None:
1255 1256 self.delete.append(f)
1256 1257 else:
1257 1258 self.putfile(f, mode, data)
1258 1259 if f in copies:
1259 1260 self.copies.append([copies[f], f])
1260 1261 if full:
1261 1262 self.delete.extend(sorted(self.manifest.difference(files)))
1262 1263 files = [f[0] for f in files]
1263 1264
1264 1265 entries = set(self.delete)
1265 1266 files = frozenset(files)
1266 1267 entries.update(self.add_dirs(files.difference(entries)))
1267 1268 if self.copies:
1268 1269 for s, d in self.copies:
1269 1270 self._copyfile(s, d)
1270 1271 self.copies = []
1271 1272 if self.delete:
1272 1273 self.xargs(self.delete, 'delete')
1273 1274 for f in self.delete:
1274 1275 self.manifest.remove(f)
1275 1276 self.delete = []
1276 1277 entries.update(self.add_files(files.difference(entries)))
1277 1278 if self.delexec:
1278 1279 self.xargs(self.delexec, 'propdel', 'svn:executable')
1279 1280 self.delexec = []
1280 1281 if self.setexec:
1281 1282 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1282 1283 self.setexec = []
1283 1284
1284 1285 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1285 1286 fp = os.fdopen(fd, 'w')
1286 1287 fp.write(commit.desc)
1287 1288 fp.close()
1288 1289 try:
1289 1290 output = self.run0('commit',
1290 1291 username=util.shortuser(commit.author),
1291 1292 file=messagefile,
1292 1293 encoding='utf-8')
1293 1294 try:
1294 1295 rev = self.commit_re.search(output).group(1)
1295 1296 except AttributeError:
1296 1297 if parents and not files:
1297 1298 return parents[0]
1298 1299 self.ui.warn(_('unexpected svn output:\n'))
1299 1300 self.ui.warn(output)
1300 1301 raise util.Abort(_('unable to cope with svn output'))
1301 1302 if commit.rev:
1302 1303 self.run('propset', 'hg:convert-rev', commit.rev,
1303 1304 revprop=True, revision=rev)
1304 1305 if commit.branch and commit.branch != 'default':
1305 1306 self.run('propset', 'hg:convert-branch', commit.branch,
1306 1307 revprop=True, revision=rev)
1307 1308 for parent in parents:
1308 1309 self.addchild(parent, rev)
1309 1310 return self.revid(rev)
1310 1311 finally:
1311 1312 os.unlink(messagefile)
1312 1313
1313 1314 def puttags(self, tags):
1314 1315 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1315 1316 return None, None
1316 1317
1317 1318 def hascommitfrommap(self, rev):
1318 1319 # We trust that revisions referenced in a map still is present
1319 1320 # TODO: implement something better if necessary and feasible
1320 1321 return True
1321 1322
1322 1323 def hascommitforsplicemap(self, rev):
1323 1324 # This is not correct as one can convert to an existing subversion
1324 1325 # repository and childmap would not list all revisions. Too bad.
1325 1326 if rev in self.childmap:
1326 1327 return True
1327 1328 raise util.Abort(_('splice map revision %s not found in subversion '
1328 1329 'child map (revision lookups are not implemented)')
1329 1330 % rev)
@@ -1,128 +1,129 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2007 Daniel Holth <dholth@fastmail.fm>
4 4 # This is a stripped-down version of the original bzr-svn transport.py,
5 5 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
6 6
7 7 # This program is free software; you can redistribute it and/or modify
8 8 # it under the terms of the GNU General Public License as published by
9 9 # the Free Software Foundation; either version 2 of the License, or
10 10 # (at your option) any later version.
11 11
12 12 # This program is distributed in the hope that it will be useful,
13 13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 15 # GNU General Public License for more details.
16 16
17 17 # You should have received a copy of the GNU General Public License
18 18 # along with this program; if not, see <http://www.gnu.org/licenses/>.
19 19
20 20 from mercurial import util
21 21 from svn.core import SubversionException, Pool
22 22 import svn.ra
23 23 import svn.client
24 24 import svn.core
25 25
26 26 # Some older versions of the Python bindings need to be
27 27 # explicitly initialized. But what we want to do probably
28 28 # won't work worth a darn against those libraries anyway!
29 29 svn.ra.initialize()
30 30
31 31 svn_config = svn.core.svn_config_get_config(None)
32 32
33 33
34 34 def _create_auth_baton(pool):
35 35 """Create a Subversion authentication baton. """
36 36 import svn.client
37 37 # Give the client context baton a suite of authentication
38 38 # providers.h
39 39 providers = [
40 40 svn.client.get_simple_provider(pool),
41 41 svn.client.get_username_provider(pool),
42 42 svn.client.get_ssl_client_cert_file_provider(pool),
43 43 svn.client.get_ssl_client_cert_pw_file_provider(pool),
44 44 svn.client.get_ssl_server_trust_file_provider(pool),
45 45 ]
46 46 # Platform-dependent authentication methods
47 47 getprovider = getattr(svn.core, 'svn_auth_get_platform_specific_provider',
48 48 None)
49 49 if getprovider:
50 50 # Available in svn >= 1.6
51 51 for name in ('gnome_keyring', 'keychain', 'kwallet', 'windows'):
52 52 for type in ('simple', 'ssl_client_cert_pw', 'ssl_server_trust'):
53 53 p = getprovider(name, type, pool)
54 54 if p:
55 55 providers.append(p)
56 56 else:
57 57 if util.safehasattr(svn.client, 'get_windows_simple_provider'):
58 58 providers.append(svn.client.get_windows_simple_provider(pool))
59 59
60 60 return svn.core.svn_auth_open(providers, pool)
61 61
62 62 class NotBranchError(SubversionException):
63 63 pass
64 64
65 65 class SvnRaTransport(object):
66 66 """
67 67 Open an ra connection to a Subversion repository.
68 68 """
69 69 def __init__(self, url="", ra=None):
70 70 self.pool = Pool()
71 71 self.svn_url = url
72 72 self.username = ''
73 73 self.password = ''
74 74
75 75 # Only Subversion 1.4 has reparent()
76 76 if ra is None or not util.safehasattr(svn.ra, 'reparent'):
77 77 self.client = svn.client.create_context(self.pool)
78 78 ab = _create_auth_baton(self.pool)
79 79 if False:
80 80 svn.core.svn_auth_set_parameter(
81 81 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username)
82 82 svn.core.svn_auth_set_parameter(
83 83 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password)
84 84 self.client.auth_baton = ab
85 85 self.client.config = svn_config
86 86 try:
87 87 self.ra = svn.client.open_ra_session(
88 88 self.svn_url,
89 89 self.client, self.pool)
90 except SubversionException, (inst, num):
90 except SubversionException as xxx_todo_changeme:
91 (inst, num) = xxx_todo_changeme.args
91 92 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL,
92 93 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED,
93 94 svn.core.SVN_ERR_BAD_URL):
94 95 raise NotBranchError(url)
95 96 raise
96 97 else:
97 98 self.ra = ra
98 99 svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
99 100
100 101 class Reporter(object):
101 102 def __init__(self, reporter_data):
102 103 self._reporter, self._baton = reporter_data
103 104
104 105 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
105 106 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
106 107 path, revnum, start_empty, lock_token, pool)
107 108
108 109 def delete_path(self, path, pool=None):
109 110 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
110 111 path, pool)
111 112
112 113 def link_path(self, path, url, revision, start_empty, lock_token,
113 114 pool=None):
114 115 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
115 116 path, url, revision, start_empty, lock_token,
116 117 pool)
117 118
118 119 def finish_report(self, pool=None):
119 120 svn.ra.reporter2_invoke_finish_report(self._reporter,
120 121 self._baton, pool)
121 122
122 123 def abort_report(self, pool=None):
123 124 svn.ra.reporter2_invoke_abort_report(self._reporter,
124 125 self._baton, pool)
125 126
126 127 def do_update(self, revnum, path, *args, **kwargs):
127 128 return self.Reporter(svn.ra.do_update(self.ra, revnum, path,
128 129 *args, **kwargs))
@@ -1,354 +1,354 b''
1 1 """automatically manage newlines in repository files
2 2
3 3 This extension allows you to manage the type of line endings (CRLF or
4 4 LF) that are used in the repository and in the local working
5 5 directory. That way you can get CRLF line endings on Windows and LF on
6 6 Unix/Mac, thereby letting everybody use their OS native line endings.
7 7
8 8 The extension reads its configuration from a versioned ``.hgeol``
9 9 configuration file found in the root of the working directory. The
10 10 ``.hgeol`` file use the same syntax as all other Mercurial
11 11 configuration files. It uses two sections, ``[patterns]`` and
12 12 ``[repository]``.
13 13
14 14 The ``[patterns]`` section specifies how line endings should be
15 15 converted between the working directory and the repository. The format is
16 16 specified by a file pattern. The first match is used, so put more
17 17 specific patterns first. The available line endings are ``LF``,
18 18 ``CRLF``, and ``BIN``.
19 19
20 20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 21 checked out and stored in the repository in that format and files
22 22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 23 ``native`` is an alias for checking out in the platform's default line
24 24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 26 default behaviour; it is only needed if you need to override a later,
27 27 more general pattern.
28 28
29 29 The optional ``[repository]`` section specifies the line endings to
30 30 use for files stored in the repository. It has a single setting,
31 31 ``native``, which determines the storage line endings for files
32 32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 35 will be converted to ``LF`` when stored in the repository. Files
36 36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 37 are always stored as-is in the repository.
38 38
39 39 Example versioned ``.hgeol`` file::
40 40
41 41 [patterns]
42 42 **.py = native
43 43 **.vcproj = CRLF
44 44 **.txt = native
45 45 Makefile = LF
46 46 **.jpg = BIN
47 47
48 48 [repository]
49 49 native = LF
50 50
51 51 .. note::
52 52
53 53 The rules will first apply when files are touched in the working
54 54 directory, e.g. by updating to null and back to tip to touch all files.
55 55
56 56 The extension uses an optional ``[eol]`` section read from both the
57 57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 58 latter overriding the former. You can use that section to control the
59 59 overall behavior. There are three settings:
60 60
61 61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 62 ``CRLF`` to override the default interpretation of ``native`` for
63 63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 64 generate an archive where files have line endings for Windows.
65 65
66 66 - ``eol.only-consistent`` (default True) can be set to False to make
67 67 the extension convert files with inconsistent EOLs. Inconsistent
68 68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 69 Such files are normally not touched under the assumption that they
70 70 have mixed EOLs on purpose.
71 71
72 72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 73 ensure that converted files end with a EOL character (either ``\\n``
74 74 or ``\\r\\n`` as per the configured patterns).
75 75
76 76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 77 like the deprecated win32text extension does. This means that you can
78 78 disable win32text and enable eol and your filters will still work. You
79 79 only need to these filters until you have prepared a ``.hgeol`` file.
80 80
81 81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 82 have been unified into a single hook named ``eol.checkheadshook``. The
83 83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 84 which means you must migrate to a ``.hgeol`` file first before using
85 85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 86 invalid revisions will be pushed. To forbid them completely, use the
87 87 ``eol.checkallhook`` hook. These hooks are best used as
88 88 ``pretxnchangegroup`` hooks.
89 89
90 90 See :hg:`help patterns` for more information about the glob patterns
91 91 used.
92 92 """
93 93
94 94 from mercurial.i18n import _
95 95 from mercurial import util, config, extensions, match, error
96 96 import re, os
97 97
98 98 # Note for extension authors: ONLY specify testedwith = 'internal' for
99 99 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
100 100 # be specifying the version(s) of Mercurial they are tested with, or
101 101 # leave the attribute unspecified.
102 102 testedwith = 'internal'
103 103
104 104 # Matches a lone LF, i.e., one that is not part of CRLF.
105 105 singlelf = re.compile('(^|[^\r])\n')
106 106 # Matches a single EOL which can either be a CRLF where repeated CR
107 107 # are removed or a LF. We do not care about old Macintosh files, so a
108 108 # stray CR is an error.
109 109 eolre = re.compile('\r*\n')
110 110
111 111
112 112 def inconsistenteol(data):
113 113 return '\r\n' in data and singlelf.search(data)
114 114
115 115 def tolf(s, params, ui, **kwargs):
116 116 """Filter to convert to LF EOLs."""
117 117 if util.binary(s):
118 118 return s
119 119 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
120 120 return s
121 121 if (ui.configbool('eol', 'fix-trailing-newline', False)
122 122 and s and s[-1] != '\n'):
123 123 s = s + '\n'
124 124 return eolre.sub('\n', s)
125 125
126 126 def tocrlf(s, params, ui, **kwargs):
127 127 """Filter to convert to CRLF EOLs."""
128 128 if util.binary(s):
129 129 return s
130 130 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
131 131 return s
132 132 if (ui.configbool('eol', 'fix-trailing-newline', False)
133 133 and s and s[-1] != '\n'):
134 134 s = s + '\n'
135 135 return eolre.sub('\r\n', s)
136 136
137 137 def isbinary(s, params):
138 138 """Filter to do nothing with the file."""
139 139 return s
140 140
141 141 filters = {
142 142 'to-lf': tolf,
143 143 'to-crlf': tocrlf,
144 144 'is-binary': isbinary,
145 145 # The following provide backwards compatibility with win32text
146 146 'cleverencode:': tolf,
147 147 'cleverdecode:': tocrlf
148 148 }
149 149
150 150 class eolfile(object):
151 151 def __init__(self, ui, root, data):
152 152 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
153 153 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
154 154
155 155 self.cfg = config.config()
156 156 # Our files should not be touched. The pattern must be
157 157 # inserted first override a '** = native' pattern.
158 158 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
159 159 # We can then parse the user's patterns.
160 160 self.cfg.parse('.hgeol', data)
161 161
162 162 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
163 163 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
164 164 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
165 165 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
166 166
167 167 include = []
168 168 exclude = []
169 169 for pattern, style in self.cfg.items('patterns'):
170 170 key = style.upper()
171 171 if key == 'BIN':
172 172 exclude.append(pattern)
173 173 else:
174 174 include.append(pattern)
175 175 # This will match the files for which we need to care
176 176 # about inconsistent newlines.
177 177 self.match = match.match(root, '', [], include, exclude)
178 178
179 179 def copytoui(self, ui):
180 180 for pattern, style in self.cfg.items('patterns'):
181 181 key = style.upper()
182 182 try:
183 183 ui.setconfig('decode', pattern, self._decode[key], 'eol')
184 184 ui.setconfig('encode', pattern, self._encode[key], 'eol')
185 185 except KeyError:
186 186 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
187 187 % (style, self.cfg.source('patterns', pattern)))
188 188 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
189 189 for k, v in self.cfg.items('eol'):
190 190 ui.setconfig('eol', k, v, 'eol')
191 191
192 192 def checkrev(self, repo, ctx, files):
193 193 failed = []
194 194 for f in (files or ctx.files()):
195 195 if f not in ctx:
196 196 continue
197 197 for pattern, style in self.cfg.items('patterns'):
198 198 if not match.match(repo.root, '', [pattern])(f):
199 199 continue
200 200 target = self._encode[style.upper()]
201 201 data = ctx[f].data()
202 202 if (target == "to-lf" and "\r\n" in data
203 203 or target == "to-crlf" and singlelf.search(data)):
204 204 failed.append((str(ctx), target, f))
205 205 break
206 206 return failed
207 207
208 208 def parseeol(ui, repo, nodes):
209 209 try:
210 210 for node in nodes:
211 211 try:
212 212 if node is None:
213 213 # Cannot use workingctx.data() since it would load
214 214 # and cache the filters before we configure them.
215 215 data = repo.wfile('.hgeol').read()
216 216 else:
217 217 data = repo[node]['.hgeol'].data()
218 218 return eolfile(ui, repo.root, data)
219 219 except (IOError, LookupError):
220 220 pass
221 except error.ParseError, inst:
221 except error.ParseError as inst:
222 222 ui.warn(_("warning: ignoring .hgeol file due to parse error "
223 223 "at %s: %s\n") % (inst.args[1], inst.args[0]))
224 224 return None
225 225
226 226 def _checkhook(ui, repo, node, headsonly):
227 227 # Get revisions to check and touched files at the same time
228 228 files = set()
229 229 revs = set()
230 230 for rev in xrange(repo[node].rev(), len(repo)):
231 231 revs.add(rev)
232 232 if headsonly:
233 233 ctx = repo[rev]
234 234 files.update(ctx.files())
235 235 for pctx in ctx.parents():
236 236 revs.discard(pctx.rev())
237 237 failed = []
238 238 for rev in revs:
239 239 ctx = repo[rev]
240 240 eol = parseeol(ui, repo, [ctx.node()])
241 241 if eol:
242 242 failed.extend(eol.checkrev(repo, ctx, files))
243 243
244 244 if failed:
245 245 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
246 246 msgs = []
247 247 for node, target, f in failed:
248 248 msgs.append(_(" %s in %s should not have %s line endings") %
249 249 (f, node, eols[target]))
250 250 raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
251 251
252 252 def checkallhook(ui, repo, node, hooktype, **kwargs):
253 253 """verify that files have expected EOLs"""
254 254 _checkhook(ui, repo, node, False)
255 255
256 256 def checkheadshook(ui, repo, node, hooktype, **kwargs):
257 257 """verify that files have expected EOLs"""
258 258 _checkhook(ui, repo, node, True)
259 259
260 260 # "checkheadshook" used to be called "hook"
261 261 hook = checkheadshook
262 262
263 263 def preupdate(ui, repo, hooktype, parent1, parent2):
264 264 repo.loadeol([parent1])
265 265 return False
266 266
267 267 def uisetup(ui):
268 268 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
269 269
270 270 def extsetup(ui):
271 271 try:
272 272 extensions.find('win32text')
273 273 ui.warn(_("the eol extension is incompatible with the "
274 274 "win32text extension\n"))
275 275 except KeyError:
276 276 pass
277 277
278 278
279 279 def reposetup(ui, repo):
280 280 uisetup(repo.ui)
281 281
282 282 if not repo.local():
283 283 return
284 284 for name, fn in filters.iteritems():
285 285 repo.adddatafilter(name, fn)
286 286
287 287 ui.setconfig('patch', 'eol', 'auto', 'eol')
288 288
289 289 class eolrepo(repo.__class__):
290 290
291 291 def loadeol(self, nodes):
292 292 eol = parseeol(self.ui, self, nodes)
293 293 if eol is None:
294 294 return None
295 295 eol.copytoui(self.ui)
296 296 return eol.match
297 297
298 298 def _hgcleardirstate(self):
299 299 self._eolfile = self.loadeol([None, 'tip'])
300 300 if not self._eolfile:
301 301 self._eolfile = util.never
302 302 return
303 303
304 304 try:
305 305 cachemtime = os.path.getmtime(self.join("eol.cache"))
306 306 except OSError:
307 307 cachemtime = 0
308 308
309 309 try:
310 310 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
311 311 except OSError:
312 312 eolmtime = 0
313 313
314 314 if eolmtime > cachemtime:
315 315 self.ui.debug("eol: detected change in .hgeol\n")
316 316 wlock = None
317 317 try:
318 318 wlock = self.wlock()
319 319 for f in self.dirstate:
320 320 if self.dirstate[f] == 'n':
321 321 # all normal files need to be looked at
322 322 # again since the new .hgeol file might no
323 323 # longer match a file it matched before
324 324 self.dirstate.normallookup(f)
325 325 # Create or touch the cache to update mtime
326 326 self.vfs("eol.cache", "w").close()
327 327 wlock.release()
328 328 except error.LockUnavailable:
329 329 # If we cannot lock the repository and clear the
330 330 # dirstate, then a commit might not see all files
331 331 # as modified. But if we cannot lock the
332 332 # repository, then we can also not make a commit,
333 333 # so ignore the error.
334 334 pass
335 335
336 336 def commitctx(self, ctx, error=False):
337 337 for f in sorted(ctx.added() + ctx.modified()):
338 338 if not self._eolfile(f):
339 339 continue
340 340 fctx = ctx[f]
341 341 if fctx is None:
342 342 continue
343 343 data = fctx.data()
344 344 if util.binary(data):
345 345 # We should not abort here, since the user should
346 346 # be able to say "** = native" to automatically
347 347 # have all non-binary files taken care of.
348 348 continue
349 349 if inconsistenteol(data):
350 350 raise util.Abort(_("inconsistent newline style "
351 351 "in %s\n") % f)
352 352 return super(eolrepo, self).commitctx(ctx, error)
353 353 repo.__class__ = eolrepo
354 354 repo._hgcleardirstate()
@@ -1,301 +1,301 b''
1 1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''commands to sign and verify changesets'''
7 7
8 8 import os, tempfile, binascii
9 9 from mercurial import util, commands, match, cmdutil
10 10 from mercurial import node as hgnode
11 11 from mercurial.i18n import _
12 12
13 13 cmdtable = {}
14 14 command = cmdutil.command(cmdtable)
15 15 # Note for extension authors: ONLY specify testedwith = 'internal' for
16 16 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
17 17 # be specifying the version(s) of Mercurial they are tested with, or
18 18 # leave the attribute unspecified.
19 19 testedwith = 'internal'
20 20
21 21 class gpg(object):
22 22 def __init__(self, path, key=None):
23 23 self.path = path
24 24 self.key = (key and " --local-user \"%s\"" % key) or ""
25 25
26 26 def sign(self, data):
27 27 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
28 28 return util.filter(data, gpgcmd)
29 29
30 30 def verify(self, data, sig):
31 31 """ returns of the good and bad signatures"""
32 32 sigfile = datafile = None
33 33 try:
34 34 # create temporary files
35 35 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
36 36 fp = os.fdopen(fd, 'wb')
37 37 fp.write(sig)
38 38 fp.close()
39 39 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
40 40 fp = os.fdopen(fd, 'wb')
41 41 fp.write(data)
42 42 fp.close()
43 43 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
44 44 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
45 45 ret = util.filter("", gpgcmd)
46 46 finally:
47 47 for f in (sigfile, datafile):
48 48 try:
49 49 if f:
50 50 os.unlink(f)
51 51 except OSError:
52 52 pass
53 53 keys = []
54 54 key, fingerprint = None, None
55 55 for l in ret.splitlines():
56 56 # see DETAILS in the gnupg documentation
57 57 # filter the logger output
58 58 if not l.startswith("[GNUPG:]"):
59 59 continue
60 60 l = l[9:]
61 61 if l.startswith("VALIDSIG"):
62 62 # fingerprint of the primary key
63 63 fingerprint = l.split()[10]
64 64 elif l.startswith("ERRSIG"):
65 65 key = l.split(" ", 3)[:2]
66 66 key.append("")
67 67 fingerprint = None
68 68 elif (l.startswith("GOODSIG") or
69 69 l.startswith("EXPSIG") or
70 70 l.startswith("EXPKEYSIG") or
71 71 l.startswith("BADSIG")):
72 72 if key is not None:
73 73 keys.append(key + [fingerprint])
74 74 key = l.split(" ", 2)
75 75 fingerprint = None
76 76 if key is not None:
77 77 keys.append(key + [fingerprint])
78 78 return keys
79 79
80 80 def newgpg(ui, **opts):
81 81 """create a new gpg instance"""
82 82 gpgpath = ui.config("gpg", "cmd", "gpg")
83 83 gpgkey = opts.get('key')
84 84 if not gpgkey:
85 85 gpgkey = ui.config("gpg", "key", None)
86 86 return gpg(gpgpath, gpgkey)
87 87
88 88 def sigwalk(repo):
89 89 """
90 90 walk over every sigs, yields a couple
91 91 ((node, version, sig), (filename, linenumber))
92 92 """
93 93 def parsefile(fileiter, context):
94 94 ln = 1
95 95 for l in fileiter:
96 96 if not l:
97 97 continue
98 98 yield (l.split(" ", 2), (context, ln))
99 99 ln += 1
100 100
101 101 # read the heads
102 102 fl = repo.file(".hgsigs")
103 103 for r in reversed(fl.heads()):
104 104 fn = ".hgsigs|%s" % hgnode.short(r)
105 105 for item in parsefile(fl.read(r).splitlines(), fn):
106 106 yield item
107 107 try:
108 108 # read local signatures
109 109 fn = "localsigs"
110 110 for item in parsefile(repo.vfs(fn), fn):
111 111 yield item
112 112 except IOError:
113 113 pass
114 114
115 115 def getkeys(ui, repo, mygpg, sigdata, context):
116 116 """get the keys who signed a data"""
117 117 fn, ln = context
118 118 node, version, sig = sigdata
119 119 prefix = "%s:%d" % (fn, ln)
120 120 node = hgnode.bin(node)
121 121
122 122 data = node2txt(repo, node, version)
123 123 sig = binascii.a2b_base64(sig)
124 124 keys = mygpg.verify(data, sig)
125 125
126 126 validkeys = []
127 127 # warn for expired key and/or sigs
128 128 for key in keys:
129 129 if key[0] == "ERRSIG":
130 130 ui.write(_("%s Unknown key ID \"%s\"\n")
131 131 % (prefix, shortkey(ui, key[1][:15])))
132 132 continue
133 133 if key[0] == "BADSIG":
134 134 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
135 135 continue
136 136 if key[0] == "EXPSIG":
137 137 ui.write(_("%s Note: Signature has expired"
138 138 " (signed by: \"%s\")\n") % (prefix, key[2]))
139 139 elif key[0] == "EXPKEYSIG":
140 140 ui.write(_("%s Note: This key has expired"
141 141 " (signed by: \"%s\")\n") % (prefix, key[2]))
142 142 validkeys.append((key[1], key[2], key[3]))
143 143 return validkeys
144 144
145 145 @command("sigs", [], _('hg sigs'))
146 146 def sigs(ui, repo):
147 147 """list signed changesets"""
148 148 mygpg = newgpg(ui)
149 149 revs = {}
150 150
151 151 for data, context in sigwalk(repo):
152 152 node, version, sig = data
153 153 fn, ln = context
154 154 try:
155 155 n = repo.lookup(node)
156 156 except KeyError:
157 157 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
158 158 continue
159 159 r = repo.changelog.rev(n)
160 160 keys = getkeys(ui, repo, mygpg, data, context)
161 161 if not keys:
162 162 continue
163 163 revs.setdefault(r, [])
164 164 revs[r].extend(keys)
165 165 for rev in sorted(revs, reverse=True):
166 166 for k in revs[rev]:
167 167 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
168 168 ui.write("%-30s %s\n" % (keystr(ui, k), r))
169 169
170 170 @command("sigcheck", [], _('hg sigcheck REV'))
171 171 def check(ui, repo, rev):
172 172 """verify all the signatures there may be for a particular revision"""
173 173 mygpg = newgpg(ui)
174 174 rev = repo.lookup(rev)
175 175 hexrev = hgnode.hex(rev)
176 176 keys = []
177 177
178 178 for data, context in sigwalk(repo):
179 179 node, version, sig = data
180 180 if node == hexrev:
181 181 k = getkeys(ui, repo, mygpg, data, context)
182 182 if k:
183 183 keys.extend(k)
184 184
185 185 if not keys:
186 186 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
187 187 return
188 188
189 189 # print summary
190 190 ui.write("%s is signed by:\n" % hgnode.short(rev))
191 191 for key in keys:
192 192 ui.write(" %s\n" % keystr(ui, key))
193 193
194 194 def keystr(ui, key):
195 195 """associate a string to a key (username, comment)"""
196 196 keyid, user, fingerprint = key
197 197 comment = ui.config("gpg", fingerprint, None)
198 198 if comment:
199 199 return "%s (%s)" % (user, comment)
200 200 else:
201 201 return user
202 202
203 203 @command("sign",
204 204 [('l', 'local', None, _('make the signature local')),
205 205 ('f', 'force', None, _('sign even if the sigfile is modified')),
206 206 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
207 207 ('k', 'key', '',
208 208 _('the key id to sign with'), _('ID')),
209 209 ('m', 'message', '',
210 210 _('use text as commit message'), _('TEXT')),
211 211 ('e', 'edit', False, _('invoke editor on commit messages')),
212 212 ] + commands.commitopts2,
213 213 _('hg sign [OPTION]... [REV]...'))
214 214 def sign(ui, repo, *revs, **opts):
215 215 """add a signature for the current or given revision
216 216
217 217 If no revision is given, the parent of the working directory is used,
218 218 or tip if no revision is checked out.
219 219
220 220 See :hg:`help dates` for a list of formats valid for -d/--date.
221 221 """
222 222
223 223 mygpg = newgpg(ui, **opts)
224 224 sigver = "0"
225 225 sigmessage = ""
226 226
227 227 date = opts.get('date')
228 228 if date:
229 229 opts['date'] = util.parsedate(date)
230 230
231 231 if revs:
232 232 nodes = [repo.lookup(n) for n in revs]
233 233 else:
234 234 nodes = [node for node in repo.dirstate.parents()
235 235 if node != hgnode.nullid]
236 236 if len(nodes) > 1:
237 237 raise util.Abort(_('uncommitted merge - please provide a '
238 238 'specific revision'))
239 239 if not nodes:
240 240 nodes = [repo.changelog.tip()]
241 241
242 242 for n in nodes:
243 243 hexnode = hgnode.hex(n)
244 244 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
245 245 hgnode.short(n)))
246 246 # build data
247 247 data = node2txt(repo, n, sigver)
248 248 sig = mygpg.sign(data)
249 249 if not sig:
250 250 raise util.Abort(_("error while signing"))
251 251 sig = binascii.b2a_base64(sig)
252 252 sig = sig.replace("\n", "")
253 253 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
254 254
255 255 # write it
256 256 if opts['local']:
257 257 repo.vfs.append("localsigs", sigmessage)
258 258 return
259 259
260 260 if not opts["force"]:
261 261 msigs = match.exact(repo.root, '', ['.hgsigs'])
262 262 if any(repo.status(match=msigs, unknown=True, ignored=True)):
263 263 raise util.Abort(_("working copy of .hgsigs is changed "),
264 264 hint=_("please commit .hgsigs manually"))
265 265
266 266 sigsfile = repo.wfile(".hgsigs", "ab")
267 267 sigsfile.write(sigmessage)
268 268 sigsfile.close()
269 269
270 270 if '.hgsigs' not in repo.dirstate:
271 271 repo[None].add([".hgsigs"])
272 272
273 273 if opts["no_commit"]:
274 274 return
275 275
276 276 message = opts['message']
277 277 if not message:
278 278 # we don't translate commit messages
279 279 message = "\n".join(["Added signature for changeset %s"
280 280 % hgnode.short(n)
281 281 for n in nodes])
282 282 try:
283 283 editor = cmdutil.getcommiteditor(editform='gpg.sign', **opts)
284 284 repo.commit(message, opts['user'], opts['date'], match=msigs,
285 285 editor=editor)
286 except ValueError, inst:
286 except ValueError as inst:
287 287 raise util.Abort(str(inst))
288 288
289 289 def shortkey(ui, key):
290 290 if len(key) != 16:
291 291 ui.debug("key ID \"%s\" format error\n" % key)
292 292 return key
293 293
294 294 return key[-8:]
295 295
296 296 def node2txt(repo, node, ver):
297 297 """map a manifest into some text"""
298 298 if ver == "0":
299 299 return "%s\n" % hgnode.hex(node)
300 300 else:
301 301 raise util.Abort(_("unknown signature version"))
@@ -1,1159 +1,1159 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 stop for amending
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
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145
146 146 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customise this behaviour by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 163 from mercurial import cmdutil
164 164 from mercurial import discovery
165 165 from mercurial import error
166 166 from mercurial import changegroup
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 184 # Note for extension authors: ONLY specify testedwith = 'internal' for
185 185 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
186 186 # be specifying the version(s) of Mercurial they are tested with, or
187 187 # leave the attribute unspecified.
188 188 testedwith = 'internal'
189 189
190 190 # i18n: command names and abbreviations must remain untranslated
191 191 editcomment = _("""# Edit history between %s and %s
192 192 #
193 193 # Commits are listed from least to most recent
194 194 #
195 195 # Commands:
196 196 # p, pick = use commit
197 197 # e, edit = use commit, but stop for amending
198 198 # f, fold = use commit, but combine it with the one above
199 199 # r, roll = like fold, but discard this commit's description
200 200 # d, drop = remove commit from history
201 201 # m, mess = edit message without changing commit content
202 202 #
203 203 """)
204 204
205 205 class histeditstate(object):
206 206 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
207 207 topmost=None, replacements=None, lock=None, wlock=None):
208 208 self.repo = repo
209 209 self.rules = rules
210 210 self.keep = keep
211 211 self.topmost = topmost
212 212 self.parentctxnode = parentctxnode
213 213 self.lock = lock
214 214 self.wlock = wlock
215 215 self.backupfile = None
216 216 if replacements is None:
217 217 self.replacements = []
218 218 else:
219 219 self.replacements = replacements
220 220
221 221 def read(self):
222 222 """Load histedit state from disk and set fields appropriately."""
223 223 try:
224 224 fp = self.repo.vfs('histedit-state', 'r')
225 except IOError, err:
225 except IOError as err:
226 226 if err.errno != errno.ENOENT:
227 227 raise
228 228 raise util.Abort(_('no histedit in progress'))
229 229
230 230 try:
231 231 data = pickle.load(fp)
232 232 parentctxnode, rules, keep, topmost, replacements = data
233 233 backupfile = None
234 234 except pickle.UnpicklingError:
235 235 data = self._load()
236 236 parentctxnode, rules, keep, topmost, replacements, backupfile = data
237 237
238 238 self.parentctxnode = parentctxnode
239 239 self.rules = rules
240 240 self.keep = keep
241 241 self.topmost = topmost
242 242 self.replacements = replacements
243 243 self.backupfile = backupfile
244 244
245 245 def write(self):
246 246 fp = self.repo.vfs('histedit-state', 'w')
247 247 fp.write('v1\n')
248 248 fp.write('%s\n' % node.hex(self.parentctxnode))
249 249 fp.write('%s\n' % node.hex(self.topmost))
250 250 fp.write('%s\n' % self.keep)
251 251 fp.write('%d\n' % len(self.rules))
252 252 for rule in self.rules:
253 253 fp.write('%s\n' % rule[0]) # action
254 254 fp.write('%s\n' % rule[1]) # remainder
255 255 fp.write('%d\n' % len(self.replacements))
256 256 for replacement in self.replacements:
257 257 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
258 258 for r in replacement[1])))
259 259 backupfile = self.backupfile
260 260 if not backupfile:
261 261 backupfile = ''
262 262 fp.write('%s\n' % backupfile)
263 263 fp.close()
264 264
265 265 def _load(self):
266 266 fp = self.repo.vfs('histedit-state', 'r')
267 267 lines = [l[:-1] for l in fp.readlines()]
268 268
269 269 index = 0
270 270 lines[index] # version number
271 271 index += 1
272 272
273 273 parentctxnode = node.bin(lines[index])
274 274 index += 1
275 275
276 276 topmost = node.bin(lines[index])
277 277 index += 1
278 278
279 279 keep = lines[index] == 'True'
280 280 index += 1
281 281
282 282 # Rules
283 283 rules = []
284 284 rulelen = int(lines[index])
285 285 index += 1
286 286 for i in xrange(rulelen):
287 287 ruleaction = lines[index]
288 288 index += 1
289 289 rule = lines[index]
290 290 index += 1
291 291 rules.append((ruleaction, rule))
292 292
293 293 # Replacements
294 294 replacements = []
295 295 replacementlen = int(lines[index])
296 296 index += 1
297 297 for i in xrange(replacementlen):
298 298 replacement = lines[index]
299 299 original = node.bin(replacement[:40])
300 300 succ = [node.bin(replacement[i:i + 40]) for i in
301 301 range(40, len(replacement), 40)]
302 302 replacements.append((original, succ))
303 303 index += 1
304 304
305 305 backupfile = lines[index]
306 306 index += 1
307 307
308 308 fp.close()
309 309
310 310 return parentctxnode, rules, keep, topmost, replacements, backupfile
311 311
312 312 def clear(self):
313 313 self.repo.vfs.unlink('histedit-state')
314 314
315 315 class histeditaction(object):
316 316 def __init__(self, state, node):
317 317 self.state = state
318 318 self.repo = state.repo
319 319 self.node = node
320 320
321 321 @classmethod
322 322 def fromrule(cls, state, rule):
323 323 """Parses the given rule, returning an instance of the histeditaction.
324 324 """
325 325 repo = state.repo
326 326 rulehash = rule.strip().split(' ', 1)[0]
327 327 try:
328 328 node = repo[rulehash].node()
329 329 except error.RepoError:
330 330 raise util.Abort(_('unknown changeset %s listed') % rulehash[:12])
331 331 return cls(state, node)
332 332
333 333 def run(self):
334 334 """Runs the action. The default behavior is simply apply the action's
335 335 rulectx onto the current parentctx."""
336 336 self.applychange()
337 337 self.continuedirty()
338 338 return self.continueclean()
339 339
340 340 def applychange(self):
341 341 """Applies the changes from this action's rulectx onto the current
342 342 parentctx, but does not commit them."""
343 343 repo = self.repo
344 344 rulectx = repo[self.node]
345 345 hg.update(repo, self.state.parentctxnode)
346 346 stats = applychanges(repo.ui, repo, rulectx, {})
347 347 if stats and stats[3] > 0:
348 348 raise error.InterventionRequired(_('Fix up the change and run '
349 349 'hg histedit --continue'))
350 350
351 351 def continuedirty(self):
352 352 """Continues the action when changes have been applied to the working
353 353 copy. The default behavior is to commit the dirty changes."""
354 354 repo = self.repo
355 355 rulectx = repo[self.node]
356 356
357 357 editor = self.commiteditor()
358 358 commit = commitfuncfor(repo, rulectx)
359 359
360 360 commit(text=rulectx.description(), user=rulectx.user(),
361 361 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
362 362
363 363 def commiteditor(self):
364 364 """The editor to be used to edit the commit message."""
365 365 return False
366 366
367 367 def continueclean(self):
368 368 """Continues the action when the working copy is clean. The default
369 369 behavior is to accept the current commit as the new version of the
370 370 rulectx."""
371 371 ctx = self.repo['.']
372 372 if ctx.node() == self.state.parentctxnode:
373 373 self.repo.ui.warn(_('%s: empty changeset\n') %
374 374 node.short(self.node))
375 375 return ctx, [(self.node, tuple())]
376 376 if ctx.node() == self.node:
377 377 # Nothing changed
378 378 return ctx, []
379 379 return ctx, [(self.node, (ctx.node(),))]
380 380
381 381 def commitfuncfor(repo, src):
382 382 """Build a commit function for the replacement of <src>
383 383
384 384 This function ensure we apply the same treatment to all changesets.
385 385
386 386 - Add a 'histedit_source' entry in extra.
387 387
388 388 Note that fold has its own separated logic because its handling is a bit
389 389 different and not easily factored out of the fold method.
390 390 """
391 391 phasemin = src.phase()
392 392 def commitfunc(**kwargs):
393 393 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
394 394 try:
395 395 repo.ui.setconfig('phases', 'new-commit', phasemin,
396 396 'histedit')
397 397 extra = kwargs.get('extra', {}).copy()
398 398 extra['histedit_source'] = src.hex()
399 399 kwargs['extra'] = extra
400 400 return repo.commit(**kwargs)
401 401 finally:
402 402 repo.ui.restoreconfig(phasebackup)
403 403 return commitfunc
404 404
405 405 def applychanges(ui, repo, ctx, opts):
406 406 """Merge changeset from ctx (only) in the current working directory"""
407 407 wcpar = repo.dirstate.parents()[0]
408 408 if ctx.p1().node() == wcpar:
409 409 # edition ar "in place" we do not need to make any merge,
410 410 # just applies changes on parent for edition
411 411 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
412 412 stats = None
413 413 else:
414 414 try:
415 415 # ui.forcemerge is an internal variable, do not document
416 416 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
417 417 'histedit')
418 418 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
419 419 finally:
420 420 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
421 421 return stats
422 422
423 423 def collapse(repo, first, last, commitopts, skipprompt=False):
424 424 """collapse the set of revisions from first to last as new one.
425 425
426 426 Expected commit options are:
427 427 - message
428 428 - date
429 429 - username
430 430 Commit message is edited in all cases.
431 431
432 432 This function works in memory."""
433 433 ctxs = list(repo.set('%d::%d', first, last))
434 434 if not ctxs:
435 435 return None
436 436 for c in ctxs:
437 437 if not c.mutable():
438 438 raise util.Abort(
439 439 _("cannot fold into public change %s") % node.short(c.node()))
440 440 base = first.parents()[0]
441 441
442 442 # commit a new version of the old changeset, including the update
443 443 # collect all files which might be affected
444 444 files = set()
445 445 for ctx in ctxs:
446 446 files.update(ctx.files())
447 447
448 448 # Recompute copies (avoid recording a -> b -> a)
449 449 copied = copies.pathcopies(base, last)
450 450
451 451 # prune files which were reverted by the updates
452 452 def samefile(f):
453 453 if f in last.manifest():
454 454 a = last.filectx(f)
455 455 if f in base.manifest():
456 456 b = base.filectx(f)
457 457 return (a.data() == b.data()
458 458 and a.flags() == b.flags())
459 459 else:
460 460 return False
461 461 else:
462 462 return f not in base.manifest()
463 463 files = [f for f in files if not samefile(f)]
464 464 # commit version of these files as defined by head
465 465 headmf = last.manifest()
466 466 def filectxfn(repo, ctx, path):
467 467 if path in headmf:
468 468 fctx = last[path]
469 469 flags = fctx.flags()
470 470 mctx = context.memfilectx(repo,
471 471 fctx.path(), fctx.data(),
472 472 islink='l' in flags,
473 473 isexec='x' in flags,
474 474 copied=copied.get(path))
475 475 return mctx
476 476 return None
477 477
478 478 if commitopts.get('message'):
479 479 message = commitopts['message']
480 480 else:
481 481 message = first.description()
482 482 user = commitopts.get('user')
483 483 date = commitopts.get('date')
484 484 extra = commitopts.get('extra')
485 485
486 486 parents = (first.p1().node(), first.p2().node())
487 487 editor = None
488 488 if not skipprompt:
489 489 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
490 490 new = context.memctx(repo,
491 491 parents=parents,
492 492 text=message,
493 493 files=files,
494 494 filectxfn=filectxfn,
495 495 user=user,
496 496 date=date,
497 497 extra=extra,
498 498 editor=editor)
499 499 return repo.commitctx(new)
500 500
501 501 class pick(histeditaction):
502 502 def run(self):
503 503 rulectx = self.repo[self.node]
504 504 if rulectx.parents()[0].node() == self.state.parentctxnode:
505 505 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
506 506 return rulectx, []
507 507
508 508 return super(pick, self).run()
509 509
510 510 class edit(histeditaction):
511 511 def run(self):
512 512 repo = self.repo
513 513 rulectx = repo[self.node]
514 514 hg.update(repo, self.state.parentctxnode)
515 515 applychanges(repo.ui, repo, rulectx, {})
516 516 raise error.InterventionRequired(
517 517 _('Make changes as needed, you may commit or record as needed '
518 518 'now.\nWhen you are finished, run hg histedit --continue to '
519 519 'resume.'))
520 520
521 521 def commiteditor(self):
522 522 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
523 523
524 524 class fold(histeditaction):
525 525 def continuedirty(self):
526 526 repo = self.repo
527 527 rulectx = repo[self.node]
528 528
529 529 commit = commitfuncfor(repo, rulectx)
530 530 commit(text='fold-temp-revision %s' % node.short(self.node),
531 531 user=rulectx.user(), date=rulectx.date(),
532 532 extra=rulectx.extra())
533 533
534 534 def continueclean(self):
535 535 repo = self.repo
536 536 ctx = repo['.']
537 537 rulectx = repo[self.node]
538 538 parentctxnode = self.state.parentctxnode
539 539 if ctx.node() == parentctxnode:
540 540 repo.ui.warn(_('%s: empty changeset\n') %
541 541 node.short(self.node))
542 542 return ctx, [(self.node, (parentctxnode,))]
543 543
544 544 parentctx = repo[parentctxnode]
545 545 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
546 546 parentctx))
547 547 if not newcommits:
548 548 repo.ui.warn(_('%s: cannot fold - working copy is not a '
549 549 'descendant of previous commit %s\n') %
550 550 (node.short(self.node), node.short(parentctxnode)))
551 551 return ctx, [(self.node, (ctx.node(),))]
552 552
553 553 middlecommits = newcommits.copy()
554 554 middlecommits.discard(ctx.node())
555 555
556 556 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
557 557 middlecommits)
558 558
559 559 def skipprompt(self):
560 560 return False
561 561
562 562 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
563 563 parent = ctx.parents()[0].node()
564 564 hg.update(repo, parent)
565 565 ### prepare new commit data
566 566 commitopts = {}
567 567 commitopts['user'] = ctx.user()
568 568 # commit message
569 569 if self.skipprompt():
570 570 newmessage = ctx.description()
571 571 else:
572 572 newmessage = '\n***\n'.join(
573 573 [ctx.description()] +
574 574 [repo[r].description() for r in internalchanges] +
575 575 [oldctx.description()]) + '\n'
576 576 commitopts['message'] = newmessage
577 577 # date
578 578 commitopts['date'] = max(ctx.date(), oldctx.date())
579 579 extra = ctx.extra().copy()
580 580 # histedit_source
581 581 # note: ctx is likely a temporary commit but that the best we can do
582 582 # here. This is sufficient to solve issue3681 anyway.
583 583 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
584 584 commitopts['extra'] = extra
585 585 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
586 586 try:
587 587 phasemin = max(ctx.phase(), oldctx.phase())
588 588 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
589 589 n = collapse(repo, ctx, repo[newnode], commitopts,
590 590 skipprompt=self.skipprompt())
591 591 finally:
592 592 repo.ui.restoreconfig(phasebackup)
593 593 if n is None:
594 594 return ctx, []
595 595 hg.update(repo, n)
596 596 replacements = [(oldctx.node(), (newnode,)),
597 597 (ctx.node(), (n,)),
598 598 (newnode, (n,)),
599 599 ]
600 600 for ich in internalchanges:
601 601 replacements.append((ich, (n,)))
602 602 return repo[n], replacements
603 603
604 604 class rollup(fold):
605 605 def skipprompt(self):
606 606 return True
607 607
608 608 class drop(histeditaction):
609 609 def run(self):
610 610 parentctx = self.repo[self.state.parentctxnode]
611 611 return parentctx, [(self.node, tuple())]
612 612
613 613 class message(histeditaction):
614 614 def commiteditor(self):
615 615 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
616 616
617 617 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
618 618 """utility function to find the first outgoing changeset
619 619
620 620 Used by initialisation code"""
621 621 dest = ui.expandpath(remote or 'default-push', remote or 'default')
622 622 dest, revs = hg.parseurl(dest, None)[:2]
623 623 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
624 624
625 625 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
626 626 other = hg.peer(repo, opts, dest)
627 627
628 628 if revs:
629 629 revs = [repo.lookup(rev) for rev in revs]
630 630
631 631 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
632 632 if not outgoing.missing:
633 633 raise util.Abort(_('no outgoing ancestors'))
634 634 roots = list(repo.revs("roots(%ln)", outgoing.missing))
635 635 if 1 < len(roots):
636 636 msg = _('there are ambiguous outgoing revisions')
637 637 hint = _('see "hg help histedit" for more detail')
638 638 raise util.Abort(msg, hint=hint)
639 639 return repo.lookup(roots[0])
640 640
641 641 actiontable = {'p': pick,
642 642 'pick': pick,
643 643 'e': edit,
644 644 'edit': edit,
645 645 'f': fold,
646 646 'fold': fold,
647 647 'r': rollup,
648 648 'roll': rollup,
649 649 'd': drop,
650 650 'drop': drop,
651 651 'm': message,
652 652 'mess': message,
653 653 }
654 654
655 655 @command('histedit',
656 656 [('', 'commands', '',
657 657 _('read history edits from the specified file'), _('FILE')),
658 658 ('c', 'continue', False, _('continue an edit already in progress')),
659 659 ('', 'edit-plan', False, _('edit remaining actions list')),
660 660 ('k', 'keep', False,
661 661 _("don't strip old nodes after edit is complete")),
662 662 ('', 'abort', False, _('abort an edit in progress')),
663 663 ('o', 'outgoing', False, _('changesets not found in destination')),
664 664 ('f', 'force', False,
665 665 _('force outgoing even for unrelated repositories')),
666 666 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
667 667 _("ANCESTOR | --outgoing [URL]"))
668 668 def histedit(ui, repo, *freeargs, **opts):
669 669 """interactively edit changeset history
670 670
671 671 This command edits changesets between ANCESTOR and the parent of
672 672 the working directory.
673 673
674 674 With --outgoing, this edits changesets not found in the
675 675 destination repository. If URL of the destination is omitted, the
676 676 'default-push' (or 'default') path will be used.
677 677
678 678 For safety, this command is aborted, also if there are ambiguous
679 679 outgoing revisions which may confuse users: for example, there are
680 680 multiple branches containing outgoing revisions.
681 681
682 682 Use "min(outgoing() and ::.)" or similar revset specification
683 683 instead of --outgoing to specify edit target revision exactly in
684 684 such ambiguous situation. See :hg:`help revsets` for detail about
685 685 selecting revisions.
686 686
687 687 Returns 0 on success, 1 if user intervention is required (not only
688 688 for intentional "edit" command, but also for resolving unexpected
689 689 conflicts).
690 690 """
691 691 state = histeditstate(repo)
692 692 try:
693 693 state.wlock = repo.wlock()
694 694 state.lock = repo.lock()
695 695 _histedit(ui, repo, state, *freeargs, **opts)
696 696 finally:
697 697 release(state.lock, state.wlock)
698 698
699 699 def _histedit(ui, repo, state, *freeargs, **opts):
700 700 # TODO only abort if we try and histedit mq patches, not just
701 701 # blanket if mq patches are applied somewhere
702 702 mq = getattr(repo, 'mq', None)
703 703 if mq and mq.applied:
704 704 raise util.Abort(_('source has mq patches applied'))
705 705
706 706 # basic argument incompatibility processing
707 707 outg = opts.get('outgoing')
708 708 cont = opts.get('continue')
709 709 editplan = opts.get('edit_plan')
710 710 abort = opts.get('abort')
711 711 force = opts.get('force')
712 712 rules = opts.get('commands', '')
713 713 revs = opts.get('rev', [])
714 714 goal = 'new' # This invocation goal, in new, continue, abort
715 715 if force and not outg:
716 716 raise util.Abort(_('--force only allowed with --outgoing'))
717 717 if cont:
718 718 if any((outg, abort, revs, freeargs, rules, editplan)):
719 719 raise util.Abort(_('no arguments allowed with --continue'))
720 720 goal = 'continue'
721 721 elif abort:
722 722 if any((outg, revs, freeargs, rules, editplan)):
723 723 raise util.Abort(_('no arguments allowed with --abort'))
724 724 goal = 'abort'
725 725 elif editplan:
726 726 if any((outg, revs, freeargs)):
727 727 raise util.Abort(_('only --commands argument allowed with '
728 728 '--edit-plan'))
729 729 goal = 'edit-plan'
730 730 else:
731 731 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
732 732 raise util.Abort(_('history edit already in progress, try '
733 733 '--continue or --abort'))
734 734 if outg:
735 735 if revs:
736 736 raise util.Abort(_('no revisions allowed with --outgoing'))
737 737 if len(freeargs) > 1:
738 738 raise util.Abort(
739 739 _('only one repo argument allowed with --outgoing'))
740 740 else:
741 741 revs.extend(freeargs)
742 742 if len(revs) == 0:
743 743 histeditdefault = ui.config('histedit', 'defaultrev')
744 744 if histeditdefault:
745 745 revs.append(histeditdefault)
746 746 if len(revs) != 1:
747 747 raise util.Abort(
748 748 _('histedit requires exactly one ancestor revision'))
749 749
750 750
751 751 replacements = []
752 752 state.keep = opts.get('keep', False)
753 753
754 754 # rebuild state
755 755 if goal == 'continue':
756 756 state.read()
757 757 state = bootstrapcontinue(ui, state, opts)
758 758 elif goal == 'edit-plan':
759 759 state.read()
760 760 if not rules:
761 761 comment = editcomment % (node.short(state.parentctxnode),
762 762 node.short(state.topmost))
763 763 rules = ruleeditor(repo, ui, state.rules, comment)
764 764 else:
765 765 if rules == '-':
766 766 f = sys.stdin
767 767 else:
768 768 f = open(rules)
769 769 rules = f.read()
770 770 f.close()
771 771 rules = [l for l in (r.strip() for r in rules.splitlines())
772 772 if l and not l.startswith('#')]
773 773 rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules])
774 774 state.rules = rules
775 775 state.write()
776 776 return
777 777 elif goal == 'abort':
778 778 state.read()
779 779 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
780 780 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
781 781
782 782 # Recover our old commits if necessary
783 783 if not state.topmost in repo and state.backupfile:
784 784 backupfile = repo.join(state.backupfile)
785 785 f = hg.openpath(ui, backupfile)
786 786 gen = exchange.readbundle(ui, f, backupfile)
787 787 changegroup.addchangegroup(repo, gen, 'histedit',
788 788 'bundle:' + backupfile)
789 789 os.remove(backupfile)
790 790
791 791 # check whether we should update away
792 792 parentnodes = [c.node() for c in repo[None].parents()]
793 793 for n in leafs | set([state.parentctxnode]):
794 794 if n in parentnodes:
795 795 hg.clean(repo, state.topmost)
796 796 break
797 797 else:
798 798 pass
799 799 cleanupnode(ui, repo, 'created', tmpnodes)
800 800 cleanupnode(ui, repo, 'temp', leafs)
801 801 state.clear()
802 802 return
803 803 else:
804 804 cmdutil.checkunfinished(repo)
805 805 cmdutil.bailifchanged(repo)
806 806
807 807 topmost, empty = repo.dirstate.parents()
808 808 if outg:
809 809 if freeargs:
810 810 remote = freeargs[0]
811 811 else:
812 812 remote = None
813 813 root = findoutgoing(ui, repo, remote, force, opts)
814 814 else:
815 815 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
816 816 if len(rr) != 1:
817 817 raise util.Abort(_('The specified revisions must have '
818 818 'exactly one common root'))
819 819 root = rr[0].node()
820 820
821 821 revs = between(repo, root, topmost, state.keep)
822 822 if not revs:
823 823 raise util.Abort(_('%s is not an ancestor of working directory') %
824 824 node.short(root))
825 825
826 826 ctxs = [repo[r] for r in revs]
827 827 if not rules:
828 828 comment = editcomment % (node.short(root), node.short(topmost))
829 829 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
830 830 else:
831 831 if rules == '-':
832 832 f = sys.stdin
833 833 else:
834 834 f = open(rules)
835 835 rules = f.read()
836 836 f.close()
837 837 rules = [l for l in (r.strip() for r in rules.splitlines())
838 838 if l and not l.startswith('#')]
839 839 rules = verifyrules(rules, repo, ctxs)
840 840
841 841 parentctxnode = repo[root].parents()[0].node()
842 842
843 843 state.parentctxnode = parentctxnode
844 844 state.rules = rules
845 845 state.topmost = topmost
846 846 state.replacements = replacements
847 847
848 848 # Create a backup so we can always abort completely.
849 849 backupfile = None
850 850 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
851 851 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
852 852 'histedit')
853 853 state.backupfile = backupfile
854 854
855 855 while state.rules:
856 856 state.write()
857 857 action, ha = state.rules.pop(0)
858 858 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
859 859 actobj = actiontable[action].fromrule(state, ha)
860 860 parentctx, replacement_ = actobj.run()
861 861 state.parentctxnode = parentctx.node()
862 862 state.replacements.extend(replacement_)
863 863 state.write()
864 864
865 865 hg.update(repo, state.parentctxnode)
866 866
867 867 mapping, tmpnodes, created, ntm = processreplacement(state)
868 868 if mapping:
869 869 for prec, succs in mapping.iteritems():
870 870 if not succs:
871 871 ui.debug('histedit: %s is dropped\n' % node.short(prec))
872 872 else:
873 873 ui.debug('histedit: %s is replaced by %s\n' % (
874 874 node.short(prec), node.short(succs[0])))
875 875 if len(succs) > 1:
876 876 m = 'histedit: %s'
877 877 for n in succs[1:]:
878 878 ui.debug(m % node.short(n))
879 879
880 880 if not state.keep:
881 881 if mapping:
882 882 movebookmarks(ui, repo, mapping, state.topmost, ntm)
883 883 # TODO update mq state
884 884 if obsolete.isenabled(repo, obsolete.createmarkersopt):
885 885 markers = []
886 886 # sort by revision number because it sound "right"
887 887 for prec in sorted(mapping, key=repo.changelog.rev):
888 888 succs = mapping[prec]
889 889 markers.append((repo[prec],
890 890 tuple(repo[s] for s in succs)))
891 891 if markers:
892 892 obsolete.createmarkers(repo, markers)
893 893 else:
894 894 cleanupnode(ui, repo, 'replaced', mapping)
895 895
896 896 cleanupnode(ui, repo, 'temp', tmpnodes)
897 897 state.clear()
898 898 if os.path.exists(repo.sjoin('undo')):
899 899 os.unlink(repo.sjoin('undo'))
900 900
901 901 def bootstrapcontinue(ui, state, opts):
902 902 repo = state.repo
903 903 if state.rules:
904 904 action, currentnode = state.rules.pop(0)
905 905
906 906 actobj = actiontable[action].fromrule(state, currentnode)
907 907
908 908 s = repo.status()
909 909 if s.modified or s.added or s.removed or s.deleted:
910 910 actobj.continuedirty()
911 911 s = repo.status()
912 912 if s.modified or s.added or s.removed or s.deleted:
913 913 raise util.Abort(_("working copy still dirty"))
914 914
915 915 parentctx, replacements = actobj.continueclean()
916 916
917 917 state.parentctxnode = parentctx.node()
918 918 state.replacements.extend(replacements)
919 919
920 920 return state
921 921
922 922 def between(repo, old, new, keep):
923 923 """select and validate the set of revision to edit
924 924
925 925 When keep is false, the specified set can't have children."""
926 926 ctxs = list(repo.set('%n::%n', old, new))
927 927 if ctxs and not keep:
928 928 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
929 929 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
930 930 raise util.Abort(_('cannot edit history that would orphan nodes'))
931 931 if repo.revs('(%ld) and merge()', ctxs):
932 932 raise util.Abort(_('cannot edit history that contains merges'))
933 933 root = ctxs[0] # list is already sorted by repo.set
934 934 if not root.mutable():
935 935 raise util.Abort(_('cannot edit public changeset: %s') % root,
936 936 hint=_('see "hg help phases" for details'))
937 937 return [c.node() for c in ctxs]
938 938
939 939 def makedesc(repo, action, rev):
940 940 """build a initial action line for a ctx
941 941
942 942 line are in the form:
943 943
944 944 <action> <hash> <rev> <summary>
945 945 """
946 946 ctx = repo[rev]
947 947 summary = ''
948 948 if ctx.description():
949 949 summary = ctx.description().splitlines()[0]
950 950 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
951 951 # trim to 80 columns so it's not stupidly wide in my editor
952 952 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
953 953 maxlen = max(maxlen, 22) # avoid truncating hash
954 954 return util.ellipsis(line, maxlen)
955 955
956 956 def ruleeditor(repo, ui, rules, editcomment=""):
957 957 """open an editor to edit rules
958 958
959 959 rules are in the format [ [act, ctx], ...] like in state.rules
960 960 """
961 961 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
962 962 rules += '\n\n'
963 963 rules += editcomment
964 964 rules = ui.edit(rules, ui.username())
965 965
966 966 # Save edit rules in .hg/histedit-last-edit.txt in case
967 967 # the user needs to ask for help after something
968 968 # surprising happens.
969 969 f = open(repo.join('histedit-last-edit.txt'), 'w')
970 970 f.write(rules)
971 971 f.close()
972 972
973 973 return rules
974 974
975 975 def verifyrules(rules, repo, ctxs):
976 976 """Verify that there exists exactly one edit rule per given changeset.
977 977
978 978 Will abort if there are to many or too few rules, a malformed rule,
979 979 or a rule on a changeset outside of the user-given range.
980 980 """
981 981 parsed = []
982 982 expected = set(c.hex() for c in ctxs)
983 983 seen = set()
984 984 for r in rules:
985 985 if ' ' not in r:
986 986 raise util.Abort(_('malformed line "%s"') % r)
987 987 action, rest = r.split(' ', 1)
988 988 ha = rest.strip().split(' ', 1)[0]
989 989 try:
990 990 ha = repo[ha].hex()
991 991 except error.RepoError:
992 992 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
993 993 if ha not in expected:
994 994 raise util.Abort(
995 995 _('may not use changesets other than the ones listed'))
996 996 if ha in seen:
997 997 raise util.Abort(_('duplicated command for changeset %s') %
998 998 ha[:12])
999 999 seen.add(ha)
1000 1000 if action not in actiontable:
1001 1001 raise util.Abort(_('unknown action "%s"') % action)
1002 1002 parsed.append([action, ha])
1003 1003 missing = sorted(expected - seen) # sort to stabilize output
1004 1004 if missing:
1005 1005 raise util.Abort(_('missing rules for changeset %s') %
1006 1006 missing[0][:12],
1007 1007 hint=_('do you want to use the drop action?'))
1008 1008 return parsed
1009 1009
1010 1010 def processreplacement(state):
1011 1011 """process the list of replacements to return
1012 1012
1013 1013 1) the final mapping between original and created nodes
1014 1014 2) the list of temporary node created by histedit
1015 1015 3) the list of new commit created by histedit"""
1016 1016 replacements = state.replacements
1017 1017 allsuccs = set()
1018 1018 replaced = set()
1019 1019 fullmapping = {}
1020 1020 # initialise basic set
1021 1021 # fullmapping record all operation recorded in replacement
1022 1022 for rep in replacements:
1023 1023 allsuccs.update(rep[1])
1024 1024 replaced.add(rep[0])
1025 1025 fullmapping.setdefault(rep[0], set()).update(rep[1])
1026 1026 new = allsuccs - replaced
1027 1027 tmpnodes = allsuccs & replaced
1028 1028 # Reduce content fullmapping into direct relation between original nodes
1029 1029 # and final node created during history edition
1030 1030 # Dropped changeset are replaced by an empty list
1031 1031 toproceed = set(fullmapping)
1032 1032 final = {}
1033 1033 while toproceed:
1034 1034 for x in list(toproceed):
1035 1035 succs = fullmapping[x]
1036 1036 for s in list(succs):
1037 1037 if s in toproceed:
1038 1038 # non final node with unknown closure
1039 1039 # We can't process this now
1040 1040 break
1041 1041 elif s in final:
1042 1042 # non final node, replace with closure
1043 1043 succs.remove(s)
1044 1044 succs.update(final[s])
1045 1045 else:
1046 1046 final[x] = succs
1047 1047 toproceed.remove(x)
1048 1048 # remove tmpnodes from final mapping
1049 1049 for n in tmpnodes:
1050 1050 del final[n]
1051 1051 # we expect all changes involved in final to exist in the repo
1052 1052 # turn `final` into list (topologically sorted)
1053 1053 nm = state.repo.changelog.nodemap
1054 1054 for prec, succs in final.items():
1055 1055 final[prec] = sorted(succs, key=nm.get)
1056 1056
1057 1057 # computed topmost element (necessary for bookmark)
1058 1058 if new:
1059 1059 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1060 1060 elif not final:
1061 1061 # Nothing rewritten at all. we won't need `newtopmost`
1062 1062 # It is the same as `oldtopmost` and `processreplacement` know it
1063 1063 newtopmost = None
1064 1064 else:
1065 1065 # every body died. The newtopmost is the parent of the root.
1066 1066 r = state.repo.changelog.rev
1067 1067 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1068 1068
1069 1069 return final, tmpnodes, new, newtopmost
1070 1070
1071 1071 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1072 1072 """Move bookmark from old to newly created node"""
1073 1073 if not mapping:
1074 1074 # if nothing got rewritten there is not purpose for this function
1075 1075 return
1076 1076 moves = []
1077 1077 for bk, old in sorted(repo._bookmarks.iteritems()):
1078 1078 if old == oldtopmost:
1079 1079 # special case ensure bookmark stay on tip.
1080 1080 #
1081 1081 # This is arguably a feature and we may only want that for the
1082 1082 # active bookmark. But the behavior is kept compatible with the old
1083 1083 # version for now.
1084 1084 moves.append((bk, newtopmost))
1085 1085 continue
1086 1086 base = old
1087 1087 new = mapping.get(base, None)
1088 1088 if new is None:
1089 1089 continue
1090 1090 while not new:
1091 1091 # base is killed, trying with parent
1092 1092 base = repo[base].p1().node()
1093 1093 new = mapping.get(base, (base,))
1094 1094 # nothing to move
1095 1095 moves.append((bk, new[-1]))
1096 1096 if moves:
1097 1097 marks = repo._bookmarks
1098 1098 for mark, new in moves:
1099 1099 old = marks[mark]
1100 1100 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1101 1101 % (mark, node.short(old), node.short(new)))
1102 1102 marks[mark] = new
1103 1103 marks.write()
1104 1104
1105 1105 def cleanupnode(ui, repo, name, nodes):
1106 1106 """strip a group of nodes from the repository
1107 1107
1108 1108 The set of node to strip may contains unknown nodes."""
1109 1109 ui.debug('should strip %s nodes %s\n' %
1110 1110 (name, ', '.join([node.short(n) for n in nodes])))
1111 1111 lock = None
1112 1112 try:
1113 1113 lock = repo.lock()
1114 1114 # Find all node that need to be stripped
1115 1115 # (we hg %lr instead of %ln to silently ignore unknown item
1116 1116 nm = repo.changelog.nodemap
1117 1117 nodes = sorted(n for n in nodes if n in nm)
1118 1118 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1119 1119 for c in roots:
1120 1120 # We should process node in reverse order to strip tip most first.
1121 1121 # but this trigger a bug in changegroup hook.
1122 1122 # This would reduce bundle overhead
1123 1123 repair.strip(ui, repo, c)
1124 1124 finally:
1125 1125 release(lock)
1126 1126
1127 1127 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1128 1128 if isinstance(nodelist, str):
1129 1129 nodelist = [nodelist]
1130 1130 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1131 1131 state = histeditstate(repo)
1132 1132 state.read()
1133 1133 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1134 1134 in state.rules if rulehash in repo])
1135 1135 strip_nodes = set([repo[n].node() for n in nodelist])
1136 1136 common_nodes = histedit_nodes & strip_nodes
1137 1137 if common_nodes:
1138 1138 raise util.Abort(_("histedit in progress, can't strip %s")
1139 1139 % ', '.join(node.short(x) for x in common_nodes))
1140 1140 return orig(ui, repo, nodelist, *args, **kwargs)
1141 1141
1142 1142 extensions.wrapfunction(repair, 'strip', stripwrapper)
1143 1143
1144 1144 def summaryhook(ui, repo):
1145 1145 if not os.path.exists(repo.join('histedit-state')):
1146 1146 return
1147 1147 state = histeditstate(repo)
1148 1148 state.read()
1149 1149 if state.rules:
1150 1150 # i18n: column positioning for "hg summary"
1151 1151 ui.write(_('hist: %s (histedit --continue)\n') %
1152 1152 (ui.label(_('%d remaining'), 'histedit.remaining') %
1153 1153 len(state.rules)))
1154 1154
1155 1155 def extsetup(ui):
1156 1156 cmdutil.summaryhooks.add('histedit', summaryhook)
1157 1157 cmdutil.unfinishedstates.append(
1158 1158 ['histedit-state', False, True, _('histedit in progress'),
1159 1159 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,221 +1,221 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''base class for store implementations and store-related utility code'''
10 10
11 11 import re
12 12
13 13 from mercurial import util, node, hg
14 14 from mercurial.i18n import _
15 15
16 16 import lfutil
17 17
18 18 class StoreError(Exception):
19 19 '''Raised when there is a problem getting files from or putting
20 20 files to a central store.'''
21 21 def __init__(self, filename, hash, url, detail):
22 22 self.filename = filename
23 23 self.hash = hash
24 24 self.url = url
25 25 self.detail = detail
26 26
27 27 def longmessage(self):
28 28 return (_("error getting id %s from url %s for file %s: %s\n") %
29 29 (self.hash, util.hidepassword(self.url), self.filename,
30 30 self.detail))
31 31
32 32 def __str__(self):
33 33 return "%s: %s" % (util.hidepassword(self.url), self.detail)
34 34
35 35 class basestore(object):
36 36 def __init__(self, ui, repo, url):
37 37 self.ui = ui
38 38 self.repo = repo
39 39 self.url = url
40 40
41 41 def put(self, source, hash):
42 42 '''Put source file into the store so it can be retrieved by hash.'''
43 43 raise NotImplementedError('abstract method')
44 44
45 45 def exists(self, hashes):
46 46 '''Check to see if the store contains the given hashes. Given an
47 47 iterable of hashes it returns a mapping from hash to bool.'''
48 48 raise NotImplementedError('abstract method')
49 49
50 50 def get(self, files):
51 51 '''Get the specified largefiles from the store and write to local
52 52 files under repo.root. files is a list of (filename, hash)
53 53 tuples. Return (success, missing), lists of files successfully
54 54 downloaded and those not found in the store. success is a list
55 55 of (filename, hash) tuples; missing is a list of filenames that
56 56 we could not get. (The detailed error message will already have
57 57 been presented to the user, so missing is just supplied as a
58 58 summary.)'''
59 59 success = []
60 60 missing = []
61 61 ui = self.ui
62 62
63 63 at = 0
64 64 available = self.exists(set(hash for (_filename, hash) in files))
65 65 for filename, hash in files:
66 66 ui.progress(_('getting largefiles'), at, unit='lfile',
67 67 total=len(files))
68 68 at += 1
69 69 ui.note(_('getting %s:%s\n') % (filename, hash))
70 70
71 71 if not available.get(hash):
72 72 ui.warn(_('%s: largefile %s not available from %s\n')
73 73 % (filename, hash, util.hidepassword(self.url)))
74 74 missing.append(filename)
75 75 continue
76 76
77 77 if self._gethash(filename, hash):
78 78 success.append((filename, hash))
79 79 else:
80 80 missing.append(filename)
81 81
82 82 ui.progress(_('getting largefiles'), None)
83 83 return (success, missing)
84 84
85 85 def _gethash(self, filename, hash):
86 86 """Get file with the provided hash and store it in the local repo's
87 87 store and in the usercache.
88 88 filename is for informational messages only.
89 89 """
90 90 util.makedirs(lfutil.storepath(self.repo, ''))
91 91 storefilename = lfutil.storepath(self.repo, hash)
92 92
93 93 tmpname = storefilename + '.tmp'
94 94 tmpfile = util.atomictempfile(tmpname,
95 95 createmode=self.repo.store.createmode)
96 96
97 97 try:
98 98 gothash = self._getfile(tmpfile, filename, hash)
99 except StoreError, err:
99 except StoreError as err:
100 100 self.ui.warn(err.longmessage())
101 101 gothash = ""
102 102 tmpfile.close()
103 103
104 104 if gothash != hash:
105 105 if gothash != "":
106 106 self.ui.warn(_('%s: data corruption (expected %s, got %s)\n')
107 107 % (filename, hash, gothash))
108 108 util.unlink(tmpname)
109 109 return False
110 110
111 111 util.rename(tmpname, storefilename)
112 112 lfutil.linktousercache(self.repo, hash)
113 113 return True
114 114
115 115 def verify(self, revs, contents=False):
116 116 '''Verify the existence (and, optionally, contents) of every big
117 117 file revision referenced by every changeset in revs.
118 118 Return 0 if all is well, non-zero on any errors.'''
119 119 failed = False
120 120
121 121 self.ui.status(_('searching %d changesets for largefiles\n') %
122 122 len(revs))
123 123 verified = set() # set of (filename, filenode) tuples
124 124
125 125 for rev in revs:
126 126 cctx = self.repo[rev]
127 127 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
128 128
129 129 for standin in cctx:
130 130 if self._verifyfile(cctx, cset, contents, standin, verified):
131 131 failed = True
132 132
133 133 numrevs = len(verified)
134 134 numlfiles = len(set([fname for (fname, fnode) in verified]))
135 135 if contents:
136 136 self.ui.status(
137 137 _('verified contents of %d revisions of %d largefiles\n')
138 138 % (numrevs, numlfiles))
139 139 else:
140 140 self.ui.status(
141 141 _('verified existence of %d revisions of %d largefiles\n')
142 142 % (numrevs, numlfiles))
143 143 return int(failed)
144 144
145 145 def _getfile(self, tmpfile, filename, hash):
146 146 '''Fetch one revision of one file from the store and write it
147 147 to tmpfile. Compute the hash of the file on-the-fly as it
148 148 downloads and return the hash. Close tmpfile. Raise
149 149 StoreError if unable to download the file (e.g. it does not
150 150 exist in the store).'''
151 151 raise NotImplementedError('abstract method')
152 152
153 153 def _verifyfile(self, cctx, cset, contents, standin, verified):
154 154 '''Perform the actual verification of a file in the store.
155 155 'cset' is only used in warnings.
156 156 'contents' controls verification of content hash.
157 157 'standin' is the standin path of the largefile to verify.
158 158 'verified' is maintained as a set of already verified files.
159 159 Returns _true_ if it is a standin and any problems are found!
160 160 '''
161 161 raise NotImplementedError('abstract method')
162 162
163 163 import localstore, wirestore
164 164
165 165 _storeprovider = {
166 166 'file': [localstore.localstore],
167 167 'http': [wirestore.wirestore],
168 168 'https': [wirestore.wirestore],
169 169 'ssh': [wirestore.wirestore],
170 170 }
171 171
172 172 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
173 173
174 174 # During clone this function is passed the src's ui object
175 175 # but it needs the dest's ui object so it can read out of
176 176 # the config file. Use repo.ui instead.
177 177 def _openstore(repo, remote=None, put=False):
178 178 ui = repo.ui
179 179
180 180 if not remote:
181 181 lfpullsource = getattr(repo, 'lfpullsource', None)
182 182 if lfpullsource:
183 183 path = ui.expandpath(lfpullsource)
184 184 elif put:
185 185 path = ui.expandpath('default-push', 'default')
186 186 else:
187 187 path = ui.expandpath('default')
188 188
189 189 # ui.expandpath() leaves 'default-push' and 'default' alone if
190 190 # they cannot be expanded: fallback to the empty string,
191 191 # meaning the current directory.
192 192 if path == 'default-push' or path == 'default':
193 193 path = ''
194 194 remote = repo
195 195 else:
196 196 path, _branches = hg.parseurl(path)
197 197 remote = hg.peer(repo, {}, path)
198 198
199 199 # The path could be a scheme so use Mercurial's normal functionality
200 200 # to resolve the scheme to a repository and use its path
201 201 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
202 202
203 203 match = _scheme_re.match(path)
204 204 if not match: # regular filesystem path
205 205 scheme = 'file'
206 206 else:
207 207 scheme = match.group(1)
208 208
209 209 try:
210 210 storeproviders = _storeprovider[scheme]
211 211 except KeyError:
212 212 raise util.Abort(_('unsupported URL scheme %r') % scheme)
213 213
214 214 for classobj in storeproviders:
215 215 try:
216 216 return classobj(ui, repo, remote)
217 217 except lfutil.storeprotonotcapable:
218 218 pass
219 219
220 220 raise util.Abort(_('%s does not appear to be a largefile store') %
221 221 util.hidepassword(path))
@@ -1,550 +1,550 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''High-level command function for lfconvert, plus the cmdtable.'''
10 10
11 11 import os, errno
12 12 import shutil
13 13
14 14 from mercurial import util, match as match_, hg, node, context, error, \
15 15 cmdutil, scmutil, commands
16 16 from mercurial.i18n import _
17 17 from mercurial.lock import release
18 18
19 19 from hgext.convert import convcmd
20 20 from hgext.convert import filemap
21 21
22 22 import lfutil
23 23 import basestore
24 24
25 25 # -- Commands ----------------------------------------------------------
26 26
27 27 cmdtable = {}
28 28 command = cmdutil.command(cmdtable)
29 29
30 30 @command('lfconvert',
31 31 [('s', 'size', '',
32 32 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
33 33 ('', 'to-normal', False,
34 34 _('convert from a largefiles repo to a normal repo')),
35 35 ],
36 36 _('hg lfconvert SOURCE DEST [FILE ...]'),
37 37 norepo=True,
38 38 inferrepo=True)
39 39 def lfconvert(ui, src, dest, *pats, **opts):
40 40 '''convert a normal repository to a largefiles repository
41 41
42 42 Convert repository SOURCE to a new repository DEST, identical to
43 43 SOURCE except that certain files will be converted as largefiles:
44 44 specifically, any file that matches any PATTERN *or* whose size is
45 45 above the minimum size threshold is converted as a largefile. The
46 46 size used to determine whether or not to track a file as a
47 47 largefile is the size of the first version of the file. The
48 48 minimum size can be specified either with --size or in
49 49 configuration as ``largefiles.size``.
50 50
51 51 After running this command you will need to make sure that
52 52 largefiles is enabled anywhere you intend to push the new
53 53 repository.
54 54
55 55 Use --to-normal to convert largefiles back to normal files; after
56 56 this, the DEST repository can be used without largefiles at all.'''
57 57
58 58 if opts['to_normal']:
59 59 tolfile = False
60 60 else:
61 61 tolfile = True
62 62 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
63 63
64 64 if not hg.islocal(src):
65 65 raise util.Abort(_('%s is not a local Mercurial repo') % src)
66 66 if not hg.islocal(dest):
67 67 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
68 68
69 69 rsrc = hg.repository(ui, src)
70 70 ui.status(_('initializing destination %s\n') % dest)
71 71 rdst = hg.repository(ui, dest, create=True)
72 72
73 73 success = False
74 74 dstwlock = dstlock = None
75 75 try:
76 76 # Get a list of all changesets in the source. The easy way to do this
77 77 # is to simply walk the changelog, using changelog.nodesbetween().
78 78 # Take a look at mercurial/revlog.py:639 for more details.
79 79 # Use a generator instead of a list to decrease memory usage
80 80 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
81 81 rsrc.heads())[0])
82 82 revmap = {node.nullid: node.nullid}
83 83 if tolfile:
84 84 # Lock destination to prevent modification while it is converted to.
85 85 # Don't need to lock src because we are just reading from its
86 86 # history which can't change.
87 87 dstwlock = rdst.wlock()
88 88 dstlock = rdst.lock()
89 89
90 90 lfiles = set()
91 91 normalfiles = set()
92 92 if not pats:
93 93 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
94 94 if pats:
95 95 matcher = match_.match(rsrc.root, '', list(pats))
96 96 else:
97 97 matcher = None
98 98
99 99 lfiletohash = {}
100 100 for ctx in ctxs:
101 101 ui.progress(_('converting revisions'), ctx.rev(),
102 102 unit=_('revision'), total=rsrc['tip'].rev())
103 103 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
104 104 lfiles, normalfiles, matcher, size, lfiletohash)
105 105 ui.progress(_('converting revisions'), None)
106 106
107 107 if os.path.exists(rdst.wjoin(lfutil.shortname)):
108 108 shutil.rmtree(rdst.wjoin(lfutil.shortname))
109 109
110 110 for f in lfiletohash.keys():
111 111 if os.path.isfile(rdst.wjoin(f)):
112 112 os.unlink(rdst.wjoin(f))
113 113 try:
114 114 os.removedirs(os.path.dirname(rdst.wjoin(f)))
115 115 except OSError:
116 116 pass
117 117
118 118 # If there were any files converted to largefiles, add largefiles
119 119 # to the destination repository's requirements.
120 120 if lfiles:
121 121 rdst.requirements.add('largefiles')
122 122 rdst._writerequirements()
123 123 else:
124 124 class lfsource(filemap.filemap_source):
125 125 def __init__(self, ui, source):
126 126 super(lfsource, self).__init__(ui, source, None)
127 127 self.filemapper.rename[lfutil.shortname] = '.'
128 128
129 129 def getfile(self, name, rev):
130 130 realname, realrev = rev
131 131 f = super(lfsource, self).getfile(name, rev)
132 132
133 133 if (not realname.startswith(lfutil.shortnameslash)
134 134 or f[0] is None):
135 135 return f
136 136
137 137 # Substitute in the largefile data for the hash
138 138 hash = f[0].strip()
139 139 path = lfutil.findfile(rsrc, hash)
140 140
141 141 if path is None:
142 142 raise util.Abort(_("missing largefile for \'%s\' in %s")
143 143 % (realname, realrev))
144 144 fp = open(path, 'rb')
145 145
146 146 try:
147 147 return (fp.read(), f[1])
148 148 finally:
149 149 fp.close()
150 150
151 151 class converter(convcmd.converter):
152 152 def __init__(self, ui, source, dest, revmapfile, opts):
153 153 src = lfsource(ui, source)
154 154
155 155 super(converter, self).__init__(ui, src, dest, revmapfile,
156 156 opts)
157 157
158 158 found, missing = downloadlfiles(ui, rsrc)
159 159 if missing != 0:
160 160 raise util.Abort(_("all largefiles must be present locally"))
161 161
162 162 orig = convcmd.converter
163 163 convcmd.converter = converter
164 164
165 165 try:
166 166 convcmd.convert(ui, src, dest)
167 167 finally:
168 168 convcmd.converter = orig
169 169 success = True
170 170 finally:
171 171 if tolfile:
172 172 rdst.dirstate.clear()
173 173 release(dstlock, dstwlock)
174 174 if not success:
175 175 # we failed, remove the new directory
176 176 shutil.rmtree(rdst.root)
177 177
178 178 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
179 179 matcher, size, lfiletohash):
180 180 # Convert src parents to dst parents
181 181 parents = _convertparents(ctx, revmap)
182 182
183 183 # Generate list of changed files
184 184 files = _getchangedfiles(ctx, parents)
185 185
186 186 dstfiles = []
187 187 for f in files:
188 188 if f not in lfiles and f not in normalfiles:
189 189 islfile = _islfile(f, ctx, matcher, size)
190 190 # If this file was renamed or copied then copy
191 191 # the largefile-ness of its predecessor
192 192 if f in ctx.manifest():
193 193 fctx = ctx.filectx(f)
194 194 renamed = fctx.renamed()
195 195 renamedlfile = renamed and renamed[0] in lfiles
196 196 islfile |= renamedlfile
197 197 if 'l' in fctx.flags():
198 198 if renamedlfile:
199 199 raise util.Abort(
200 200 _('renamed/copied largefile %s becomes symlink')
201 201 % f)
202 202 islfile = False
203 203 if islfile:
204 204 lfiles.add(f)
205 205 else:
206 206 normalfiles.add(f)
207 207
208 208 if f in lfiles:
209 209 dstfiles.append(lfutil.standin(f))
210 210 # largefile in manifest if it has not been removed/renamed
211 211 if f in ctx.manifest():
212 212 fctx = ctx.filectx(f)
213 213 if 'l' in fctx.flags():
214 214 renamed = fctx.renamed()
215 215 if renamed and renamed[0] in lfiles:
216 216 raise util.Abort(_('largefile %s becomes symlink') % f)
217 217
218 218 # largefile was modified, update standins
219 219 m = util.sha1('')
220 220 m.update(ctx[f].data())
221 221 hash = m.hexdigest()
222 222 if f not in lfiletohash or lfiletohash[f] != hash:
223 223 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
224 224 executable = 'x' in ctx[f].flags()
225 225 lfutil.writestandin(rdst, lfutil.standin(f), hash,
226 226 executable)
227 227 lfiletohash[f] = hash
228 228 else:
229 229 # normal file
230 230 dstfiles.append(f)
231 231
232 232 def getfilectx(repo, memctx, f):
233 233 if lfutil.isstandin(f):
234 234 # if the file isn't in the manifest then it was removed
235 235 # or renamed, raise IOError to indicate this
236 236 srcfname = lfutil.splitstandin(f)
237 237 try:
238 238 fctx = ctx.filectx(srcfname)
239 239 except error.LookupError:
240 240 return None
241 241 renamed = fctx.renamed()
242 242 if renamed:
243 243 # standin is always a largefile because largefile-ness
244 244 # doesn't change after rename or copy
245 245 renamed = lfutil.standin(renamed[0])
246 246
247 247 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
248 248 'l' in fctx.flags(), 'x' in fctx.flags(),
249 249 renamed)
250 250 else:
251 251 return _getnormalcontext(repo, ctx, f, revmap)
252 252
253 253 # Commit
254 254 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
255 255
256 256 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
257 257 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
258 258 getfilectx, ctx.user(), ctx.date(), ctx.extra())
259 259 ret = rdst.commitctx(mctx)
260 260 lfutil.copyalltostore(rdst, ret)
261 261 rdst.setparents(ret)
262 262 revmap[ctx.node()] = rdst.changelog.tip()
263 263
264 264 # Generate list of changed files
265 265 def _getchangedfiles(ctx, parents):
266 266 files = set(ctx.files())
267 267 if node.nullid not in parents:
268 268 mc = ctx.manifest()
269 269 mp1 = ctx.parents()[0].manifest()
270 270 mp2 = ctx.parents()[1].manifest()
271 271 files |= (set(mp1) | set(mp2)) - set(mc)
272 272 for f in mc:
273 273 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
274 274 files.add(f)
275 275 return files
276 276
277 277 # Convert src parents to dst parents
278 278 def _convertparents(ctx, revmap):
279 279 parents = []
280 280 for p in ctx.parents():
281 281 parents.append(revmap[p.node()])
282 282 while len(parents) < 2:
283 283 parents.append(node.nullid)
284 284 return parents
285 285
286 286 # Get memfilectx for a normal file
287 287 def _getnormalcontext(repo, ctx, f, revmap):
288 288 try:
289 289 fctx = ctx.filectx(f)
290 290 except error.LookupError:
291 291 return None
292 292 renamed = fctx.renamed()
293 293 if renamed:
294 294 renamed = renamed[0]
295 295
296 296 data = fctx.data()
297 297 if f == '.hgtags':
298 298 data = _converttags (repo.ui, revmap, data)
299 299 return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
300 300 'x' in fctx.flags(), renamed)
301 301
302 302 # Remap tag data using a revision map
303 303 def _converttags(ui, revmap, data):
304 304 newdata = []
305 305 for line in data.splitlines():
306 306 try:
307 307 id, name = line.split(' ', 1)
308 308 except ValueError:
309 309 ui.warn(_('skipping incorrectly formatted tag %s\n')
310 310 % line)
311 311 continue
312 312 try:
313 313 newid = node.bin(id)
314 314 except TypeError:
315 315 ui.warn(_('skipping incorrectly formatted id %s\n')
316 316 % id)
317 317 continue
318 318 try:
319 319 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
320 320 name))
321 321 except KeyError:
322 322 ui.warn(_('no mapping for id %s\n') % id)
323 323 continue
324 324 return ''.join(newdata)
325 325
326 326 def _islfile(file, ctx, matcher, size):
327 327 '''Return true if file should be considered a largefile, i.e.
328 328 matcher matches it or it is larger than size.'''
329 329 # never store special .hg* files as largefiles
330 330 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
331 331 return False
332 332 if matcher and matcher(file):
333 333 return True
334 334 try:
335 335 return ctx.filectx(file).size() >= size * 1024 * 1024
336 336 except error.LookupError:
337 337 return False
338 338
339 339 def uploadlfiles(ui, rsrc, rdst, files):
340 340 '''upload largefiles to the central store'''
341 341
342 342 if not files:
343 343 return
344 344
345 345 store = basestore._openstore(rsrc, rdst, put=True)
346 346
347 347 at = 0
348 348 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
349 349 retval = store.exists(files)
350 350 files = filter(lambda h: not retval[h], files)
351 351 ui.debug("%d largefiles need to be uploaded\n" % len(files))
352 352
353 353 for hash in files:
354 354 ui.progress(_('uploading largefiles'), at, unit='largefile',
355 355 total=len(files))
356 356 source = lfutil.findfile(rsrc, hash)
357 357 if not source:
358 358 raise util.Abort(_('largefile %s missing from store'
359 359 ' (needs to be uploaded)') % hash)
360 360 # XXX check for errors here
361 361 store.put(source, hash)
362 362 at += 1
363 363 ui.progress(_('uploading largefiles'), None)
364 364
365 365 def verifylfiles(ui, repo, all=False, contents=False):
366 366 '''Verify that every largefile revision in the current changeset
367 367 exists in the central store. With --contents, also verify that
368 368 the contents of each local largefile file revision are correct (SHA-1 hash
369 369 matches the revision ID). With --all, check every changeset in
370 370 this repository.'''
371 371 if all:
372 372 revs = repo.revs('all()')
373 373 else:
374 374 revs = ['.']
375 375
376 376 store = basestore._openstore(repo)
377 377 return store.verify(revs, contents=contents)
378 378
379 379 def cachelfiles(ui, repo, node, filelist=None):
380 380 '''cachelfiles ensures that all largefiles needed by the specified revision
381 381 are present in the repository's largefile cache.
382 382
383 383 returns a tuple (cached, missing). cached is the list of files downloaded
384 384 by this operation; missing is the list of files that were needed but could
385 385 not be found.'''
386 386 lfiles = lfutil.listlfiles(repo, node)
387 387 if filelist:
388 388 lfiles = set(lfiles) & set(filelist)
389 389 toget = []
390 390
391 391 for lfile in lfiles:
392 392 try:
393 393 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
394 except IOError, err:
394 except IOError as err:
395 395 if err.errno == errno.ENOENT:
396 396 continue # node must be None and standin wasn't found in wctx
397 397 raise
398 398 if not lfutil.findfile(repo, expectedhash):
399 399 toget.append((lfile, expectedhash))
400 400
401 401 if toget:
402 402 store = basestore._openstore(repo)
403 403 ret = store.get(toget)
404 404 return ret
405 405
406 406 return ([], [])
407 407
408 408 def downloadlfiles(ui, repo, rev=None):
409 409 matchfn = scmutil.match(repo[None],
410 410 [repo.wjoin(lfutil.shortname)], {})
411 411 def prepare(ctx, fns):
412 412 pass
413 413 totalsuccess = 0
414 414 totalmissing = 0
415 415 if rev != []: # walkchangerevs on empty list would return all revs
416 416 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
417 417 prepare):
418 418 success, missing = cachelfiles(ui, repo, ctx.node())
419 419 totalsuccess += len(success)
420 420 totalmissing += len(missing)
421 421 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
422 422 if totalmissing > 0:
423 423 ui.status(_("%d largefiles failed to download\n") % totalmissing)
424 424 return totalsuccess, totalmissing
425 425
426 426 def updatelfiles(ui, repo, filelist=None, printmessage=None,
427 427 normallookup=False):
428 428 '''Update largefiles according to standins in the working directory
429 429
430 430 If ``printmessage`` is other than ``None``, it means "print (or
431 431 ignore, for false) message forcibly".
432 432 '''
433 433 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
434 434 wlock = repo.wlock()
435 435 try:
436 436 lfdirstate = lfutil.openlfdirstate(ui, repo)
437 437 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
438 438
439 439 if filelist is not None:
440 440 filelist = set(filelist)
441 441 lfiles = [f for f in lfiles if f in filelist]
442 442
443 443 update = {}
444 444 updated, removed = 0, 0
445 445 for lfile in lfiles:
446 446 abslfile = repo.wjoin(lfile)
447 447 absstandin = repo.wjoin(lfutil.standin(lfile))
448 448 if os.path.exists(absstandin):
449 449 if (os.path.exists(absstandin + '.orig') and
450 450 os.path.exists(abslfile)):
451 451 shutil.copyfile(abslfile, abslfile + '.orig')
452 452 util.unlinkpath(absstandin + '.orig')
453 453 expecthash = lfutil.readstandin(repo, lfile)
454 454 if expecthash != '':
455 455 if lfile not in repo[None]: # not switched to normal file
456 456 util.unlinkpath(abslfile, ignoremissing=True)
457 457 # use normallookup() to allocate an entry in largefiles
458 458 # dirstate to prevent lfilesrepo.status() from reporting
459 459 # missing files as removed.
460 460 lfdirstate.normallookup(lfile)
461 461 update[lfile] = expecthash
462 462 else:
463 463 # Remove lfiles for which the standin is deleted, unless the
464 464 # lfile is added to the repository again. This happens when a
465 465 # largefile is converted back to a normal file: the standin
466 466 # disappears, but a new (normal) file appears as the lfile.
467 467 if (os.path.exists(abslfile) and
468 468 repo.dirstate.normalize(lfile) not in repo[None]):
469 469 util.unlinkpath(abslfile)
470 470 removed += 1
471 471
472 472 # largefile processing might be slow and be interrupted - be prepared
473 473 lfdirstate.write()
474 474
475 475 if lfiles:
476 476 statuswriter(_('getting changed largefiles\n'))
477 477 cachelfiles(ui, repo, None, lfiles)
478 478
479 479 for lfile in lfiles:
480 480 update1 = 0
481 481
482 482 expecthash = update.get(lfile)
483 483 if expecthash:
484 484 if not lfutil.copyfromcache(repo, expecthash, lfile):
485 485 # failed ... but already removed and set to normallookup
486 486 continue
487 487 # Synchronize largefile dirstate to the last modified
488 488 # time of the file
489 489 lfdirstate.normal(lfile)
490 490 update1 = 1
491 491
492 492 # copy the state of largefile standin from the repository's
493 493 # dirstate to its state in the lfdirstate.
494 494 abslfile = repo.wjoin(lfile)
495 495 absstandin = repo.wjoin(lfutil.standin(lfile))
496 496 if os.path.exists(absstandin):
497 497 mode = os.stat(absstandin).st_mode
498 498 if mode != os.stat(abslfile).st_mode:
499 499 os.chmod(abslfile, mode)
500 500 update1 = 1
501 501
502 502 updated += update1
503 503
504 504 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
505 505
506 506 lfdirstate.write()
507 507 if lfiles:
508 508 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
509 509 removed))
510 510 finally:
511 511 wlock.release()
512 512
513 513 @command('lfpull',
514 514 [('r', 'rev', [], _('pull largefiles for these revisions'))
515 515 ] + commands.remoteopts,
516 516 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
517 517 def lfpull(ui, repo, source="default", **opts):
518 518 """pull largefiles for the specified revisions from the specified source
519 519
520 520 Pull largefiles that are referenced from local changesets but missing
521 521 locally, pulling from a remote repository to the local cache.
522 522
523 523 If SOURCE is omitted, the 'default' path will be used.
524 524 See :hg:`help urls` for more information.
525 525
526 526 .. container:: verbose
527 527
528 528 Some examples:
529 529
530 530 - pull largefiles for all branch heads::
531 531
532 532 hg lfpull -r "head() and not closed()"
533 533
534 534 - pull largefiles on the default branch::
535 535
536 536 hg lfpull -r "branch(default)"
537 537 """
538 538 repo.lfpullsource = source
539 539
540 540 revs = opts.get('rev', [])
541 541 if not revs:
542 542 raise util.Abort(_('no revisions specified'))
543 543 revs = scmutil.revrange(repo, revs)
544 544
545 545 numcached = 0
546 546 for rev in revs:
547 547 ui.note(_('pulling largefiles for revision %s\n') % rev)
548 548 (cached, missing) = cachelfiles(ui, repo, rev)
549 549 numcached += len(cached)
550 550 ui.status(_("%d largefiles cached\n") % numcached)
@@ -1,1385 +1,1385 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''Overridden Mercurial commands and functions for the largefiles extension'''
10 10
11 11 import os
12 12 import copy
13 13
14 14 from mercurial import hg, util, cmdutil, scmutil, match as match_, \
15 15 archival, pathutil, revset
16 16 from mercurial.i18n import _
17 17
18 18 import lfutil
19 19 import lfcommands
20 20 import basestore
21 21
22 22 # -- Utility functions: commonly/repeatedly needed functionality ---------------
23 23
24 24 def composelargefilematcher(match, manifest):
25 25 '''create a matcher that matches only the largefiles in the original
26 26 matcher'''
27 27 m = copy.copy(match)
28 28 lfile = lambda f: lfutil.standin(f) in manifest
29 29 m._files = filter(lfile, m._files)
30 30 m._fileroots = set(m._files)
31 31 m._always = False
32 32 origmatchfn = m.matchfn
33 33 m.matchfn = lambda f: lfile(f) and origmatchfn(f)
34 34 return m
35 35
36 36 def composenormalfilematcher(match, manifest, exclude=None):
37 37 excluded = set()
38 38 if exclude is not None:
39 39 excluded.update(exclude)
40 40
41 41 m = copy.copy(match)
42 42 notlfile = lambda f: not (lfutil.isstandin(f) or lfutil.standin(f) in
43 43 manifest or f in excluded)
44 44 m._files = filter(notlfile, m._files)
45 45 m._fileroots = set(m._files)
46 46 m._always = False
47 47 origmatchfn = m.matchfn
48 48 m.matchfn = lambda f: notlfile(f) and origmatchfn(f)
49 49 return m
50 50
51 51 def installnormalfilesmatchfn(manifest):
52 52 '''installmatchfn with a matchfn that ignores all largefiles'''
53 53 def overridematch(ctx, pats=[], opts={}, globbed=False,
54 54 default='relpath', badfn=None):
55 55 match = oldmatch(ctx, pats, opts, globbed, default, badfn=badfn)
56 56 return composenormalfilematcher(match, manifest)
57 57 oldmatch = installmatchfn(overridematch)
58 58
59 59 def installmatchfn(f):
60 60 '''monkey patch the scmutil module with a custom match function.
61 61 Warning: it is monkey patching the _module_ on runtime! Not thread safe!'''
62 62 oldmatch = scmutil.match
63 63 setattr(f, 'oldmatch', oldmatch)
64 64 scmutil.match = f
65 65 return oldmatch
66 66
67 67 def restorematchfn():
68 68 '''restores scmutil.match to what it was before installmatchfn
69 69 was called. no-op if scmutil.match is its original function.
70 70
71 71 Note that n calls to installmatchfn will require n calls to
72 72 restore the original matchfn.'''
73 73 scmutil.match = getattr(scmutil.match, 'oldmatch')
74 74
75 75 def installmatchandpatsfn(f):
76 76 oldmatchandpats = scmutil.matchandpats
77 77 setattr(f, 'oldmatchandpats', oldmatchandpats)
78 78 scmutil.matchandpats = f
79 79 return oldmatchandpats
80 80
81 81 def restorematchandpatsfn():
82 82 '''restores scmutil.matchandpats to what it was before
83 83 installmatchandpatsfn was called. No-op if scmutil.matchandpats
84 84 is its original function.
85 85
86 86 Note that n calls to installmatchandpatsfn will require n calls
87 87 to restore the original matchfn.'''
88 88 scmutil.matchandpats = getattr(scmutil.matchandpats, 'oldmatchandpats',
89 89 scmutil.matchandpats)
90 90
91 91 def addlargefiles(ui, repo, isaddremove, matcher, **opts):
92 92 large = opts.get('large')
93 93 lfsize = lfutil.getminsize(
94 94 ui, lfutil.islfilesrepo(repo), opts.get('lfsize'))
95 95
96 96 lfmatcher = None
97 97 if lfutil.islfilesrepo(repo):
98 98 lfpats = ui.configlist(lfutil.longname, 'patterns', default=[])
99 99 if lfpats:
100 100 lfmatcher = match_.match(repo.root, '', list(lfpats))
101 101
102 102 lfnames = []
103 103 m = matcher
104 104
105 105 wctx = repo[None]
106 106 for f in repo.walk(match_.badmatch(m, lambda x, y: None)):
107 107 exact = m.exact(f)
108 108 lfile = lfutil.standin(f) in wctx
109 109 nfile = f in wctx
110 110 exists = lfile or nfile
111 111
112 112 # addremove in core gets fancy with the name, add doesn't
113 113 if isaddremove:
114 114 name = m.uipath(f)
115 115 else:
116 116 name = m.rel(f)
117 117
118 118 # Don't warn the user when they attempt to add a normal tracked file.
119 119 # The normal add code will do that for us.
120 120 if exact and exists:
121 121 if lfile:
122 122 ui.warn(_('%s already a largefile\n') % name)
123 123 continue
124 124
125 125 if (exact or not exists) and not lfutil.isstandin(f):
126 126 # In case the file was removed previously, but not committed
127 127 # (issue3507)
128 128 if not repo.wvfs.exists(f):
129 129 continue
130 130
131 131 abovemin = (lfsize and
132 132 repo.wvfs.lstat(f).st_size >= lfsize * 1024 * 1024)
133 133 if large or abovemin or (lfmatcher and lfmatcher(f)):
134 134 lfnames.append(f)
135 135 if ui.verbose or not exact:
136 136 ui.status(_('adding %s as a largefile\n') % name)
137 137
138 138 bad = []
139 139
140 140 # Need to lock, otherwise there could be a race condition between
141 141 # when standins are created and added to the repo.
142 142 wlock = repo.wlock()
143 143 try:
144 144 if not opts.get('dry_run'):
145 145 standins = []
146 146 lfdirstate = lfutil.openlfdirstate(ui, repo)
147 147 for f in lfnames:
148 148 standinname = lfutil.standin(f)
149 149 lfutil.writestandin(repo, standinname, hash='',
150 150 executable=lfutil.getexecutable(repo.wjoin(f)))
151 151 standins.append(standinname)
152 152 if lfdirstate[f] == 'r':
153 153 lfdirstate.normallookup(f)
154 154 else:
155 155 lfdirstate.add(f)
156 156 lfdirstate.write()
157 157 bad += [lfutil.splitstandin(f)
158 158 for f in repo[None].add(standins)
159 159 if f in m.files()]
160 160
161 161 added = [f for f in lfnames if f not in bad]
162 162 finally:
163 163 wlock.release()
164 164 return added, bad
165 165
166 166 def removelargefiles(ui, repo, isaddremove, matcher, **opts):
167 167 after = opts.get('after')
168 168 m = composelargefilematcher(matcher, repo[None].manifest())
169 169 try:
170 170 repo.lfstatus = True
171 171 s = repo.status(match=m, clean=not isaddremove)
172 172 finally:
173 173 repo.lfstatus = False
174 174 manifest = repo[None].manifest()
175 175 modified, added, deleted, clean = [[f for f in list
176 176 if lfutil.standin(f) in manifest]
177 177 for list in (s.modified, s.added,
178 178 s.deleted, s.clean)]
179 179
180 180 def warn(files, msg):
181 181 for f in files:
182 182 ui.warn(msg % m.rel(f))
183 183 return int(len(files) > 0)
184 184
185 185 result = 0
186 186
187 187 if after:
188 188 remove = deleted
189 189 result = warn(modified + added + clean,
190 190 _('not removing %s: file still exists\n'))
191 191 else:
192 192 remove = deleted + clean
193 193 result = warn(modified, _('not removing %s: file is modified (use -f'
194 194 ' to force removal)\n'))
195 195 result = warn(added, _('not removing %s: file has been marked for add'
196 196 ' (use forget to undo)\n')) or result
197 197
198 198 # Need to lock because standin files are deleted then removed from the
199 199 # repository and we could race in-between.
200 200 wlock = repo.wlock()
201 201 try:
202 202 lfdirstate = lfutil.openlfdirstate(ui, repo)
203 203 for f in sorted(remove):
204 204 if ui.verbose or not m.exact(f):
205 205 # addremove in core gets fancy with the name, remove doesn't
206 206 if isaddremove:
207 207 name = m.uipath(f)
208 208 else:
209 209 name = m.rel(f)
210 210 ui.status(_('removing %s\n') % name)
211 211
212 212 if not opts.get('dry_run'):
213 213 if not after:
214 214 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
215 215
216 216 if opts.get('dry_run'):
217 217 return result
218 218
219 219 remove = [lfutil.standin(f) for f in remove]
220 220 # If this is being called by addremove, let the original addremove
221 221 # function handle this.
222 222 if not isaddremove:
223 223 for f in remove:
224 224 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
225 225 repo[None].forget(remove)
226 226
227 227 for f in remove:
228 228 lfutil.synclfdirstate(repo, lfdirstate, lfutil.splitstandin(f),
229 229 False)
230 230
231 231 lfdirstate.write()
232 232 finally:
233 233 wlock.release()
234 234
235 235 return result
236 236
237 237 # For overriding mercurial.hgweb.webcommands so that largefiles will
238 238 # appear at their right place in the manifests.
239 239 def decodepath(orig, path):
240 240 return lfutil.splitstandin(path) or path
241 241
242 242 # -- Wrappers: modify existing commands --------------------------------
243 243
244 244 def overrideadd(orig, ui, repo, *pats, **opts):
245 245 if opts.get('normal') and opts.get('large'):
246 246 raise util.Abort(_('--normal cannot be used with --large'))
247 247 return orig(ui, repo, *pats, **opts)
248 248
249 249 def cmdutiladd(orig, ui, repo, matcher, prefix, explicitonly, **opts):
250 250 # The --normal flag short circuits this override
251 251 if opts.get('normal'):
252 252 return orig(ui, repo, matcher, prefix, explicitonly, **opts)
253 253
254 254 ladded, lbad = addlargefiles(ui, repo, False, matcher, **opts)
255 255 normalmatcher = composenormalfilematcher(matcher, repo[None].manifest(),
256 256 ladded)
257 257 bad = orig(ui, repo, normalmatcher, prefix, explicitonly, **opts)
258 258
259 259 bad.extend(f for f in lbad)
260 260 return bad
261 261
262 262 def cmdutilremove(orig, ui, repo, matcher, prefix, after, force, subrepos):
263 263 normalmatcher = composenormalfilematcher(matcher, repo[None].manifest())
264 264 result = orig(ui, repo, normalmatcher, prefix, after, force, subrepos)
265 265 return removelargefiles(ui, repo, False, matcher, after=after,
266 266 force=force) or result
267 267
268 268 def overridestatusfn(orig, repo, rev2, **opts):
269 269 try:
270 270 repo._repo.lfstatus = True
271 271 return orig(repo, rev2, **opts)
272 272 finally:
273 273 repo._repo.lfstatus = False
274 274
275 275 def overridestatus(orig, ui, repo, *pats, **opts):
276 276 try:
277 277 repo.lfstatus = True
278 278 return orig(ui, repo, *pats, **opts)
279 279 finally:
280 280 repo.lfstatus = False
281 281
282 282 def overridedirty(orig, repo, ignoreupdate=False):
283 283 try:
284 284 repo._repo.lfstatus = True
285 285 return orig(repo, ignoreupdate)
286 286 finally:
287 287 repo._repo.lfstatus = False
288 288
289 289 def overridelog(orig, ui, repo, *pats, **opts):
290 290 def overridematchandpats(ctx, pats=[], opts={}, globbed=False,
291 291 default='relpath', badfn=None):
292 292 """Matcher that merges root directory with .hglf, suitable for log.
293 293 It is still possible to match .hglf directly.
294 294 For any listed files run log on the standin too.
295 295 matchfn tries both the given filename and with .hglf stripped.
296 296 """
297 297 matchandpats = oldmatchandpats(ctx, pats, opts, globbed, default,
298 298 badfn=badfn)
299 299 m, p = copy.copy(matchandpats)
300 300
301 301 if m.always():
302 302 # We want to match everything anyway, so there's no benefit trying
303 303 # to add standins.
304 304 return matchandpats
305 305
306 306 pats = set(p)
307 307
308 308 def fixpats(pat, tostandin=lfutil.standin):
309 309 if pat.startswith('set:'):
310 310 return pat
311 311
312 312 kindpat = match_._patsplit(pat, None)
313 313
314 314 if kindpat[0] is not None:
315 315 return kindpat[0] + ':' + tostandin(kindpat[1])
316 316 return tostandin(kindpat[1])
317 317
318 318 if m._cwd:
319 319 hglf = lfutil.shortname
320 320 back = util.pconvert(m.rel(hglf)[:-len(hglf)])
321 321
322 322 def tostandin(f):
323 323 # The file may already be a standin, so trucate the back
324 324 # prefix and test before mangling it. This avoids turning
325 325 # 'glob:../.hglf/foo*' into 'glob:../.hglf/../.hglf/foo*'.
326 326 if f.startswith(back) and lfutil.splitstandin(f[len(back):]):
327 327 return f
328 328
329 329 # An absolute path is from outside the repo, so truncate the
330 330 # path to the root before building the standin. Otherwise cwd
331 331 # is somewhere in the repo, relative to root, and needs to be
332 332 # prepended before building the standin.
333 333 if os.path.isabs(m._cwd):
334 334 f = f[len(back):]
335 335 else:
336 336 f = m._cwd + '/' + f
337 337 return back + lfutil.standin(f)
338 338
339 339 pats.update(fixpats(f, tostandin) for f in p)
340 340 else:
341 341 def tostandin(f):
342 342 if lfutil.splitstandin(f):
343 343 return f
344 344 return lfutil.standin(f)
345 345 pats.update(fixpats(f, tostandin) for f in p)
346 346
347 347 for i in range(0, len(m._files)):
348 348 # Don't add '.hglf' to m.files, since that is already covered by '.'
349 349 if m._files[i] == '.':
350 350 continue
351 351 standin = lfutil.standin(m._files[i])
352 352 # If the "standin" is a directory, append instead of replace to
353 353 # support naming a directory on the command line with only
354 354 # largefiles. The original directory is kept to support normal
355 355 # files.
356 356 if standin in repo[ctx.node()]:
357 357 m._files[i] = standin
358 358 elif m._files[i] not in repo[ctx.node()] \
359 359 and repo.wvfs.isdir(standin):
360 360 m._files.append(standin)
361 361
362 362 m._fileroots = set(m._files)
363 363 m._always = False
364 364 origmatchfn = m.matchfn
365 365 def lfmatchfn(f):
366 366 lf = lfutil.splitstandin(f)
367 367 if lf is not None and origmatchfn(lf):
368 368 return True
369 369 r = origmatchfn(f)
370 370 return r
371 371 m.matchfn = lfmatchfn
372 372
373 373 ui.debug('updated patterns: %s\n' % sorted(pats))
374 374 return m, pats
375 375
376 376 # For hg log --patch, the match object is used in two different senses:
377 377 # (1) to determine what revisions should be printed out, and
378 378 # (2) to determine what files to print out diffs for.
379 379 # The magic matchandpats override should be used for case (1) but not for
380 380 # case (2).
381 381 def overridemakelogfilematcher(repo, pats, opts, badfn=None):
382 382 wctx = repo[None]
383 383 match, pats = oldmatchandpats(wctx, pats, opts, badfn=badfn)
384 384 return lambda rev: match
385 385
386 386 oldmatchandpats = installmatchandpatsfn(overridematchandpats)
387 387 oldmakelogfilematcher = cmdutil._makenofollowlogfilematcher
388 388 setattr(cmdutil, '_makenofollowlogfilematcher', overridemakelogfilematcher)
389 389
390 390 try:
391 391 return orig(ui, repo, *pats, **opts)
392 392 finally:
393 393 restorematchandpatsfn()
394 394 setattr(cmdutil, '_makenofollowlogfilematcher', oldmakelogfilematcher)
395 395
396 396 def overrideverify(orig, ui, repo, *pats, **opts):
397 397 large = opts.pop('large', False)
398 398 all = opts.pop('lfa', False)
399 399 contents = opts.pop('lfc', False)
400 400
401 401 result = orig(ui, repo, *pats, **opts)
402 402 if large or all or contents:
403 403 result = result or lfcommands.verifylfiles(ui, repo, all, contents)
404 404 return result
405 405
406 406 def overridedebugstate(orig, ui, repo, *pats, **opts):
407 407 large = opts.pop('large', False)
408 408 if large:
409 409 class fakerepo(object):
410 410 dirstate = lfutil.openlfdirstate(ui, repo)
411 411 orig(ui, fakerepo, *pats, **opts)
412 412 else:
413 413 orig(ui, repo, *pats, **opts)
414 414
415 415 # Before starting the manifest merge, merge.updates will call
416 416 # _checkunknownfile to check if there are any files in the merged-in
417 417 # changeset that collide with unknown files in the working copy.
418 418 #
419 419 # The largefiles are seen as unknown, so this prevents us from merging
420 420 # in a file 'foo' if we already have a largefile with the same name.
421 421 #
422 422 # The overridden function filters the unknown files by removing any
423 423 # largefiles. This makes the merge proceed and we can then handle this
424 424 # case further in the overridden calculateupdates function below.
425 425 def overridecheckunknownfile(origfn, repo, wctx, mctx, f, f2=None):
426 426 if lfutil.standin(repo.dirstate.normalize(f)) in wctx:
427 427 return False
428 428 return origfn(repo, wctx, mctx, f, f2)
429 429
430 430 # The manifest merge handles conflicts on the manifest level. We want
431 431 # to handle changes in largefile-ness of files at this level too.
432 432 #
433 433 # The strategy is to run the original calculateupdates and then process
434 434 # the action list it outputs. There are two cases we need to deal with:
435 435 #
436 436 # 1. Normal file in p1, largefile in p2. Here the largefile is
437 437 # detected via its standin file, which will enter the working copy
438 438 # with a "get" action. It is not "merge" since the standin is all
439 439 # Mercurial is concerned with at this level -- the link to the
440 440 # existing normal file is not relevant here.
441 441 #
442 442 # 2. Largefile in p1, normal file in p2. Here we get a "merge" action
443 443 # since the largefile will be present in the working copy and
444 444 # different from the normal file in p2. Mercurial therefore
445 445 # triggers a merge action.
446 446 #
447 447 # In both cases, we prompt the user and emit new actions to either
448 448 # remove the standin (if the normal file was kept) or to remove the
449 449 # normal file and get the standin (if the largefile was kept). The
450 450 # default prompt answer is to use the largefile version since it was
451 451 # presumably changed on purpose.
452 452 #
453 453 # Finally, the merge.applyupdates function will then take care of
454 454 # writing the files into the working copy and lfcommands.updatelfiles
455 455 # will update the largefiles.
456 456 def overridecalculateupdates(origfn, repo, p1, p2, pas, branchmerge, force,
457 457 partial, acceptremote, followcopies):
458 458 overwrite = force and not branchmerge
459 459 actions, diverge, renamedelete = origfn(
460 460 repo, p1, p2, pas, branchmerge, force, partial, acceptremote,
461 461 followcopies)
462 462
463 463 if overwrite:
464 464 return actions, diverge, renamedelete
465 465
466 466 # Convert to dictionary with filename as key and action as value.
467 467 lfiles = set()
468 468 for f in actions:
469 469 splitstandin = f and lfutil.splitstandin(f)
470 470 if splitstandin in p1:
471 471 lfiles.add(splitstandin)
472 472 elif lfutil.standin(f) in p1:
473 473 lfiles.add(f)
474 474
475 475 for lfile in lfiles:
476 476 standin = lfutil.standin(lfile)
477 477 (lm, largs, lmsg) = actions.get(lfile, (None, None, None))
478 478 (sm, sargs, smsg) = actions.get(standin, (None, None, None))
479 479 if sm in ('g', 'dc') and lm != 'r':
480 480 # Case 1: normal file in the working copy, largefile in
481 481 # the second parent
482 482 usermsg = _('remote turned local normal file %s into a largefile\n'
483 483 'use (l)argefile or keep (n)ormal file?'
484 484 '$$ &Largefile $$ &Normal file') % lfile
485 485 if repo.ui.promptchoice(usermsg, 0) == 0: # pick remote largefile
486 486 actions[lfile] = ('r', None, 'replaced by standin')
487 487 actions[standin] = ('g', sargs, 'replaces standin')
488 488 else: # keep local normal file
489 489 actions[lfile] = ('k', None, 'replaces standin')
490 490 if branchmerge:
491 491 actions[standin] = ('k', None, 'replaced by non-standin')
492 492 else:
493 493 actions[standin] = ('r', None, 'replaced by non-standin')
494 494 elif lm in ('g', 'dc') and sm != 'r':
495 495 # Case 2: largefile in the working copy, normal file in
496 496 # the second parent
497 497 usermsg = _('remote turned local largefile %s into a normal file\n'
498 498 'keep (l)argefile or use (n)ormal file?'
499 499 '$$ &Largefile $$ &Normal file') % lfile
500 500 if repo.ui.promptchoice(usermsg, 0) == 0: # keep local largefile
501 501 if branchmerge:
502 502 # largefile can be restored from standin safely
503 503 actions[lfile] = ('k', None, 'replaced by standin')
504 504 actions[standin] = ('k', None, 'replaces standin')
505 505 else:
506 506 # "lfile" should be marked as "removed" without
507 507 # removal of itself
508 508 actions[lfile] = ('lfmr', None,
509 509 'forget non-standin largefile')
510 510
511 511 # linear-merge should treat this largefile as 're-added'
512 512 actions[standin] = ('a', None, 'keep standin')
513 513 else: # pick remote normal file
514 514 actions[lfile] = ('g', largs, 'replaces standin')
515 515 actions[standin] = ('r', None, 'replaced by non-standin')
516 516
517 517 return actions, diverge, renamedelete
518 518
519 519 def mergerecordupdates(orig, repo, actions, branchmerge):
520 520 if 'lfmr' in actions:
521 521 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
522 522 for lfile, args, msg in actions['lfmr']:
523 523 # this should be executed before 'orig', to execute 'remove'
524 524 # before all other actions
525 525 repo.dirstate.remove(lfile)
526 526 # make sure lfile doesn't get synclfdirstate'd as normal
527 527 lfdirstate.add(lfile)
528 528 lfdirstate.write()
529 529
530 530 return orig(repo, actions, branchmerge)
531 531
532 532
533 533 # Override filemerge to prompt the user about how they wish to merge
534 534 # largefiles. This will handle identical edits without prompting the user.
535 535 def overridefilemerge(origfn, repo, mynode, orig, fcd, fco, fca, labels=None):
536 536 if not lfutil.isstandin(orig):
537 537 return origfn(repo, mynode, orig, fcd, fco, fca, labels=labels)
538 538
539 539 ahash = fca.data().strip().lower()
540 540 dhash = fcd.data().strip().lower()
541 541 ohash = fco.data().strip().lower()
542 542 if (ohash != ahash and
543 543 ohash != dhash and
544 544 (dhash == ahash or
545 545 repo.ui.promptchoice(
546 546 _('largefile %s has a merge conflict\nancestor was %s\n'
547 547 'keep (l)ocal %s or\ntake (o)ther %s?'
548 548 '$$ &Local $$ &Other') %
549 549 (lfutil.splitstandin(orig), ahash, dhash, ohash),
550 550 0) == 1)):
551 551 repo.wwrite(fcd.path(), fco.data(), fco.flags())
552 552 return 0
553 553
554 554 def copiespathcopies(orig, ctx1, ctx2, match=None):
555 555 copies = orig(ctx1, ctx2, match=match)
556 556 updated = {}
557 557
558 558 for k, v in copies.iteritems():
559 559 updated[lfutil.splitstandin(k) or k] = lfutil.splitstandin(v) or v
560 560
561 561 return updated
562 562
563 563 # Copy first changes the matchers to match standins instead of
564 564 # largefiles. Then it overrides util.copyfile in that function it
565 565 # checks if the destination largefile already exists. It also keeps a
566 566 # list of copied files so that the largefiles can be copied and the
567 567 # dirstate updated.
568 568 def overridecopy(orig, ui, repo, pats, opts, rename=False):
569 569 # doesn't remove largefile on rename
570 570 if len(pats) < 2:
571 571 # this isn't legal, let the original function deal with it
572 572 return orig(ui, repo, pats, opts, rename)
573 573
574 574 # This could copy both lfiles and normal files in one command,
575 575 # but we don't want to do that. First replace their matcher to
576 576 # only match normal files and run it, then replace it to just
577 577 # match largefiles and run it again.
578 578 nonormalfiles = False
579 579 nolfiles = False
580 580 installnormalfilesmatchfn(repo[None].manifest())
581 581 try:
582 582 result = orig(ui, repo, pats, opts, rename)
583 except util.Abort, e:
583 except util.Abort as e:
584 584 if str(e) != _('no files to copy'):
585 585 raise e
586 586 else:
587 587 nonormalfiles = True
588 588 result = 0
589 589 finally:
590 590 restorematchfn()
591 591
592 592 # The first rename can cause our current working directory to be removed.
593 593 # In that case there is nothing left to copy/rename so just quit.
594 594 try:
595 595 repo.getcwd()
596 596 except OSError:
597 597 return result
598 598
599 599 def makestandin(relpath):
600 600 path = pathutil.canonpath(repo.root, repo.getcwd(), relpath)
601 601 return os.path.join(repo.wjoin(lfutil.standin(path)))
602 602
603 603 fullpats = scmutil.expandpats(pats)
604 604 dest = fullpats[-1]
605 605
606 606 if os.path.isdir(dest):
607 607 if not os.path.isdir(makestandin(dest)):
608 608 os.makedirs(makestandin(dest))
609 609
610 610 try:
611 611 # When we call orig below it creates the standins but we don't add
612 612 # them to the dir state until later so lock during that time.
613 613 wlock = repo.wlock()
614 614
615 615 manifest = repo[None].manifest()
616 616 def overridematch(ctx, pats=[], opts={}, globbed=False,
617 617 default='relpath', badfn=None):
618 618 newpats = []
619 619 # The patterns were previously mangled to add the standin
620 620 # directory; we need to remove that now
621 621 for pat in pats:
622 622 if match_.patkind(pat) is None and lfutil.shortname in pat:
623 623 newpats.append(pat.replace(lfutil.shortname, ''))
624 624 else:
625 625 newpats.append(pat)
626 626 match = oldmatch(ctx, newpats, opts, globbed, default, badfn=badfn)
627 627 m = copy.copy(match)
628 628 lfile = lambda f: lfutil.standin(f) in manifest
629 629 m._files = [lfutil.standin(f) for f in m._files if lfile(f)]
630 630 m._fileroots = set(m._files)
631 631 origmatchfn = m.matchfn
632 632 m.matchfn = lambda f: (lfutil.isstandin(f) and
633 633 (f in manifest) and
634 634 origmatchfn(lfutil.splitstandin(f)) or
635 635 None)
636 636 return m
637 637 oldmatch = installmatchfn(overridematch)
638 638 listpats = []
639 639 for pat in pats:
640 640 if match_.patkind(pat) is not None:
641 641 listpats.append(pat)
642 642 else:
643 643 listpats.append(makestandin(pat))
644 644
645 645 try:
646 646 origcopyfile = util.copyfile
647 647 copiedfiles = []
648 648 def overridecopyfile(src, dest):
649 649 if (lfutil.shortname in src and
650 650 dest.startswith(repo.wjoin(lfutil.shortname))):
651 651 destlfile = dest.replace(lfutil.shortname, '')
652 652 if not opts['force'] and os.path.exists(destlfile):
653 653 raise IOError('',
654 654 _('destination largefile already exists'))
655 655 copiedfiles.append((src, dest))
656 656 origcopyfile(src, dest)
657 657
658 658 util.copyfile = overridecopyfile
659 659 result += orig(ui, repo, listpats, opts, rename)
660 660 finally:
661 661 util.copyfile = origcopyfile
662 662
663 663 lfdirstate = lfutil.openlfdirstate(ui, repo)
664 664 for (src, dest) in copiedfiles:
665 665 if (lfutil.shortname in src and
666 666 dest.startswith(repo.wjoin(lfutil.shortname))):
667 667 srclfile = src.replace(repo.wjoin(lfutil.standin('')), '')
668 668 destlfile = dest.replace(repo.wjoin(lfutil.standin('')), '')
669 669 destlfiledir = os.path.dirname(repo.wjoin(destlfile)) or '.'
670 670 if not os.path.isdir(destlfiledir):
671 671 os.makedirs(destlfiledir)
672 672 if rename:
673 673 os.rename(repo.wjoin(srclfile), repo.wjoin(destlfile))
674 674
675 675 # The file is gone, but this deletes any empty parent
676 676 # directories as a side-effect.
677 677 util.unlinkpath(repo.wjoin(srclfile), True)
678 678 lfdirstate.remove(srclfile)
679 679 else:
680 680 util.copyfile(repo.wjoin(srclfile),
681 681 repo.wjoin(destlfile))
682 682
683 683 lfdirstate.add(destlfile)
684 684 lfdirstate.write()
685 except util.Abort, e:
685 except util.Abort as e:
686 686 if str(e) != _('no files to copy'):
687 687 raise e
688 688 else:
689 689 nolfiles = True
690 690 finally:
691 691 restorematchfn()
692 692 wlock.release()
693 693
694 694 if nolfiles and nonormalfiles:
695 695 raise util.Abort(_('no files to copy'))
696 696
697 697 return result
698 698
699 699 # When the user calls revert, we have to be careful to not revert any
700 700 # changes to other largefiles accidentally. This means we have to keep
701 701 # track of the largefiles that are being reverted so we only pull down
702 702 # the necessary largefiles.
703 703 #
704 704 # Standins are only updated (to match the hash of largefiles) before
705 705 # commits. Update the standins then run the original revert, changing
706 706 # the matcher to hit standins instead of largefiles. Based on the
707 707 # resulting standins update the largefiles.
708 708 def overriderevert(orig, ui, repo, ctx, parents, *pats, **opts):
709 709 # Because we put the standins in a bad state (by updating them)
710 710 # and then return them to a correct state we need to lock to
711 711 # prevent others from changing them in their incorrect state.
712 712 wlock = repo.wlock()
713 713 try:
714 714 lfdirstate = lfutil.openlfdirstate(ui, repo)
715 715 s = lfutil.lfdirstatestatus(lfdirstate, repo)
716 716 lfdirstate.write()
717 717 for lfile in s.modified:
718 718 lfutil.updatestandin(repo, lfutil.standin(lfile))
719 719 for lfile in s.deleted:
720 720 if (os.path.exists(repo.wjoin(lfutil.standin(lfile)))):
721 721 os.unlink(repo.wjoin(lfutil.standin(lfile)))
722 722
723 723 oldstandins = lfutil.getstandinsstate(repo)
724 724
725 725 def overridematch(mctx, pats=[], opts={}, globbed=False,
726 726 default='relpath', badfn=None):
727 727 match = oldmatch(mctx, pats, opts, globbed, default, badfn=badfn)
728 728 m = copy.copy(match)
729 729
730 730 # revert supports recursing into subrepos, and though largefiles
731 731 # currently doesn't work correctly in that case, this match is
732 732 # called, so the lfdirstate above may not be the correct one for
733 733 # this invocation of match.
734 734 lfdirstate = lfutil.openlfdirstate(mctx.repo().ui, mctx.repo(),
735 735 False)
736 736
737 737 def tostandin(f):
738 738 standin = lfutil.standin(f)
739 739 if standin in ctx or standin in mctx:
740 740 return standin
741 741 elif standin in repo[None] or lfdirstate[f] == 'r':
742 742 return None
743 743 return f
744 744 m._files = [tostandin(f) for f in m._files]
745 745 m._files = [f for f in m._files if f is not None]
746 746 m._fileroots = set(m._files)
747 747 origmatchfn = m.matchfn
748 748 def matchfn(f):
749 749 if lfutil.isstandin(f):
750 750 return (origmatchfn(lfutil.splitstandin(f)) and
751 751 (f in ctx or f in mctx))
752 752 return origmatchfn(f)
753 753 m.matchfn = matchfn
754 754 return m
755 755 oldmatch = installmatchfn(overridematch)
756 756 try:
757 757 orig(ui, repo, ctx, parents, *pats, **opts)
758 758 finally:
759 759 restorematchfn()
760 760
761 761 newstandins = lfutil.getstandinsstate(repo)
762 762 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
763 763 # lfdirstate should be 'normallookup'-ed for updated files,
764 764 # because reverting doesn't touch dirstate for 'normal' files
765 765 # when target revision is explicitly specified: in such case,
766 766 # 'n' and valid timestamp in dirstate doesn't ensure 'clean'
767 767 # of target (standin) file.
768 768 lfcommands.updatelfiles(ui, repo, filelist, printmessage=False,
769 769 normallookup=True)
770 770
771 771 finally:
772 772 wlock.release()
773 773
774 774 # after pulling changesets, we need to take some extra care to get
775 775 # largefiles updated remotely
776 776 def overridepull(orig, ui, repo, source=None, **opts):
777 777 revsprepull = len(repo)
778 778 if not source:
779 779 source = 'default'
780 780 repo.lfpullsource = source
781 781 result = orig(ui, repo, source, **opts)
782 782 revspostpull = len(repo)
783 783 lfrevs = opts.get('lfrev', [])
784 784 if opts.get('all_largefiles'):
785 785 lfrevs.append('pulled()')
786 786 if lfrevs and revspostpull > revsprepull:
787 787 numcached = 0
788 788 repo.firstpulled = revsprepull # for pulled() revset expression
789 789 try:
790 790 for rev in scmutil.revrange(repo, lfrevs):
791 791 ui.note(_('pulling largefiles for revision %s\n') % rev)
792 792 (cached, missing) = lfcommands.cachelfiles(ui, repo, rev)
793 793 numcached += len(cached)
794 794 finally:
795 795 del repo.firstpulled
796 796 ui.status(_("%d largefiles cached\n") % numcached)
797 797 return result
798 798
799 799 def pulledrevsetsymbol(repo, subset, x):
800 800 """``pulled()``
801 801 Changesets that just has been pulled.
802 802
803 803 Only available with largefiles from pull --lfrev expressions.
804 804
805 805 .. container:: verbose
806 806
807 807 Some examples:
808 808
809 809 - pull largefiles for all new changesets::
810 810
811 811 hg pull -lfrev "pulled()"
812 812
813 813 - pull largefiles for all new branch heads::
814 814
815 815 hg pull -lfrev "head(pulled()) and not closed()"
816 816
817 817 """
818 818
819 819 try:
820 820 firstpulled = repo.firstpulled
821 821 except AttributeError:
822 822 raise util.Abort(_("pulled() only available in --lfrev"))
823 823 return revset.baseset([r for r in subset if r >= firstpulled])
824 824
825 825 def overrideclone(orig, ui, source, dest=None, **opts):
826 826 d = dest
827 827 if d is None:
828 828 d = hg.defaultdest(source)
829 829 if opts.get('all_largefiles') and not hg.islocal(d):
830 830 raise util.Abort(_(
831 831 '--all-largefiles is incompatible with non-local destination %s') %
832 832 d)
833 833
834 834 return orig(ui, source, dest, **opts)
835 835
836 836 def hgclone(orig, ui, opts, *args, **kwargs):
837 837 result = orig(ui, opts, *args, **kwargs)
838 838
839 839 if result is not None:
840 840 sourcerepo, destrepo = result
841 841 repo = destrepo.local()
842 842
843 843 # When cloning to a remote repo (like through SSH), no repo is available
844 844 # from the peer. Therefore the largefiles can't be downloaded and the
845 845 # hgrc can't be updated.
846 846 if not repo:
847 847 return result
848 848
849 849 # If largefiles is required for this repo, permanently enable it locally
850 850 if 'largefiles' in repo.requirements:
851 851 fp = repo.vfs('hgrc', 'a', text=True)
852 852 try:
853 853 fp.write('\n[extensions]\nlargefiles=\n')
854 854 finally:
855 855 fp.close()
856 856
857 857 # Caching is implicitly limited to 'rev' option, since the dest repo was
858 858 # truncated at that point. The user may expect a download count with
859 859 # this option, so attempt whether or not this is a largefile repo.
860 860 if opts.get('all_largefiles'):
861 861 success, missing = lfcommands.downloadlfiles(ui, repo, None)
862 862
863 863 if missing != 0:
864 864 return None
865 865
866 866 return result
867 867
868 868 def overriderebase(orig, ui, repo, **opts):
869 869 if not util.safehasattr(repo, '_largefilesenabled'):
870 870 return orig(ui, repo, **opts)
871 871
872 872 resuming = opts.get('continue')
873 873 repo._lfcommithooks.append(lfutil.automatedcommithook(resuming))
874 874 repo._lfstatuswriters.append(lambda *msg, **opts: None)
875 875 try:
876 876 return orig(ui, repo, **opts)
877 877 finally:
878 878 repo._lfstatuswriters.pop()
879 879 repo._lfcommithooks.pop()
880 880
881 881 def overridearchive(orig, repo, dest, node, kind, decode=True, matchfn=None,
882 882 prefix='', mtime=None, subrepos=None):
883 883 # No need to lock because we are only reading history and
884 884 # largefile caches, neither of which are modified.
885 885 if node is not None:
886 886 lfcommands.cachelfiles(repo.ui, repo, node)
887 887
888 888 if kind not in archival.archivers:
889 889 raise util.Abort(_("unknown archive type '%s'") % kind)
890 890
891 891 ctx = repo[node]
892 892
893 893 if kind == 'files':
894 894 if prefix:
895 895 raise util.Abort(
896 896 _('cannot give prefix when archiving to files'))
897 897 else:
898 898 prefix = archival.tidyprefix(dest, kind, prefix)
899 899
900 900 def write(name, mode, islink, getdata):
901 901 if matchfn and not matchfn(name):
902 902 return
903 903 data = getdata()
904 904 if decode:
905 905 data = repo.wwritedata(name, data)
906 906 archiver.addfile(prefix + name, mode, islink, data)
907 907
908 908 archiver = archival.archivers[kind](dest, mtime or ctx.date()[0])
909 909
910 910 if repo.ui.configbool("ui", "archivemeta", True):
911 911 write('.hg_archival.txt', 0o644, False,
912 912 lambda: archival.buildmetadata(ctx))
913 913
914 914 for f in ctx:
915 915 ff = ctx.flags(f)
916 916 getdata = ctx[f].data
917 917 if lfutil.isstandin(f):
918 918 if node is not None:
919 919 path = lfutil.findfile(repo, getdata().strip())
920 920
921 921 if path is None:
922 922 raise util.Abort(
923 923 _('largefile %s not found in repo store or system cache')
924 924 % lfutil.splitstandin(f))
925 925 else:
926 926 path = lfutil.splitstandin(f)
927 927
928 928 f = lfutil.splitstandin(f)
929 929
930 930 def getdatafn():
931 931 fd = None
932 932 try:
933 933 fd = open(path, 'rb')
934 934 return fd.read()
935 935 finally:
936 936 if fd:
937 937 fd.close()
938 938
939 939 getdata = getdatafn
940 940 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, getdata)
941 941
942 942 if subrepos:
943 943 for subpath in sorted(ctx.substate):
944 944 sub = ctx.workingsub(subpath)
945 945 submatch = match_.narrowmatcher(subpath, matchfn)
946 946 sub.archive(archiver, prefix, submatch)
947 947
948 948 archiver.done()
949 949
950 950 def hgsubrepoarchive(orig, repo, archiver, prefix, match=None):
951 951 repo._get(repo._state + ('hg',))
952 952 rev = repo._state[1]
953 953 ctx = repo._repo[rev]
954 954
955 955 if ctx.node() is not None:
956 956 lfcommands.cachelfiles(repo.ui, repo._repo, ctx.node())
957 957
958 958 def write(name, mode, islink, getdata):
959 959 # At this point, the standin has been replaced with the largefile name,
960 960 # so the normal matcher works here without the lfutil variants.
961 961 if match and not match(f):
962 962 return
963 963 data = getdata()
964 964
965 965 archiver.addfile(prefix + repo._path + '/' + name, mode, islink, data)
966 966
967 967 for f in ctx:
968 968 ff = ctx.flags(f)
969 969 getdata = ctx[f].data
970 970 if lfutil.isstandin(f):
971 971 if ctx.node() is not None:
972 972 path = lfutil.findfile(repo._repo, getdata().strip())
973 973
974 974 if path is None:
975 975 raise util.Abort(
976 976 _('largefile %s not found in repo store or system cache')
977 977 % lfutil.splitstandin(f))
978 978 else:
979 979 path = lfutil.splitstandin(f)
980 980
981 981 f = lfutil.splitstandin(f)
982 982
983 983 def getdatafn():
984 984 fd = None
985 985 try:
986 986 fd = open(os.path.join(prefix, path), 'rb')
987 987 return fd.read()
988 988 finally:
989 989 if fd:
990 990 fd.close()
991 991
992 992 getdata = getdatafn
993 993
994 994 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, getdata)
995 995
996 996 for subpath in sorted(ctx.substate):
997 997 sub = ctx.workingsub(subpath)
998 998 submatch = match_.narrowmatcher(subpath, match)
999 999 sub.archive(archiver, prefix + repo._path + '/', submatch)
1000 1000
1001 1001 # If a largefile is modified, the change is not reflected in its
1002 1002 # standin until a commit. cmdutil.bailifchanged() raises an exception
1003 1003 # if the repo has uncommitted changes. Wrap it to also check if
1004 1004 # largefiles were changed. This is used by bisect, backout and fetch.
1005 1005 def overridebailifchanged(orig, repo, *args, **kwargs):
1006 1006 orig(repo, *args, **kwargs)
1007 1007 repo.lfstatus = True
1008 1008 s = repo.status()
1009 1009 repo.lfstatus = False
1010 1010 if s.modified or s.added or s.removed or s.deleted:
1011 1011 raise util.Abort(_('uncommitted changes'))
1012 1012
1013 1013 def cmdutilforget(orig, ui, repo, match, prefix, explicitonly):
1014 1014 normalmatcher = composenormalfilematcher(match, repo[None].manifest())
1015 1015 bad, forgot = orig(ui, repo, normalmatcher, prefix, explicitonly)
1016 1016 m = composelargefilematcher(match, repo[None].manifest())
1017 1017
1018 1018 try:
1019 1019 repo.lfstatus = True
1020 1020 s = repo.status(match=m, clean=True)
1021 1021 finally:
1022 1022 repo.lfstatus = False
1023 1023 forget = sorted(s.modified + s.added + s.deleted + s.clean)
1024 1024 forget = [f for f in forget if lfutil.standin(f) in repo[None].manifest()]
1025 1025
1026 1026 for f in forget:
1027 1027 if lfutil.standin(f) not in repo.dirstate and not \
1028 1028 repo.wvfs.isdir(lfutil.standin(f)):
1029 1029 ui.warn(_('not removing %s: file is already untracked\n')
1030 1030 % m.rel(f))
1031 1031 bad.append(f)
1032 1032
1033 1033 for f in forget:
1034 1034 if ui.verbose or not m.exact(f):
1035 1035 ui.status(_('removing %s\n') % m.rel(f))
1036 1036
1037 1037 # Need to lock because standin files are deleted then removed from the
1038 1038 # repository and we could race in-between.
1039 1039 wlock = repo.wlock()
1040 1040 try:
1041 1041 lfdirstate = lfutil.openlfdirstate(ui, repo)
1042 1042 for f in forget:
1043 1043 if lfdirstate[f] == 'a':
1044 1044 lfdirstate.drop(f)
1045 1045 else:
1046 1046 lfdirstate.remove(f)
1047 1047 lfdirstate.write()
1048 1048 standins = [lfutil.standin(f) for f in forget]
1049 1049 for f in standins:
1050 1050 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1051 1051 rejected = repo[None].forget(standins)
1052 1052 finally:
1053 1053 wlock.release()
1054 1054
1055 1055 bad.extend(f for f in rejected if f in m.files())
1056 1056 forgot.extend(f for f in forget if f not in rejected)
1057 1057 return bad, forgot
1058 1058
1059 1059 def _getoutgoings(repo, other, missing, addfunc):
1060 1060 """get pairs of filename and largefile hash in outgoing revisions
1061 1061 in 'missing'.
1062 1062
1063 1063 largefiles already existing on 'other' repository are ignored.
1064 1064
1065 1065 'addfunc' is invoked with each unique pairs of filename and
1066 1066 largefile hash value.
1067 1067 """
1068 1068 knowns = set()
1069 1069 lfhashes = set()
1070 1070 def dedup(fn, lfhash):
1071 1071 k = (fn, lfhash)
1072 1072 if k not in knowns:
1073 1073 knowns.add(k)
1074 1074 lfhashes.add(lfhash)
1075 1075 lfutil.getlfilestoupload(repo, missing, dedup)
1076 1076 if lfhashes:
1077 1077 lfexists = basestore._openstore(repo, other).exists(lfhashes)
1078 1078 for fn, lfhash in knowns:
1079 1079 if not lfexists[lfhash]: # lfhash doesn't exist on "other"
1080 1080 addfunc(fn, lfhash)
1081 1081
1082 1082 def outgoinghook(ui, repo, other, opts, missing):
1083 1083 if opts.pop('large', None):
1084 1084 lfhashes = set()
1085 1085 if ui.debugflag:
1086 1086 toupload = {}
1087 1087 def addfunc(fn, lfhash):
1088 1088 if fn not in toupload:
1089 1089 toupload[fn] = []
1090 1090 toupload[fn].append(lfhash)
1091 1091 lfhashes.add(lfhash)
1092 1092 def showhashes(fn):
1093 1093 for lfhash in sorted(toupload[fn]):
1094 1094 ui.debug(' %s\n' % (lfhash))
1095 1095 else:
1096 1096 toupload = set()
1097 1097 def addfunc(fn, lfhash):
1098 1098 toupload.add(fn)
1099 1099 lfhashes.add(lfhash)
1100 1100 def showhashes(fn):
1101 1101 pass
1102 1102 _getoutgoings(repo, other, missing, addfunc)
1103 1103
1104 1104 if not toupload:
1105 1105 ui.status(_('largefiles: no files to upload\n'))
1106 1106 else:
1107 1107 ui.status(_('largefiles to upload (%d entities):\n')
1108 1108 % (len(lfhashes)))
1109 1109 for file in sorted(toupload):
1110 1110 ui.status(lfutil.splitstandin(file) + '\n')
1111 1111 showhashes(file)
1112 1112 ui.status('\n')
1113 1113
1114 1114 def summaryremotehook(ui, repo, opts, changes):
1115 1115 largeopt = opts.get('large', False)
1116 1116 if changes is None:
1117 1117 if largeopt:
1118 1118 return (False, True) # only outgoing check is needed
1119 1119 else:
1120 1120 return (False, False)
1121 1121 elif largeopt:
1122 1122 url, branch, peer, outgoing = changes[1]
1123 1123 if peer is None:
1124 1124 # i18n: column positioning for "hg summary"
1125 1125 ui.status(_('largefiles: (no remote repo)\n'))
1126 1126 return
1127 1127
1128 1128 toupload = set()
1129 1129 lfhashes = set()
1130 1130 def addfunc(fn, lfhash):
1131 1131 toupload.add(fn)
1132 1132 lfhashes.add(lfhash)
1133 1133 _getoutgoings(repo, peer, outgoing.missing, addfunc)
1134 1134
1135 1135 if not toupload:
1136 1136 # i18n: column positioning for "hg summary"
1137 1137 ui.status(_('largefiles: (no files to upload)\n'))
1138 1138 else:
1139 1139 # i18n: column positioning for "hg summary"
1140 1140 ui.status(_('largefiles: %d entities for %d files to upload\n')
1141 1141 % (len(lfhashes), len(toupload)))
1142 1142
1143 1143 def overridesummary(orig, ui, repo, *pats, **opts):
1144 1144 try:
1145 1145 repo.lfstatus = True
1146 1146 orig(ui, repo, *pats, **opts)
1147 1147 finally:
1148 1148 repo.lfstatus = False
1149 1149
1150 1150 def scmutiladdremove(orig, repo, matcher, prefix, opts={}, dry_run=None,
1151 1151 similarity=None):
1152 1152 if not lfutil.islfilesrepo(repo):
1153 1153 return orig(repo, matcher, prefix, opts, dry_run, similarity)
1154 1154 # Get the list of missing largefiles so we can remove them
1155 1155 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1156 1156 unsure, s = lfdirstate.status(match_.always(repo.root, repo.getcwd()), [],
1157 1157 False, False, False)
1158 1158
1159 1159 # Call into the normal remove code, but the removing of the standin, we want
1160 1160 # to have handled by original addremove. Monkey patching here makes sure
1161 1161 # we don't remove the standin in the largefiles code, preventing a very
1162 1162 # confused state later.
1163 1163 if s.deleted:
1164 1164 m = copy.copy(matcher)
1165 1165
1166 1166 # The m._files and m._map attributes are not changed to the deleted list
1167 1167 # because that affects the m.exact() test, which in turn governs whether
1168 1168 # or not the file name is printed, and how. Simply limit the original
1169 1169 # matches to those in the deleted status list.
1170 1170 matchfn = m.matchfn
1171 1171 m.matchfn = lambda f: f in s.deleted and matchfn(f)
1172 1172
1173 1173 removelargefiles(repo.ui, repo, True, m, **opts)
1174 1174 # Call into the normal add code, and any files that *should* be added as
1175 1175 # largefiles will be
1176 1176 added, bad = addlargefiles(repo.ui, repo, True, matcher, **opts)
1177 1177 # Now that we've handled largefiles, hand off to the original addremove
1178 1178 # function to take care of the rest. Make sure it doesn't do anything with
1179 1179 # largefiles by passing a matcher that will ignore them.
1180 1180 matcher = composenormalfilematcher(matcher, repo[None].manifest(), added)
1181 1181 return orig(repo, matcher, prefix, opts, dry_run, similarity)
1182 1182
1183 1183 # Calling purge with --all will cause the largefiles to be deleted.
1184 1184 # Override repo.status to prevent this from happening.
1185 1185 def overridepurge(orig, ui, repo, *dirs, **opts):
1186 1186 # XXX Monkey patching a repoview will not work. The assigned attribute will
1187 1187 # be set on the unfiltered repo, but we will only lookup attributes in the
1188 1188 # unfiltered repo if the lookup in the repoview object itself fails. As the
1189 1189 # monkey patched method exists on the repoview class the lookup will not
1190 1190 # fail. As a result, the original version will shadow the monkey patched
1191 1191 # one, defeating the monkey patch.
1192 1192 #
1193 1193 # As a work around we use an unfiltered repo here. We should do something
1194 1194 # cleaner instead.
1195 1195 repo = repo.unfiltered()
1196 1196 oldstatus = repo.status
1197 1197 def overridestatus(node1='.', node2=None, match=None, ignored=False,
1198 1198 clean=False, unknown=False, listsubrepos=False):
1199 1199 r = oldstatus(node1, node2, match, ignored, clean, unknown,
1200 1200 listsubrepos)
1201 1201 lfdirstate = lfutil.openlfdirstate(ui, repo)
1202 1202 unknown = [f for f in r.unknown if lfdirstate[f] == '?']
1203 1203 ignored = [f for f in r.ignored if lfdirstate[f] == '?']
1204 1204 return scmutil.status(r.modified, r.added, r.removed, r.deleted,
1205 1205 unknown, ignored, r.clean)
1206 1206 repo.status = overridestatus
1207 1207 orig(ui, repo, *dirs, **opts)
1208 1208 repo.status = oldstatus
1209 1209 def overriderollback(orig, ui, repo, **opts):
1210 1210 wlock = repo.wlock()
1211 1211 try:
1212 1212 before = repo.dirstate.parents()
1213 1213 orphans = set(f for f in repo.dirstate
1214 1214 if lfutil.isstandin(f) and repo.dirstate[f] != 'r')
1215 1215 result = orig(ui, repo, **opts)
1216 1216 after = repo.dirstate.parents()
1217 1217 if before == after:
1218 1218 return result # no need to restore standins
1219 1219
1220 1220 pctx = repo['.']
1221 1221 for f in repo.dirstate:
1222 1222 if lfutil.isstandin(f):
1223 1223 orphans.discard(f)
1224 1224 if repo.dirstate[f] == 'r':
1225 1225 repo.wvfs.unlinkpath(f, ignoremissing=True)
1226 1226 elif f in pctx:
1227 1227 fctx = pctx[f]
1228 1228 repo.wwrite(f, fctx.data(), fctx.flags())
1229 1229 else:
1230 1230 # content of standin is not so important in 'a',
1231 1231 # 'm' or 'n' (coming from the 2nd parent) cases
1232 1232 lfutil.writestandin(repo, f, '', False)
1233 1233 for standin in orphans:
1234 1234 repo.wvfs.unlinkpath(standin, ignoremissing=True)
1235 1235
1236 1236 lfdirstate = lfutil.openlfdirstate(ui, repo)
1237 1237 orphans = set(lfdirstate)
1238 1238 lfiles = lfutil.listlfiles(repo)
1239 1239 for file in lfiles:
1240 1240 lfutil.synclfdirstate(repo, lfdirstate, file, True)
1241 1241 orphans.discard(file)
1242 1242 for lfile in orphans:
1243 1243 lfdirstate.drop(lfile)
1244 1244 lfdirstate.write()
1245 1245 finally:
1246 1246 wlock.release()
1247 1247 return result
1248 1248
1249 1249 def overridetransplant(orig, ui, repo, *revs, **opts):
1250 1250 resuming = opts.get('continue')
1251 1251 repo._lfcommithooks.append(lfutil.automatedcommithook(resuming))
1252 1252 repo._lfstatuswriters.append(lambda *msg, **opts: None)
1253 1253 try:
1254 1254 result = orig(ui, repo, *revs, **opts)
1255 1255 finally:
1256 1256 repo._lfstatuswriters.pop()
1257 1257 repo._lfcommithooks.pop()
1258 1258 return result
1259 1259
1260 1260 def overridecat(orig, ui, repo, file1, *pats, **opts):
1261 1261 ctx = scmutil.revsingle(repo, opts.get('rev'))
1262 1262 err = 1
1263 1263 notbad = set()
1264 1264 m = scmutil.match(ctx, (file1,) + pats, opts)
1265 1265 origmatchfn = m.matchfn
1266 1266 def lfmatchfn(f):
1267 1267 if origmatchfn(f):
1268 1268 return True
1269 1269 lf = lfutil.splitstandin(f)
1270 1270 if lf is None:
1271 1271 return False
1272 1272 notbad.add(lf)
1273 1273 return origmatchfn(lf)
1274 1274 m.matchfn = lfmatchfn
1275 1275 origbadfn = m.bad
1276 1276 def lfbadfn(f, msg):
1277 1277 if not f in notbad:
1278 1278 origbadfn(f, msg)
1279 1279 m.bad = lfbadfn
1280 1280
1281 1281 origvisitdirfn = m.visitdir
1282 1282 def lfvisitdirfn(dir):
1283 1283 if dir == lfutil.shortname:
1284 1284 return True
1285 1285 ret = origvisitdirfn(dir)
1286 1286 if ret:
1287 1287 return ret
1288 1288 lf = lfutil.splitstandin(dir)
1289 1289 if lf is None:
1290 1290 return False
1291 1291 return origvisitdirfn(lf)
1292 1292 m.visitdir = lfvisitdirfn
1293 1293
1294 1294 for f in ctx.walk(m):
1295 1295 fp = cmdutil.makefileobj(repo, opts.get('output'), ctx.node(),
1296 1296 pathname=f)
1297 1297 lf = lfutil.splitstandin(f)
1298 1298 if lf is None or origmatchfn(f):
1299 1299 # duplicating unreachable code from commands.cat
1300 1300 data = ctx[f].data()
1301 1301 if opts.get('decode'):
1302 1302 data = repo.wwritedata(f, data)
1303 1303 fp.write(data)
1304 1304 else:
1305 1305 hash = lfutil.readstandin(repo, lf, ctx.rev())
1306 1306 if not lfutil.inusercache(repo.ui, hash):
1307 1307 store = basestore._openstore(repo)
1308 1308 success, missing = store.get([(lf, hash)])
1309 1309 if len(success) != 1:
1310 1310 raise util.Abort(
1311 1311 _('largefile %s is not in cache and could not be '
1312 1312 'downloaded') % lf)
1313 1313 path = lfutil.usercachepath(repo.ui, hash)
1314 1314 fpin = open(path, "rb")
1315 1315 for chunk in util.filechunkiter(fpin, 128 * 1024):
1316 1316 fp.write(chunk)
1317 1317 fpin.close()
1318 1318 fp.close()
1319 1319 err = 0
1320 1320 return err
1321 1321
1322 1322 def mergeupdate(orig, repo, node, branchmerge, force, partial,
1323 1323 *args, **kwargs):
1324 1324 wlock = repo.wlock()
1325 1325 try:
1326 1326 # branch | | |
1327 1327 # merge | force | partial | action
1328 1328 # -------+-------+---------+--------------
1329 1329 # x | x | x | linear-merge
1330 1330 # o | x | x | branch-merge
1331 1331 # x | o | x | overwrite (as clean update)
1332 1332 # o | o | x | force-branch-merge (*1)
1333 1333 # x | x | o | (*)
1334 1334 # o | x | o | (*)
1335 1335 # x | o | o | overwrite (as revert)
1336 1336 # o | o | o | (*)
1337 1337 #
1338 1338 # (*) don't care
1339 1339 # (*1) deprecated, but used internally (e.g: "rebase --collapse")
1340 1340
1341 1341 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1342 1342 unsure, s = lfdirstate.status(match_.always(repo.root,
1343 1343 repo.getcwd()),
1344 1344 [], False, False, False)
1345 1345 pctx = repo['.']
1346 1346 for lfile in unsure + s.modified:
1347 1347 lfileabs = repo.wvfs.join(lfile)
1348 1348 if not os.path.exists(lfileabs):
1349 1349 continue
1350 1350 lfhash = lfutil.hashrepofile(repo, lfile)
1351 1351 standin = lfutil.standin(lfile)
1352 1352 lfutil.writestandin(repo, standin, lfhash,
1353 1353 lfutil.getexecutable(lfileabs))
1354 1354 if (standin in pctx and
1355 1355 lfhash == lfutil.readstandin(repo, lfile, '.')):
1356 1356 lfdirstate.normal(lfile)
1357 1357 for lfile in s.added:
1358 1358 lfutil.updatestandin(repo, lfutil.standin(lfile))
1359 1359 lfdirstate.write()
1360 1360
1361 1361 oldstandins = lfutil.getstandinsstate(repo)
1362 1362
1363 1363 result = orig(repo, node, branchmerge, force, partial, *args, **kwargs)
1364 1364
1365 1365 newstandins = lfutil.getstandinsstate(repo)
1366 1366 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
1367 1367 if branchmerge or force or partial:
1368 1368 filelist.extend(s.deleted + s.removed)
1369 1369
1370 1370 lfcommands.updatelfiles(repo.ui, repo, filelist=filelist,
1371 1371 normallookup=partial)
1372 1372
1373 1373 return result
1374 1374 finally:
1375 1375 wlock.release()
1376 1376
1377 1377 def scmutilmarktouched(orig, repo, files, *args, **kwargs):
1378 1378 result = orig(repo, files, *args, **kwargs)
1379 1379
1380 1380 filelist = [lfutil.splitstandin(f) for f in files if lfutil.isstandin(f)]
1381 1381 if filelist:
1382 1382 lfcommands.updatelfiles(repo.ui, repo, filelist=filelist,
1383 1383 printmessage=False, normallookup=True)
1384 1384
1385 1385 return result
@@ -1,175 +1,175 b''
1 1 # Copyright 2011 Fog Creek Software
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 import os
7 7 import urllib2
8 8 import re
9 9
10 10 from mercurial import error, httppeer, util, wireproto
11 11 from mercurial.i18n import _
12 12
13 13 import lfutil
14 14
15 15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
16 16 '\n\nPlease enable it in your Mercurial config '
17 17 'file.\n')
18 18
19 19 # these will all be replaced by largefiles.uisetup
20 20 capabilitiesorig = None
21 21 ssholdcallstream = None
22 22 httpoldcallstream = None
23 23
24 24 def putlfile(repo, proto, sha):
25 25 '''Put a largefile into a repository's local store and into the
26 26 user cache.'''
27 27 proto.redirect()
28 28
29 29 path = lfutil.storepath(repo, sha)
30 30 util.makedirs(os.path.dirname(path))
31 31 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
32 32
33 33 try:
34 34 proto.getfile(tmpfp)
35 35 tmpfp._fp.seek(0)
36 36 if sha != lfutil.hexsha1(tmpfp._fp):
37 37 raise IOError(0, _('largefile contents do not match hash'))
38 38 tmpfp.close()
39 39 lfutil.linktousercache(repo, sha)
40 except IOError, e:
40 except IOError as e:
41 41 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
42 42 (sha, e.strerror))
43 43 return wireproto.pushres(1)
44 44 finally:
45 45 tmpfp.discard()
46 46
47 47 return wireproto.pushres(0)
48 48
49 49 def getlfile(repo, proto, sha):
50 50 '''Retrieve a largefile from the repository-local cache or system
51 51 cache.'''
52 52 filename = lfutil.findfile(repo, sha)
53 53 if not filename:
54 54 raise util.Abort(_('requested largefile %s not present in cache') % sha)
55 55 f = open(filename, 'rb')
56 56 length = os.fstat(f.fileno())[6]
57 57
58 58 # Since we can't set an HTTP content-length header here, and
59 59 # Mercurial core provides no way to give the length of a streamres
60 60 # (and reading the entire file into RAM would be ill-advised), we
61 61 # just send the length on the first line of the response, like the
62 62 # ssh proto does for string responses.
63 63 def generator():
64 64 yield '%d\n' % length
65 65 for chunk in util.filechunkiter(f):
66 66 yield chunk
67 67 return wireproto.streamres(generator())
68 68
69 69 def statlfile(repo, proto, sha):
70 70 '''Return '2\n' if the largefile is missing, '0\n' if it seems to be in
71 71 good condition.
72 72
73 73 The value 1 is reserved for mismatched checksum, but that is too expensive
74 74 to be verified on every stat and must be caught be running 'hg verify'
75 75 server side.'''
76 76 filename = lfutil.findfile(repo, sha)
77 77 if not filename:
78 78 return '2\n'
79 79 return '0\n'
80 80
81 81 def wirereposetup(ui, repo):
82 82 class lfileswirerepository(repo.__class__):
83 83 def putlfile(self, sha, fd):
84 84 # unfortunately, httprepository._callpush tries to convert its
85 85 # input file-like into a bundle before sending it, so we can't use
86 86 # it ...
87 87 if issubclass(self.__class__, httppeer.httppeer):
88 88 res = None
89 89 try:
90 90 res = self._call('putlfile', data=fd, sha=sha,
91 91 headers={'content-type':'application/mercurial-0.1'})
92 92 d, output = res.split('\n', 1)
93 93 for l in output.splitlines(True):
94 94 self.ui.warn(_('remote: '), l) # assume l ends with \n
95 95 return int(d)
96 96 except (ValueError, urllib2.HTTPError):
97 97 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
98 98 return 1
99 99 # ... but we can't use sshrepository._call because the data=
100 100 # argument won't get sent, and _callpush does exactly what we want
101 101 # in this case: send the data straight through
102 102 else:
103 103 try:
104 104 ret, output = self._callpush("putlfile", fd, sha=sha)
105 105 if ret == "":
106 106 raise error.ResponseError(_('putlfile failed:'),
107 107 output)
108 108 return int(ret)
109 109 except IOError:
110 110 return 1
111 111 except ValueError:
112 112 raise error.ResponseError(
113 113 _('putlfile failed (unexpected response):'), ret)
114 114
115 115 def getlfile(self, sha):
116 116 """returns an iterable with the chunks of the file with sha sha"""
117 117 stream = self._callstream("getlfile", sha=sha)
118 118 length = stream.readline()
119 119 try:
120 120 length = int(length)
121 121 except ValueError:
122 122 self._abort(error.ResponseError(_("unexpected response:"),
123 123 length))
124 124
125 125 # SSH streams will block if reading more than length
126 126 for chunk in util.filechunkiter(stream, 128 * 1024, length):
127 127 yield chunk
128 128 # HTTP streams must hit the end to process the last empty
129 129 # chunk of Chunked-Encoding so the connection can be reused.
130 130 if issubclass(self.__class__, httppeer.httppeer):
131 131 chunk = stream.read(1)
132 132 if chunk:
133 133 self._abort(error.ResponseError(_("unexpected response:"),
134 134 chunk))
135 135
136 136 @wireproto.batchable
137 137 def statlfile(self, sha):
138 138 f = wireproto.future()
139 139 result = {'sha': sha}
140 140 yield result, f
141 141 try:
142 142 yield int(f.value)
143 143 except (ValueError, urllib2.HTTPError):
144 144 # If the server returns anything but an integer followed by a
145 145 # newline, newline, it's not speaking our language; if we get
146 146 # an HTTP error, we can't be sure the largefile is present;
147 147 # either way, consider it missing.
148 148 yield 2
149 149
150 150 repo.__class__ = lfileswirerepository
151 151
152 152 # advertise the largefiles=serve capability
153 153 def capabilities(repo, proto):
154 154 return capabilitiesorig(repo, proto) + ' largefiles=serve'
155 155
156 156 def heads(repo, proto):
157 157 if lfutil.islfilesrepo(repo):
158 158 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
159 159 return wireproto.heads(repo, proto)
160 160
161 161 def sshrepocallstream(self, cmd, **args):
162 162 if cmd == 'heads' and self.capable('largefiles'):
163 163 cmd = 'lheads'
164 164 if cmd == 'batch' and self.capable('largefiles'):
165 165 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
166 166 return ssholdcallstream(self, cmd, **args)
167 167
168 168 headsre = re.compile(r'(^|;)heads\b')
169 169
170 170 def httprepocallstream(self, cmd, **args):
171 171 if cmd == 'heads' and self.capable('largefiles'):
172 172 cmd = 'lheads'
173 173 if cmd == 'batch' and self.capable('largefiles'):
174 174 args['cmds'] = headsre.sub('lheads', args['cmds'])
175 175 return httpoldcallstream(self, cmd, **args)
@@ -1,98 +1,98 b''
1 1 # Copyright 2010-2011 Fog Creek Software
2 2 # Copyright 2010-2011 Unity Technologies
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 '''remote largefile store; the base class for wirestore'''
8 8
9 9 import urllib2
10 10
11 11 from mercurial import util, wireproto
12 12 from mercurial.i18n import _
13 13
14 14 import lfutil
15 15 import basestore
16 16
17 17 class remotestore(basestore.basestore):
18 18 '''a largefile store accessed over a network'''
19 19 def __init__(self, ui, repo, url):
20 20 super(remotestore, self).__init__(ui, repo, url)
21 21
22 22 def put(self, source, hash):
23 23 if self.sendfile(source, hash):
24 24 raise util.Abort(
25 25 _('remotestore: could not put %s to remote store %s')
26 26 % (source, util.hidepassword(self.url)))
27 27 self.ui.debug(
28 28 _('remotestore: put %s to remote store %s\n')
29 29 % (source, util.hidepassword(self.url)))
30 30
31 31 def exists(self, hashes):
32 32 return dict((h, s == 0) for (h, s) in # dict-from-generator
33 33 self._stat(hashes).iteritems())
34 34
35 35 def sendfile(self, filename, hash):
36 36 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 37 fd = None
38 38 try:
39 39 fd = lfutil.httpsendfile(self.ui, filename)
40 40 return self._put(hash, fd)
41 except IOError, e:
41 except IOError as e:
42 42 raise util.Abort(
43 43 _('remotestore: could not open file %s: %s')
44 44 % (filename, str(e)))
45 45 finally:
46 46 if fd:
47 47 fd.close()
48 48
49 49 def _getfile(self, tmpfile, filename, hash):
50 50 try:
51 51 chunks = self._get(hash)
52 except urllib2.HTTPError, e:
52 except urllib2.HTTPError as e:
53 53 # 401s get converted to util.Aborts; everything else is fine being
54 54 # turned into a StoreError
55 55 raise basestore.StoreError(filename, hash, self.url, str(e))
56 except urllib2.URLError, e:
56 except urllib2.URLError as e:
57 57 # This usually indicates a connection problem, so don't
58 58 # keep trying with the other files... they will probably
59 59 # all fail too.
60 60 raise util.Abort('%s: %s' %
61 61 (util.hidepassword(self.url), e.reason))
62 except IOError, e:
62 except IOError as e:
63 63 raise basestore.StoreError(filename, hash, self.url, str(e))
64 64
65 65 return lfutil.copyandhash(chunks, tmpfile)
66 66
67 67 def _verifyfile(self, cctx, cset, contents, standin, verified):
68 68 filename = lfutil.splitstandin(standin)
69 69 if not filename:
70 70 return False
71 71 fctx = cctx[standin]
72 72 key = (filename, fctx.filenode())
73 73 if key in verified:
74 74 return False
75 75
76 76 verified.add(key)
77 77
78 78 expecthash = fctx.data()[0:40]
79 79 stat = self._stat([expecthash])[expecthash]
80 80 if not stat:
81 81 return False
82 82 elif stat == 1:
83 83 self.ui.warn(
84 84 _('changeset %s: %s: contents differ\n')
85 85 % (cset, filename))
86 86 return True # failed
87 87 elif stat == 2:
88 88 self.ui.warn(
89 89 _('changeset %s: %s missing\n')
90 90 % (cset, filename))
91 91 return True # failed
92 92 else:
93 93 raise RuntimeError('verify failed: unexpected response from '
94 94 'statlfile (%r)' % stat)
95 95
96 96 def batch(self):
97 97 '''Support for remote batching.'''
98 98 return wireproto.remotebatch(self)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now