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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 | |
|
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 | |
|
306 | 306 | |
|
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 |
@@ -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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
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 |
|
|
71 | except NoRepo as inst: | |
|
72 | 72 | ui.note(_("convert: %s\n") % inst) |
|
73 |
except MissingTool |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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