##// END OF EJS Templates
merge with stable
Matt Mackall -
r20870:6500a2ee merge default
parent child Browse files
Show More

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

@@ -1,563 +1,563 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """style and portability checker for Mercurial
10 """style and portability checker for Mercurial
11
11
12 when a rule triggers wrong, do one of the following (prefer one from top):
12 when a rule triggers wrong, do one of the following (prefer one from top):
13 * do the work-around the rule suggests
13 * do the work-around the rule suggests
14 * doublecheck that it is a false match
14 * doublecheck that it is a false match
15 * improve the rule pattern
15 * improve the rule pattern
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 (you can append a short comment and match this, like: #re-raises, # no-py24)
17 (you can append a short comment and match this, like: #re-raises, # no-py24)
18 * change the pattern to a warning and list the exception in test-check-code-hg
18 * change the pattern to a warning and list the exception in test-check-code-hg
19 * ONLY use no--check-code for skipping entire files from external sources
19 * ONLY use no--check-code for skipping entire files from external sources
20 """
20 """
21
21
22 import re, glob, os, sys
22 import re, glob, os, sys
23 import keyword
23 import keyword
24 import optparse
24 import optparse
25 try:
25 try:
26 import re2
26 import re2
27 except ImportError:
27 except ImportError:
28 re2 = None
28 re2 = None
29
29
30 def compilere(pat, multiline=False):
30 def compilere(pat, multiline=False):
31 if multiline:
31 if multiline:
32 pat = '(?m)' + pat
32 pat = '(?m)' + pat
33 if re2:
33 if re2:
34 try:
34 try:
35 return re2.compile(pat)
35 return re2.compile(pat)
36 except re2.error:
36 except re2.error:
37 pass
37 pass
38 return re.compile(pat)
38 return re.compile(pat)
39
39
40 def repquote(m):
40 def repquote(m):
41 fromc = '.:'
41 fromc = '.:'
42 tochr = 'pq'
42 tochr = 'pq'
43 def encodechr(i):
43 def encodechr(i):
44 if i > 255:
44 if i > 255:
45 return 'u'
45 return 'u'
46 c = chr(i)
46 c = chr(i)
47 if c in ' \n':
47 if c in ' \n':
48 return c
48 return c
49 if c.isalpha():
49 if c.isalpha():
50 return 'x'
50 return 'x'
51 if c.isdigit():
51 if c.isdigit():
52 return 'n'
52 return 'n'
53 try:
53 try:
54 return tochr[fromc.find(c)]
54 return tochr[fromc.find(c)]
55 except (ValueError, IndexError):
55 except (ValueError, IndexError):
56 return 'o'
56 return 'o'
57 t = m.group('text')
57 t = m.group('text')
58 tt = ''.join(encodechr(i) for i in xrange(256))
58 tt = ''.join(encodechr(i) for i in xrange(256))
59 t = t.translate(tt)
59 t = t.translate(tt)
60 return m.group('quote') + t + m.group('quote')
60 return m.group('quote') + t + m.group('quote')
61
61
62 def reppython(m):
62 def reppython(m):
63 comment = m.group('comment')
63 comment = m.group('comment')
64 if comment:
64 if comment:
65 l = len(comment.rstrip())
65 l = len(comment.rstrip())
66 return "#" * l + comment[l:]
66 return "#" * l + comment[l:]
67 return repquote(m)
67 return repquote(m)
68
68
69 def repcomment(m):
69 def repcomment(m):
70 return m.group(1) + "#" * len(m.group(2))
70 return m.group(1) + "#" * len(m.group(2))
71
71
72 def repccomment(m):
72 def repccomment(m):
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
74 return m.group(1) + t + "*/"
74 return m.group(1) + t + "*/"
75
75
76 def repcallspaces(m):
76 def repcallspaces(m):
77 t = re.sub(r"\n\s+", "\n", m.group(2))
77 t = re.sub(r"\n\s+", "\n", m.group(2))
78 return m.group(1) + t
78 return m.group(1) + t
79
79
80 def repinclude(m):
80 def repinclude(m):
81 return m.group(1) + "<foo>"
81 return m.group(1) + "<foo>"
82
82
83 def rephere(m):
83 def rephere(m):
84 t = re.sub(r"\S", "x", m.group(2))
84 t = re.sub(r"\S", "x", m.group(2))
85 return m.group(1) + t
85 return m.group(1) + t
86
86
87
87
88 testpats = [
88 testpats = [
89 [
89 [
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
97 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
97 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
107 "use egrep for extended grep syntax"),
107 "use egrep for extended grep syntax"),
108 (r'/bin/', "don't use explicit paths for tools"),
108 (r'/bin/', "don't use explicit paths for tools"),
109 (r'[^\n]\Z', "no trailing newline"),
109 (r'[^\n]\Z', "no trailing newline"),
110 (r'export.*=', "don't export and assign at once"),
110 (r'export.*=', "don't export and assign at once"),
111 (r'^source\b', "don't use 'source', use '.'"),
111 (r'^source\b', "don't use 'source', use '.'"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
117 (r'^alias\b.*=', "don't use alias, use a function"),
117 (r'^alias\b.*=', "don't use alias, use a function"),
118 (r'if\s*!', "don't use '!' to negate exit status"),
118 (r'if\s*!', "don't use '!' to negate exit status"),
119 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
119 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
120 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
120 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
121 (r'^( *)\t', "don't use tabs to indent"),
121 (r'^( *)\t', "don't use tabs to indent"),
122 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
122 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
123 "put a backslash-escaped newline after sed 'i' command"),
123 "put a backslash-escaped newline after sed 'i' command"),
124 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
124 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
125 ],
125 ],
126 # warnings
126 # warnings
127 [
127 [
128 (r'^function', "don't use 'function', use old style"),
128 (r'^function', "don't use 'function', use old style"),
129 (r'^diff.*-\w*N', "don't use 'diff -N'"),
129 (r'^diff.*-\w*N', "don't use 'diff -N'"),
130 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
130 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
131 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
131 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
132 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
132 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
133 ]
133 ]
134 ]
134 ]
135
135
136 testfilters = [
136 testfilters = [
137 (r"( *)(#([^\n]*\S)?)", repcomment),
137 (r"( *)(#([^\n]*\S)?)", repcomment),
138 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
138 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
139 ]
139 ]
140
140
141 winglobmsg = "use (glob) to match Windows paths too"
141 winglobmsg = "use (glob) to match Windows paths too"
142 uprefix = r"^ \$ "
142 uprefix = r"^ \$ "
143 utestpats = [
143 utestpats = [
144 [
144 [
145 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
145 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
146 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
146 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
147 "use regex test output patterns instead of sed"),
147 "use regex test output patterns instead of sed"),
148 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
148 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
149 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
149 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
150 (uprefix + r'.*\|\| echo.*(fail|error)',
150 (uprefix + r'.*\|\| echo.*(fail|error)',
151 "explicit exit code checks unnecessary"),
151 "explicit exit code checks unnecessary"),
152 (uprefix + r'set -e', "don't use set -e"),
152 (uprefix + r'set -e', "don't use set -e"),
153 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
153 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
154 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
154 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
155 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
155 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
156 'hg pull -q file:../test'), # in test-pull.t which is skipped on windows
156 'hg pull -q file:../test'), # in test-pull.t which is skipped on windows
157 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
157 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
158 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
158 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
159 winglobmsg),
159 winglobmsg),
160 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
160 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
161 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
161 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
162 (r'^ reverting .*/.*[^)]$', winglobmsg),
162 (r'^ reverting .*/.*[^)]$', winglobmsg),
163 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
163 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
164 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
164 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
165 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
165 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
166 (r'^ moving \S+/.*[^)]$', winglobmsg),
166 (r'^ moving \S+/.*[^)]$', winglobmsg),
167 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
167 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
168 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
168 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
169 (r'^ .*file://\$TESTTMP',
169 (r'^ .*file://\$TESTTMP',
170 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
170 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
171 ],
171 ],
172 # warnings
172 # warnings
173 [
173 [
174 (r'^ [^*?/\n]* \(glob\)$',
174 (r'^ [^*?/\n]* \(glob\)$',
175 "glob match with no glob character (?*/)"),
175 "glob match with no glob character (?*/)"),
176 ]
176 ]
177 ]
177 ]
178
178
179 for i in [0, 1]:
179 for i in [0, 1]:
180 for p, m in testpats[i]:
180 for p, m in testpats[i]:
181 if p.startswith(r'^'):
181 if p.startswith(r'^'):
182 p = r"^ [$>] (%s)" % p[1:]
182 p = r"^ [$>] (%s)" % p[1:]
183 else:
183 else:
184 p = r"^ [$>] .*(%s)" % p
184 p = r"^ [$>] .*(%s)" % p
185 utestpats[i].append((p, m))
185 utestpats[i].append((p, m))
186
186
187 utestfilters = [
187 utestfilters = [
188 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
188 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
189 (r"( *)(#([^\n]*\S)?)", repcomment),
189 (r"( *)(#([^\n]*\S)?)", repcomment),
190 ]
190 ]
191
191
192 pypats = [
192 pypats = [
193 [
193 [
194 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
194 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
195 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
195 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
196 "tuple parameter unpacking not available in Python 3+"),
196 "tuple parameter unpacking not available in Python 3+"),
197 (r'lambda\s*\(.*,.*\)',
197 (r'lambda\s*\(.*,.*\)',
198 "tuple parameter unpacking not available in Python 3+"),
198 "tuple parameter unpacking not available in Python 3+"),
199 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
199 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
200 '2to3 can\'t always rewrite "import qux, foo.bar", '
200 '2to3 can\'t always rewrite "import qux, foo.bar", '
201 'use "import foo.bar" on its own line instead.'),
201 'use "import foo.bar" on its own line instead.'),
202 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
202 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
203 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
203 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
204 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
204 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
205 'dict-from-generator'),
205 'dict-from-generator'),
206 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
206 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
207 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
207 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
208 (r'^\s*\t', "don't use tabs"),
208 (r'^\s*\t', "don't use tabs"),
209 (r'\S;\s*\n', "semicolon"),
209 (r'\S;\s*\n', "semicolon"),
210 (r'[^_]_\("[^"]+"\s*%', "don't use % inside _()"),
210 (r'[^_]_\((?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
211 (r"[^_]_\('[^']+'\s*%", "don't use % inside _()"),
211 (r"[^_]_\((?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
212 (r'(\w|\)),\w', "missing whitespace after ,"),
212 (r'(\w|\)),\w', "missing whitespace after ,"),
213 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
213 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
214 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
214 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
215 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
215 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
216 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Python 2.4'),
216 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Python 2.4'),
217 (r'(?<!def)(\s+|^|\()next\(.+\)',
217 (r'(?<!def)(\s+|^|\()next\(.+\)',
218 'no next(foo) in Python 2.4 and 2.5, use foo.next() instead'),
218 'no next(foo) in Python 2.4 and 2.5, use foo.next() instead'),
219 (r'(\s+)try:\n((?:\n|\1\s.*\n)*?)\1\s*yield\b.*?'
219 (r'(\s+)try:\n((?:\n|\1\s.*\n)*?)\1\s*yield\b.*?'
220 r'((?:\n|\1\s.*\n)+?)\1finally:',
220 r'((?:\n|\1\s.*\n)+?)\1finally:',
221 'no yield inside try/finally in Python 2.4'),
221 'no yield inside try/finally in Python 2.4'),
222 (r'.{81}', "line too long"),
222 (r'.{81}', "line too long"),
223 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
223 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
224 (r'[^\n]\Z', "no trailing newline"),
224 (r'[^\n]\Z', "no trailing newline"),
225 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
225 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
226 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
226 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
227 # "don't use underbars in identifiers"),
227 # "don't use underbars in identifiers"),
228 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
228 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
229 "don't use camelcase in identifiers"),
229 "don't use camelcase in identifiers"),
230 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
230 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
231 "linebreak after :"),
231 "linebreak after :"),
232 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
232 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
233 (r'class\s[^( \n]+\(\):',
233 (r'class\s[^( \n]+\(\):',
234 "class foo() not available in Python 2.4, use class foo(object)"),
234 "class foo() not available in Python 2.4, use class foo(object)"),
235 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
235 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
236 "Python keyword is not a function"),
236 "Python keyword is not a function"),
237 (r',]', "unneeded trailing ',' in list"),
237 (r',]', "unneeded trailing ',' in list"),
238 # (r'class\s[A-Z][^\(]*\((?!Exception)',
238 # (r'class\s[A-Z][^\(]*\((?!Exception)',
239 # "don't capitalize non-exception classes"),
239 # "don't capitalize non-exception classes"),
240 # (r'in range\(', "use xrange"),
240 # (r'in range\(', "use xrange"),
241 # (r'^\s*print\s+', "avoid using print in core and extensions"),
241 # (r'^\s*print\s+', "avoid using print in core and extensions"),
242 (r'[\x80-\xff]', "non-ASCII character literal"),
242 (r'[\x80-\xff]', "non-ASCII character literal"),
243 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
243 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
244 (r'^\s*with\s+', "with not available in Python 2.4"),
244 (r'^\s*with\s+', "with not available in Python 2.4"),
245 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
245 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
246 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
246 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
247 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
247 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
248 (r'(?<!def)\s+(any|all|format)\(',
248 (r'(?<!def)\s+(any|all|format)\(',
249 "any/all/format not available in Python 2.4", 'no-py24'),
249 "any/all/format not available in Python 2.4", 'no-py24'),
250 (r'(?<!def)\s+(callable)\(',
250 (r'(?<!def)\s+(callable)\(',
251 "callable not available in Python 3, use getattr(f, '__call__', None)"),
251 "callable not available in Python 3, use getattr(f, '__call__', None)"),
252 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
252 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
253 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
253 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
254 "gratuitous whitespace after Python keyword"),
254 "gratuitous whitespace after Python keyword"),
255 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
255 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
256 # (r'\s\s=', "gratuitous whitespace before ="),
256 # (r'\s\s=', "gratuitous whitespace before ="),
257 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
257 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
258 "missing whitespace around operator"),
258 "missing whitespace around operator"),
259 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
259 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
260 "missing whitespace around operator"),
260 "missing whitespace around operator"),
261 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
261 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
262 "missing whitespace around operator"),
262 "missing whitespace around operator"),
263 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
263 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
264 "wrong whitespace around ="),
264 "wrong whitespace around ="),
265 (r'\([^()]*( =[^=]|[^<>!=]= )',
265 (r'\([^()]*( =[^=]|[^<>!=]= )',
266 "no whitespace around = for named parameters"),
266 "no whitespace around = for named parameters"),
267 (r'raise Exception', "don't raise generic exceptions"),
267 (r'raise Exception', "don't raise generic exceptions"),
268 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
268 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
269 "don't use old-style two-argument raise, use Exception(message)"),
269 "don't use old-style two-argument raise, use Exception(message)"),
270 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
270 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
271 (r' [=!]=\s+(True|False|None)',
271 (r' [=!]=\s+(True|False|None)',
272 "comparison with singleton, use 'is' or 'is not' instead"),
272 "comparison with singleton, use 'is' or 'is not' instead"),
273 (r'^\s*(while|if) [01]:',
273 (r'^\s*(while|if) [01]:',
274 "use True/False for constant Boolean expression"),
274 "use True/False for constant Boolean expression"),
275 (r'(?:(?<!def)\s+|\()hasattr',
275 (r'(?:(?<!def)\s+|\()hasattr',
276 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
276 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
277 (r'opener\([^)]*\).read\(',
277 (r'opener\([^)]*\).read\(',
278 "use opener.read() instead"),
278 "use opener.read() instead"),
279 (r'BaseException', 'not in Python 2.4, use Exception'),
279 (r'BaseException', 'not in Python 2.4, use Exception'),
280 (r'os\.path\.relpath', 'os.path.relpath is not in Python 2.5'),
280 (r'os\.path\.relpath', 'os.path.relpath is not in Python 2.5'),
281 (r'opener\([^)]*\).write\(',
281 (r'opener\([^)]*\).write\(',
282 "use opener.write() instead"),
282 "use opener.write() instead"),
283 (r'[\s\(](open|file)\([^)]*\)\.read\(',
283 (r'[\s\(](open|file)\([^)]*\)\.read\(',
284 "use util.readfile() instead"),
284 "use util.readfile() instead"),
285 (r'[\s\(](open|file)\([^)]*\)\.write\(',
285 (r'[\s\(](open|file)\([^)]*\)\.write\(',
286 "use util.writefile() instead"),
286 "use util.writefile() instead"),
287 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
287 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
288 "always assign an opened file to a variable, and close it afterwards"),
288 "always assign an opened file to a variable, and close it afterwards"),
289 (r'[\s\(](open|file)\([^)]*\)\.',
289 (r'[\s\(](open|file)\([^)]*\)\.',
290 "always assign an opened file to a variable, and close it afterwards"),
290 "always assign an opened file to a variable, and close it afterwards"),
291 (r'(?i)descendent', "the proper spelling is descendAnt"),
291 (r'(?i)descendent', "the proper spelling is descendAnt"),
292 (r'\.debug\(\_', "don't mark debug messages for translation"),
292 (r'\.debug\(\_', "don't mark debug messages for translation"),
293 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
293 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
294 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
294 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
295 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
295 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
296 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
296 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
297 "missing _() in ui message (use () to hide false-positives)"),
297 "missing _() in ui message (use () to hide false-positives)"),
298 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
298 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
299 ],
299 ],
300 # warnings
300 # warnings
301 [
301 [
302 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
302 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
303 ]
303 ]
304 ]
304 ]
305
305
306 pyfilters = [
306 pyfilters = [
307 (r"""(?msx)(?P<comment>\#.*?$)|
307 (r"""(?msx)(?P<comment>\#.*?$)|
308 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
308 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
309 (?P<text>(([^\\]|\\.)*?))
309 (?P<text>(([^\\]|\\.)*?))
310 (?P=quote))""", reppython),
310 (?P=quote))""", reppython),
311 ]
311 ]
312
312
313 txtfilters = []
313 txtfilters = []
314
314
315 txtpats = [
315 txtpats = [
316 [
316 [
317 ('\s$', 'trailing whitespace'),
317 ('\s$', 'trailing whitespace'),
318 ('.. note::[ \n][^\n]', 'add two newlines after note::')
318 ('.. note::[ \n][^\n]', 'add two newlines after note::')
319 ],
319 ],
320 []
320 []
321 ]
321 ]
322
322
323 cpats = [
323 cpats = [
324 [
324 [
325 (r'//', "don't use //-style comments"),
325 (r'//', "don't use //-style comments"),
326 (r'^ ', "don't use spaces to indent"),
326 (r'^ ', "don't use spaces to indent"),
327 (r'\S\t', "don't use tabs except for indent"),
327 (r'\S\t', "don't use tabs except for indent"),
328 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
328 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
329 (r'.{81}', "line too long"),
329 (r'.{81}', "line too long"),
330 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
330 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
331 (r'return\(', "return is not a function"),
331 (r'return\(', "return is not a function"),
332 (r' ;', "no space before ;"),
332 (r' ;', "no space before ;"),
333 (r'[)][{]', "space between ) and {"),
333 (r'[)][{]', "space between ) and {"),
334 (r'\w+\* \w+', "use int *foo, not int* foo"),
334 (r'\w+\* \w+', "use int *foo, not int* foo"),
335 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
335 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
336 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
336 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
337 (r'\w,\w', "missing whitespace after ,"),
337 (r'\w,\w', "missing whitespace after ,"),
338 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
338 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
339 (r'^#\s+\w', "use #foo, not # foo"),
339 (r'^#\s+\w', "use #foo, not # foo"),
340 (r'[^\n]\Z', "no trailing newline"),
340 (r'[^\n]\Z', "no trailing newline"),
341 (r'^\s*#import\b', "use only #include in standard C code"),
341 (r'^\s*#import\b', "use only #include in standard C code"),
342 ],
342 ],
343 # warnings
343 # warnings
344 []
344 []
345 ]
345 ]
346
346
347 cfilters = [
347 cfilters = [
348 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
348 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
349 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
349 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
350 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
350 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
351 (r'(\()([^)]+\))', repcallspaces),
351 (r'(\()([^)]+\))', repcallspaces),
352 ]
352 ]
353
353
354 inutilpats = [
354 inutilpats = [
355 [
355 [
356 (r'\bui\.', "don't use ui in util"),
356 (r'\bui\.', "don't use ui in util"),
357 ],
357 ],
358 # warnings
358 # warnings
359 []
359 []
360 ]
360 ]
361
361
362 inrevlogpats = [
362 inrevlogpats = [
363 [
363 [
364 (r'\brepo\.', "don't use repo in revlog"),
364 (r'\brepo\.', "don't use repo in revlog"),
365 ],
365 ],
366 # warnings
366 # warnings
367 []
367 []
368 ]
368 ]
369
369
370 checks = [
370 checks = [
371 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
371 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
372 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
372 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
373 ('c', r'.*\.[ch]$', cfilters, cpats),
373 ('c', r'.*\.[ch]$', cfilters, cpats),
374 ('unified test', r'.*\.t$', utestfilters, utestpats),
374 ('unified test', r'.*\.t$', utestfilters, utestpats),
375 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
375 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
376 inrevlogpats),
376 inrevlogpats),
377 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
377 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
378 inutilpats),
378 inutilpats),
379 ('txt', r'.*\.txt$', txtfilters, txtpats),
379 ('txt', r'.*\.txt$', txtfilters, txtpats),
380 ]
380 ]
381
381
382 def _preparepats():
382 def _preparepats():
383 for c in checks:
383 for c in checks:
384 failandwarn = c[-1]
384 failandwarn = c[-1]
385 for pats in failandwarn:
385 for pats in failandwarn:
386 for i, pseq in enumerate(pats):
386 for i, pseq in enumerate(pats):
387 # fix-up regexes for multi-line searches
387 # fix-up regexes for multi-line searches
388 p = pseq[0]
388 p = pseq[0]
389 # \s doesn't match \n
389 # \s doesn't match \n
390 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
390 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
391 # [^...] doesn't match newline
391 # [^...] doesn't match newline
392 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
392 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
393
393
394 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
394 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
395 filters = c[2]
395 filters = c[2]
396 for i, flt in enumerate(filters):
396 for i, flt in enumerate(filters):
397 filters[i] = re.compile(flt[0]), flt[1]
397 filters[i] = re.compile(flt[0]), flt[1]
398 _preparepats()
398 _preparepats()
399
399
400 class norepeatlogger(object):
400 class norepeatlogger(object):
401 def __init__(self):
401 def __init__(self):
402 self._lastseen = None
402 self._lastseen = None
403
403
404 def log(self, fname, lineno, line, msg, blame):
404 def log(self, fname, lineno, line, msg, blame):
405 """print error related a to given line of a given file.
405 """print error related a to given line of a given file.
406
406
407 The faulty line will also be printed but only once in the case
407 The faulty line will also be printed but only once in the case
408 of multiple errors.
408 of multiple errors.
409
409
410 :fname: filename
410 :fname: filename
411 :lineno: line number
411 :lineno: line number
412 :line: actual content of the line
412 :line: actual content of the line
413 :msg: error message
413 :msg: error message
414 """
414 """
415 msgid = fname, lineno, line
415 msgid = fname, lineno, line
416 if msgid != self._lastseen:
416 if msgid != self._lastseen:
417 if blame:
417 if blame:
418 print "%s:%d (%s):" % (fname, lineno, blame)
418 print "%s:%d (%s):" % (fname, lineno, blame)
419 else:
419 else:
420 print "%s:%d:" % (fname, lineno)
420 print "%s:%d:" % (fname, lineno)
421 print " > %s" % line
421 print " > %s" % line
422 self._lastseen = msgid
422 self._lastseen = msgid
423 print " " + msg
423 print " " + msg
424
424
425 _defaultlogger = norepeatlogger()
425 _defaultlogger = norepeatlogger()
426
426
427 def getblame(f):
427 def getblame(f):
428 lines = []
428 lines = []
429 for l in os.popen('hg annotate -un %s' % f):
429 for l in os.popen('hg annotate -un %s' % f):
430 start, line = l.split(':', 1)
430 start, line = l.split(':', 1)
431 user, rev = start.split()
431 user, rev = start.split()
432 lines.append((line[1:-1], user, rev))
432 lines.append((line[1:-1], user, rev))
433 return lines
433 return lines
434
434
435 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
435 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
436 blame=False, debug=False, lineno=True):
436 blame=False, debug=False, lineno=True):
437 """checks style and portability of a given file
437 """checks style and portability of a given file
438
438
439 :f: filepath
439 :f: filepath
440 :logfunc: function used to report error
440 :logfunc: function used to report error
441 logfunc(filename, linenumber, linecontent, errormessage)
441 logfunc(filename, linenumber, linecontent, errormessage)
442 :maxerr: number of error to display before aborting.
442 :maxerr: number of error to display before aborting.
443 Set to false (default) to report all errors
443 Set to false (default) to report all errors
444
444
445 return True if no error is found, False otherwise.
445 return True if no error is found, False otherwise.
446 """
446 """
447 blamecache = None
447 blamecache = None
448 result = True
448 result = True
449 for name, match, filters, pats in checks:
449 for name, match, filters, pats in checks:
450 if debug:
450 if debug:
451 print name, f
451 print name, f
452 fc = 0
452 fc = 0
453 if not re.match(match, f):
453 if not re.match(match, f):
454 if debug:
454 if debug:
455 print "Skipping %s for %s it doesn't match %s" % (
455 print "Skipping %s for %s it doesn't match %s" % (
456 name, match, f)
456 name, match, f)
457 continue
457 continue
458 try:
458 try:
459 fp = open(f)
459 fp = open(f)
460 except IOError, e:
460 except IOError, e:
461 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
461 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
462 continue
462 continue
463 pre = post = fp.read()
463 pre = post = fp.read()
464 fp.close()
464 fp.close()
465 if "no-" "check-code" in pre:
465 if "no-" "check-code" in pre:
466 print "Skipping %s it has no-" "check-code" % f
466 print "Skipping %s it has no-" "check-code" % f
467 return "Skip" # skip checking this file
467 return "Skip" # skip checking this file
468 for p, r in filters:
468 for p, r in filters:
469 post = re.sub(p, r, post)
469 post = re.sub(p, r, post)
470 nerrs = len(pats[0]) # nerr elements are errors
470 nerrs = len(pats[0]) # nerr elements are errors
471 if warnings:
471 if warnings:
472 pats = pats[0] + pats[1]
472 pats = pats[0] + pats[1]
473 else:
473 else:
474 pats = pats[0]
474 pats = pats[0]
475 # print post # uncomment to show filtered version
475 # print post # uncomment to show filtered version
476
476
477 if debug:
477 if debug:
478 print "Checking %s for %s" % (name, f)
478 print "Checking %s for %s" % (name, f)
479
479
480 prelines = None
480 prelines = None
481 errors = []
481 errors = []
482 for i, pat in enumerate(pats):
482 for i, pat in enumerate(pats):
483 if len(pat) == 3:
483 if len(pat) == 3:
484 p, msg, ignore = pat
484 p, msg, ignore = pat
485 else:
485 else:
486 p, msg = pat
486 p, msg = pat
487 ignore = None
487 ignore = None
488 if i >= nerrs:
488 if i >= nerrs:
489 msg = "warning: " + msg
489 msg = "warning: " + msg
490
490
491 pos = 0
491 pos = 0
492 n = 0
492 n = 0
493 for m in p.finditer(post):
493 for m in p.finditer(post):
494 if prelines is None:
494 if prelines is None:
495 prelines = pre.splitlines()
495 prelines = pre.splitlines()
496 postlines = post.splitlines(True)
496 postlines = post.splitlines(True)
497
497
498 start = m.start()
498 start = m.start()
499 while n < len(postlines):
499 while n < len(postlines):
500 step = len(postlines[n])
500 step = len(postlines[n])
501 if pos + step > start:
501 if pos + step > start:
502 break
502 break
503 pos += step
503 pos += step
504 n += 1
504 n += 1
505 l = prelines[n]
505 l = prelines[n]
506
506
507 if ignore and re.search(ignore, l, re.MULTILINE):
507 if ignore and re.search(ignore, l, re.MULTILINE):
508 if debug:
508 if debug:
509 print "Skipping %s for %s:%s (ignore pattern)" % (
509 print "Skipping %s for %s:%s (ignore pattern)" % (
510 name, f, n)
510 name, f, n)
511 continue
511 continue
512 bd = ""
512 bd = ""
513 if blame:
513 if blame:
514 bd = 'working directory'
514 bd = 'working directory'
515 if not blamecache:
515 if not blamecache:
516 blamecache = getblame(f)
516 blamecache = getblame(f)
517 if n < len(blamecache):
517 if n < len(blamecache):
518 bl, bu, br = blamecache[n]
518 bl, bu, br = blamecache[n]
519 if bl == l:
519 if bl == l:
520 bd = '%s@%s' % (bu, br)
520 bd = '%s@%s' % (bu, br)
521
521
522 errors.append((f, lineno and n + 1, l, msg, bd))
522 errors.append((f, lineno and n + 1, l, msg, bd))
523 result = False
523 result = False
524
524
525 errors.sort()
525 errors.sort()
526 for e in errors:
526 for e in errors:
527 logfunc(*e)
527 logfunc(*e)
528 fc += 1
528 fc += 1
529 if maxerr and fc >= maxerr:
529 if maxerr and fc >= maxerr:
530 print " (too many errors, giving up)"
530 print " (too many errors, giving up)"
531 break
531 break
532
532
533 return result
533 return result
534
534
535 if __name__ == "__main__":
535 if __name__ == "__main__":
536 parser = optparse.OptionParser("%prog [options] [files]")
536 parser = optparse.OptionParser("%prog [options] [files]")
537 parser.add_option("-w", "--warnings", action="store_true",
537 parser.add_option("-w", "--warnings", action="store_true",
538 help="include warning-level checks")
538 help="include warning-level checks")
539 parser.add_option("-p", "--per-file", type="int",
539 parser.add_option("-p", "--per-file", type="int",
540 help="max warnings per file")
540 help="max warnings per file")
541 parser.add_option("-b", "--blame", action="store_true",
541 parser.add_option("-b", "--blame", action="store_true",
542 help="use annotate to generate blame info")
542 help="use annotate to generate blame info")
543 parser.add_option("", "--debug", action="store_true",
543 parser.add_option("", "--debug", action="store_true",
544 help="show debug information")
544 help="show debug information")
545 parser.add_option("", "--nolineno", action="store_false",
545 parser.add_option("", "--nolineno", action="store_false",
546 dest='lineno', help="don't show line numbers")
546 dest='lineno', help="don't show line numbers")
547
547
548 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
548 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
549 lineno=True)
549 lineno=True)
550 (options, args) = parser.parse_args()
550 (options, args) = parser.parse_args()
551
551
552 if len(args) == 0:
552 if len(args) == 0:
553 check = glob.glob("*")
553 check = glob.glob("*")
554 else:
554 else:
555 check = args
555 check = args
556
556
557 ret = 0
557 ret = 0
558 for f in check:
558 for f in check:
559 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
559 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
560 blame=options.blame, debug=options.debug,
560 blame=options.blame, debug=options.debug,
561 lineno=options.lineno):
561 lineno=options.lineno):
562 ret = 1
562 ret = 1
563 sys.exit(ret)
563 sys.exit(ret)
@@ -1,350 +1,350 b''
1 """automatically manage newlines in repository files
1 """automatically manage newlines in repository files
2
2
3 This extension allows you to manage the type of line endings (CRLF or
3 This extension allows you to manage the type of line endings (CRLF or
4 LF) that are used in the repository and in the local working
4 LF) that are used in the repository and in the local working
5 directory. That way you can get CRLF line endings on Windows and LF on
5 directory. That way you can get CRLF line endings on Windows and LF on
6 Unix/Mac, thereby letting everybody use their OS native line endings.
6 Unix/Mac, thereby letting everybody use their OS native line endings.
7
7
8 The extension reads its configuration from a versioned ``.hgeol``
8 The extension reads its configuration from a versioned ``.hgeol``
9 configuration file found in the root of the working copy. The
9 configuration file found in the root of the working copy. The
10 ``.hgeol`` file use the same syntax as all other Mercurial
10 ``.hgeol`` file use the same syntax as all other Mercurial
11 configuration files. It uses two sections, ``[patterns]`` and
11 configuration files. It uses two sections, ``[patterns]`` and
12 ``[repository]``.
12 ``[repository]``.
13
13
14 The ``[patterns]`` section specifies how line endings should be
14 The ``[patterns]`` section specifies how line endings should be
15 converted between the working copy and the repository. The format is
15 converted between the working copy and the repository. The format is
16 specified by a file pattern. The first match is used, so put more
16 specified by a file pattern. The first match is used, so put more
17 specific patterns first. The available line endings are ``LF``,
17 specific patterns first. The available line endings are ``LF``,
18 ``CRLF``, and ``BIN``.
18 ``CRLF``, and ``BIN``.
19
19
20 Files with the declared format of ``CRLF`` or ``LF`` are always
20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 checked out and stored in the repository in that format and files
21 checked out and stored in the repository in that format and files
22 declared to be binary (``BIN``) are left unchanged. Additionally,
22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 ``native`` is an alias for checking out in the platform's default line
23 ``native`` is an alias for checking out in the platform's default line
24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 default behaviour; it is only needed if you need to override a later,
26 default behaviour; it is only needed if you need to override a later,
27 more general pattern.
27 more general pattern.
28
28
29 The optional ``[repository]`` section specifies the line endings to
29 The optional ``[repository]`` section specifies the line endings to
30 use for files stored in the repository. It has a single setting,
30 use for files stored in the repository. It has a single setting,
31 ``native``, which determines the storage line endings for files
31 ``native``, which determines the storage line endings for files
32 declared as ``native`` in the ``[patterns]`` section. It can be set to
32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 that on Windows, files configured as ``native`` (``CRLF`` by default)
34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 will be converted to ``LF`` when stored in the repository. Files
35 will be converted to ``LF`` when stored in the repository. Files
36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 are always stored as-is in the repository.
37 are always stored as-is in the repository.
38
38
39 Example versioned ``.hgeol`` file::
39 Example versioned ``.hgeol`` file::
40
40
41 [patterns]
41 [patterns]
42 **.py = native
42 **.py = native
43 **.vcproj = CRLF
43 **.vcproj = CRLF
44 **.txt = native
44 **.txt = native
45 Makefile = LF
45 Makefile = LF
46 **.jpg = BIN
46 **.jpg = BIN
47
47
48 [repository]
48 [repository]
49 native = LF
49 native = LF
50
50
51 .. note::
51 .. note::
52
52
53 The rules will first apply when files are touched in the working
53 The rules will first apply when files are touched in the working
54 copy, e.g. by updating to null and back to tip to touch all files.
54 copy, e.g. by updating to null and back to tip to touch all files.
55
55
56 The extension uses an optional ``[eol]`` section read from both the
56 The extension uses an optional ``[eol]`` section read from both the
57 normal Mercurial configuration files and the ``.hgeol`` file, with the
57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 latter overriding the former. You can use that section to control the
58 latter overriding the former. You can use that section to control the
59 overall behavior. There are three settings:
59 overall behavior. There are three settings:
60
60
61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 ``CRLF`` to override the default interpretation of ``native`` for
62 ``CRLF`` to override the default interpretation of ``native`` for
63 checkout. This can be used with :hg:`archive` on Unix, say, to
63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 generate an archive where files have line endings for Windows.
64 generate an archive where files have line endings for Windows.
65
65
66 - ``eol.only-consistent`` (default True) can be set to False to make
66 - ``eol.only-consistent`` (default True) can be set to False to make
67 the extension convert files with inconsistent EOLs. Inconsistent
67 the extension convert files with inconsistent EOLs. Inconsistent
68 means that there is both ``CRLF`` and ``LF`` present in the file.
68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 Such files are normally not touched under the assumption that they
69 Such files are normally not touched under the assumption that they
70 have mixed EOLs on purpose.
70 have mixed EOLs on purpose.
71
71
72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 ensure that converted files end with a EOL character (either ``\\n``
73 ensure that converted files end with a EOL character (either ``\\n``
74 or ``\\r\\n`` as per the configured patterns).
74 or ``\\r\\n`` as per the configured patterns).
75
75
76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 like the deprecated win32text extension does. This means that you can
77 like the deprecated win32text extension does. This means that you can
78 disable win32text and enable eol and your filters will still work. You
78 disable win32text and enable eol and your filters will still work. You
79 only need to these filters until you have prepared a ``.hgeol`` file.
79 only need to these filters until you have prepared a ``.hgeol`` file.
80
80
81 The ``win32text.forbid*`` hooks provided by the win32text extension
81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 have been unified into a single hook named ``eol.checkheadshook``. The
82 have been unified into a single hook named ``eol.checkheadshook``. The
83 hook will lookup the expected line endings from the ``.hgeol`` file,
83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 which means you must migrate to a ``.hgeol`` file first before using
84 which means you must migrate to a ``.hgeol`` file first before using
85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 invalid revisions will be pushed. To forbid them completely, use the
86 invalid revisions will be pushed. To forbid them completely, use the
87 ``eol.checkallhook`` hook. These hooks are best used as
87 ``eol.checkallhook`` hook. These hooks are best used as
88 ``pretxnchangegroup`` hooks.
88 ``pretxnchangegroup`` hooks.
89
89
90 See :hg:`help patterns` for more information about the glob patterns
90 See :hg:`help patterns` for more information about the glob patterns
91 used.
91 used.
92 """
92 """
93
93
94 from mercurial.i18n import _
94 from mercurial.i18n import _
95 from mercurial import util, config, extensions, match, error
95 from mercurial import util, config, extensions, match, error
96 import re, os
96 import re, os
97
97
98 testedwith = 'internal'
98 testedwith = 'internal'
99
99
100 # Matches a lone LF, i.e., one that is not part of CRLF.
100 # Matches a lone LF, i.e., one that is not part of CRLF.
101 singlelf = re.compile('(^|[^\r])\n')
101 singlelf = re.compile('(^|[^\r])\n')
102 # Matches a single EOL which can either be a CRLF where repeated CR
102 # Matches a single EOL which can either be a CRLF where repeated CR
103 # are removed or a LF. We do not care about old Macintosh files, so a
103 # are removed or a LF. We do not care about old Macintosh files, so a
104 # stray CR is an error.
104 # stray CR is an error.
105 eolre = re.compile('\r*\n')
105 eolre = re.compile('\r*\n')
106
106
107
107
108 def inconsistenteol(data):
108 def inconsistenteol(data):
109 return '\r\n' in data and singlelf.search(data)
109 return '\r\n' in data and singlelf.search(data)
110
110
111 def tolf(s, params, ui, **kwargs):
111 def tolf(s, params, ui, **kwargs):
112 """Filter to convert to LF EOLs."""
112 """Filter to convert to LF EOLs."""
113 if util.binary(s):
113 if util.binary(s):
114 return s
114 return s
115 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
115 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
116 return s
116 return s
117 if (ui.configbool('eol', 'fix-trailing-newline', False)
117 if (ui.configbool('eol', 'fix-trailing-newline', False)
118 and s and s[-1] != '\n'):
118 and s and s[-1] != '\n'):
119 s = s + '\n'
119 s = s + '\n'
120 return eolre.sub('\n', s)
120 return eolre.sub('\n', s)
121
121
122 def tocrlf(s, params, ui, **kwargs):
122 def tocrlf(s, params, ui, **kwargs):
123 """Filter to convert to CRLF EOLs."""
123 """Filter to convert to CRLF EOLs."""
124 if util.binary(s):
124 if util.binary(s):
125 return s
125 return s
126 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
126 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
127 return s
127 return s
128 if (ui.configbool('eol', 'fix-trailing-newline', False)
128 if (ui.configbool('eol', 'fix-trailing-newline', False)
129 and s and s[-1] != '\n'):
129 and s and s[-1] != '\n'):
130 s = s + '\n'
130 s = s + '\n'
131 return eolre.sub('\r\n', s)
131 return eolre.sub('\r\n', s)
132
132
133 def isbinary(s, params):
133 def isbinary(s, params):
134 """Filter to do nothing with the file."""
134 """Filter to do nothing with the file."""
135 return s
135 return s
136
136
137 filters = {
137 filters = {
138 'to-lf': tolf,
138 'to-lf': tolf,
139 'to-crlf': tocrlf,
139 'to-crlf': tocrlf,
140 'is-binary': isbinary,
140 'is-binary': isbinary,
141 # The following provide backwards compatibility with win32text
141 # The following provide backwards compatibility with win32text
142 'cleverencode:': tolf,
142 'cleverencode:': tolf,
143 'cleverdecode:': tocrlf
143 'cleverdecode:': tocrlf
144 }
144 }
145
145
146 class eolfile(object):
146 class eolfile(object):
147 def __init__(self, ui, root, data):
147 def __init__(self, ui, root, data):
148 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
148 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
149 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
149 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
150
150
151 self.cfg = config.config()
151 self.cfg = config.config()
152 # Our files should not be touched. The pattern must be
152 # Our files should not be touched. The pattern must be
153 # inserted first override a '** = native' pattern.
153 # inserted first override a '** = native' pattern.
154 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
154 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
155 # We can then parse the user's patterns.
155 # We can then parse the user's patterns.
156 self.cfg.parse('.hgeol', data)
156 self.cfg.parse('.hgeol', data)
157
157
158 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
158 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
159 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
159 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
160 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
160 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
161 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
161 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
162
162
163 include = []
163 include = []
164 exclude = []
164 exclude = []
165 for pattern, style in self.cfg.items('patterns'):
165 for pattern, style in self.cfg.items('patterns'):
166 key = style.upper()
166 key = style.upper()
167 if key == 'BIN':
167 if key == 'BIN':
168 exclude.append(pattern)
168 exclude.append(pattern)
169 else:
169 else:
170 include.append(pattern)
170 include.append(pattern)
171 # This will match the files for which we need to care
171 # This will match the files for which we need to care
172 # about inconsistent newlines.
172 # about inconsistent newlines.
173 self.match = match.match(root, '', [], include, exclude)
173 self.match = match.match(root, '', [], include, exclude)
174
174
175 def copytoui(self, ui):
175 def copytoui(self, ui):
176 for pattern, style in self.cfg.items('patterns'):
176 for pattern, style in self.cfg.items('patterns'):
177 key = style.upper()
177 key = style.upper()
178 try:
178 try:
179 ui.setconfig('decode', pattern, self._decode[key], 'eol')
179 ui.setconfig('decode', pattern, self._decode[key], 'eol')
180 ui.setconfig('encode', pattern, self._encode[key], 'eol')
180 ui.setconfig('encode', pattern, self._encode[key], 'eol')
181 except KeyError:
181 except KeyError:
182 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
182 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
183 % (style, self.cfg.source('patterns', pattern)))
183 % (style, self.cfg.source('patterns', pattern)))
184 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
184 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
185 for k, v in self.cfg.items('eol'):
185 for k, v in self.cfg.items('eol'):
186 ui.setconfig('eol', k, v, 'eol')
186 ui.setconfig('eol', k, v, 'eol')
187
187
188 def checkrev(self, repo, ctx, files):
188 def checkrev(self, repo, ctx, files):
189 failed = []
189 failed = []
190 for f in (files or ctx.files()):
190 for f in (files or ctx.files()):
191 if f not in ctx:
191 if f not in ctx:
192 continue
192 continue
193 for pattern, style in self.cfg.items('patterns'):
193 for pattern, style in self.cfg.items('patterns'):
194 if not match.match(repo.root, '', [pattern])(f):
194 if not match.match(repo.root, '', [pattern])(f):
195 continue
195 continue
196 target = self._encode[style.upper()]
196 target = self._encode[style.upper()]
197 data = ctx[f].data()
197 data = ctx[f].data()
198 if (target == "to-lf" and "\r\n" in data
198 if (target == "to-lf" and "\r\n" in data
199 or target == "to-crlf" and singlelf.search(data)):
199 or target == "to-crlf" and singlelf.search(data)):
200 failed.append((str(ctx), target, f))
200 failed.append((str(ctx), target, f))
201 break
201 break
202 return failed
202 return failed
203
203
204 def parseeol(ui, repo, nodes):
204 def parseeol(ui, repo, nodes):
205 try:
205 try:
206 for node in nodes:
206 for node in nodes:
207 try:
207 try:
208 if node is None:
208 if node is None:
209 # Cannot use workingctx.data() since it would load
209 # Cannot use workingctx.data() since it would load
210 # and cache the filters before we configure them.
210 # and cache the filters before we configure them.
211 data = repo.wfile('.hgeol').read()
211 data = repo.wfile('.hgeol').read()
212 else:
212 else:
213 data = repo[node]['.hgeol'].data()
213 data = repo[node]['.hgeol'].data()
214 return eolfile(ui, repo.root, data)
214 return eolfile(ui, repo.root, data)
215 except (IOError, LookupError):
215 except (IOError, LookupError):
216 pass
216 pass
217 except error.ParseError, inst:
217 except error.ParseError, inst:
218 ui.warn(_("warning: ignoring .hgeol file due to parse error "
218 ui.warn(_("warning: ignoring .hgeol file due to parse error "
219 "at %s: %s\n") % (inst.args[1], inst.args[0]))
219 "at %s: %s\n") % (inst.args[1], inst.args[0]))
220 return None
220 return None
221
221
222 def _checkhook(ui, repo, node, headsonly):
222 def _checkhook(ui, repo, node, headsonly):
223 # Get revisions to check and touched files at the same time
223 # Get revisions to check and touched files at the same time
224 files = set()
224 files = set()
225 revs = set()
225 revs = set()
226 for rev in xrange(repo[node].rev(), len(repo)):
226 for rev in xrange(repo[node].rev(), len(repo)):
227 revs.add(rev)
227 revs.add(rev)
228 if headsonly:
228 if headsonly:
229 ctx = repo[rev]
229 ctx = repo[rev]
230 files.update(ctx.files())
230 files.update(ctx.files())
231 for pctx in ctx.parents():
231 for pctx in ctx.parents():
232 revs.discard(pctx.rev())
232 revs.discard(pctx.rev())
233 failed = []
233 failed = []
234 for rev in revs:
234 for rev in revs:
235 ctx = repo[rev]
235 ctx = repo[rev]
236 eol = parseeol(ui, repo, [ctx.node()])
236 eol = parseeol(ui, repo, [ctx.node()])
237 if eol:
237 if eol:
238 failed.extend(eol.checkrev(repo, ctx, files))
238 failed.extend(eol.checkrev(repo, ctx, files))
239
239
240 if failed:
240 if failed:
241 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
241 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
242 msgs = []
242 msgs = []
243 for node, target, f in failed:
243 for node, target, f in failed:
244 msgs.append(_(" %s in %s should not have %s line endings") %
244 msgs.append(_(" %s in %s should not have %s line endings") %
245 (f, node, eols[target]))
245 (f, node, eols[target]))
246 raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
246 raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
247
247
248 def checkallhook(ui, repo, node, hooktype, **kwargs):
248 def checkallhook(ui, repo, node, hooktype, **kwargs):
249 """verify that files have expected EOLs"""
249 """verify that files have expected EOLs"""
250 _checkhook(ui, repo, node, False)
250 _checkhook(ui, repo, node, False)
251
251
252 def checkheadshook(ui, repo, node, hooktype, **kwargs):
252 def checkheadshook(ui, repo, node, hooktype, **kwargs):
253 """verify that files have expected EOLs"""
253 """verify that files have expected EOLs"""
254 _checkhook(ui, repo, node, True)
254 _checkhook(ui, repo, node, True)
255
255
256 # "checkheadshook" used to be called "hook"
256 # "checkheadshook" used to be called "hook"
257 hook = checkheadshook
257 hook = checkheadshook
258
258
259 def preupdate(ui, repo, hooktype, parent1, parent2):
259 def preupdate(ui, repo, hooktype, parent1, parent2):
260 repo.loadeol([parent1])
260 repo.loadeol([parent1])
261 return False
261 return False
262
262
263 def uisetup(ui):
263 def uisetup(ui):
264 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
264 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
265
265
266 def extsetup(ui):
266 def extsetup(ui):
267 try:
267 try:
268 extensions.find('win32text')
268 extensions.find('win32text')
269 ui.warn(_("the eol extension is incompatible with the "
269 ui.warn(_("the eol extension is incompatible with the "
270 "win32text extension\n"))
270 "win32text extension\n"))
271 except KeyError:
271 except KeyError:
272 pass
272 pass
273
273
274
274
275 def reposetup(ui, repo):
275 def reposetup(ui, repo):
276 uisetup(repo.ui)
276 uisetup(repo.ui)
277
277
278 if not repo.local():
278 if not repo.local():
279 return
279 return
280 for name, fn in filters.iteritems():
280 for name, fn in filters.iteritems():
281 repo.adddatafilter(name, fn)
281 repo.adddatafilter(name, fn)
282
282
283 ui.setconfig('patch', 'eol', 'auto', 'eol')
283 ui.setconfig('patch', 'eol', 'auto', 'eol')
284
284
285 class eolrepo(repo.__class__):
285 class eolrepo(repo.__class__):
286
286
287 def loadeol(self, nodes):
287 def loadeol(self, nodes):
288 eol = parseeol(self.ui, self, nodes)
288 eol = parseeol(self.ui, self, nodes)
289 if eol is None:
289 if eol is None:
290 return None
290 return None
291 eol.copytoui(self.ui)
291 eol.copytoui(self.ui)
292 return eol.match
292 return eol.match
293
293
294 def _hgcleardirstate(self):
294 def _hgcleardirstate(self):
295 self._eolfile = self.loadeol([None, 'tip'])
295 self._eolfile = self.loadeol([None, 'tip'])
296 if not self._eolfile:
296 if not self._eolfile:
297 self._eolfile = util.never
297 self._eolfile = util.never
298 return
298 return
299
299
300 try:
300 try:
301 cachemtime = os.path.getmtime(self.join("eol.cache"))
301 cachemtime = os.path.getmtime(self.join("eol.cache"))
302 except OSError:
302 except OSError:
303 cachemtime = 0
303 cachemtime = 0
304
304
305 try:
305 try:
306 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
306 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
307 except OSError:
307 except OSError:
308 eolmtime = 0
308 eolmtime = 0
309
309
310 if eolmtime > cachemtime:
310 if eolmtime > cachemtime:
311 self.ui.debug("eol: detected change in .hgeol\n")
311 self.ui.debug("eol: detected change in .hgeol\n")
312 wlock = None
312 wlock = None
313 try:
313 try:
314 wlock = self.wlock()
314 wlock = self.wlock()
315 for f in self.dirstate:
315 for f in self.dirstate:
316 if self.dirstate[f] == 'n':
316 if self.dirstate[f] == 'n':
317 # all normal files need to be looked at
317 # all normal files need to be looked at
318 # again since the new .hgeol file might no
318 # again since the new .hgeol file might no
319 # longer match a file it matched before
319 # longer match a file it matched before
320 self.dirstate.normallookup(f)
320 self.dirstate.normallookup(f)
321 # Create or touch the cache to update mtime
321 # Create or touch the cache to update mtime
322 self.opener("eol.cache", "w").close()
322 self.opener("eol.cache", "w").close()
323 wlock.release()
323 wlock.release()
324 except error.LockUnavailable:
324 except error.LockUnavailable:
325 # If we cannot lock the repository and clear the
325 # If we cannot lock the repository and clear the
326 # dirstate, then a commit might not see all files
326 # dirstate, then a commit might not see all files
327 # as modified. But if we cannot lock the
327 # as modified. But if we cannot lock the
328 # repository, then we can also not make a commit,
328 # repository, then we can also not make a commit,
329 # so ignore the error.
329 # so ignore the error.
330 pass
330 pass
331
331
332 def commitctx(self, ctx, error=False):
332 def commitctx(self, ctx, error=False):
333 for f in sorted(ctx.added() + ctx.modified()):
333 for f in sorted(ctx.added() + ctx.modified()):
334 if not self._eolfile(f):
334 if not self._eolfile(f):
335 continue
335 continue
336 try:
336 try:
337 data = ctx[f].data()
337 data = ctx[f].data()
338 except IOError:
338 except IOError:
339 continue
339 continue
340 if util.binary(data):
340 if util.binary(data):
341 # We should not abort here, since the user should
341 # We should not abort here, since the user should
342 # be able to say "** = native" to automatically
342 # be able to say "** = native" to automatically
343 # have all non-binary files taken care of.
343 # have all non-binary files taken care of.
344 continue
344 continue
345 if inconsistenteol(data):
345 if inconsistenteol(data):
346 raise util.Abort(_("inconsistent newline style "
346 raise util.Abort(_("inconsistent newline style "
347 "in %s\n" % f))
347 "in %s\n") % f)
348 return super(eolrepo, self).commitctx(ctx, error)
348 return super(eolrepo, self).commitctx(ctx, error)
349 repo.__class__ = eolrepo
349 repo.__class__ = eolrepo
350 repo._hgcleardirstate()
350 repo._hgcleardirstate()
@@ -1,582 +1,582 b''
1 # Copyright 2009-2010 Gregory P. Ward
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
4 # Copyright 2010-2011 Unity Technologies
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''High-level command function for lfconvert, plus the cmdtable.'''
9 '''High-level command function for lfconvert, plus the cmdtable.'''
10
10
11 import os, errno
11 import os, errno
12 import shutil
12 import shutil
13
13
14 from mercurial import util, match as match_, hg, node, context, error, \
14 from mercurial import util, match as match_, hg, node, context, error, \
15 cmdutil, scmutil, commands
15 cmdutil, scmutil, commands
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.lock import release
17 from mercurial.lock import release
18
18
19 import lfutil
19 import lfutil
20 import basestore
20 import basestore
21
21
22 # -- Commands ----------------------------------------------------------
22 # -- Commands ----------------------------------------------------------
23
23
24 def lfconvert(ui, src, dest, *pats, **opts):
24 def lfconvert(ui, src, dest, *pats, **opts):
25 '''convert a normal repository to a largefiles repository
25 '''convert a normal repository to a largefiles repository
26
26
27 Convert repository SOURCE to a new repository DEST, identical to
27 Convert repository SOURCE to a new repository DEST, identical to
28 SOURCE except that certain files will be converted as largefiles:
28 SOURCE except that certain files will be converted as largefiles:
29 specifically, any file that matches any PATTERN *or* whose size is
29 specifically, any file that matches any PATTERN *or* whose size is
30 above the minimum size threshold is converted as a largefile. The
30 above the minimum size threshold is converted as a largefile. The
31 size used to determine whether or not to track a file as a
31 size used to determine whether or not to track a file as a
32 largefile is the size of the first version of the file. The
32 largefile is the size of the first version of the file. The
33 minimum size can be specified either with --size or in
33 minimum size can be specified either with --size or in
34 configuration as ``largefiles.size``.
34 configuration as ``largefiles.size``.
35
35
36 After running this command you will need to make sure that
36 After running this command you will need to make sure that
37 largefiles is enabled anywhere you intend to push the new
37 largefiles is enabled anywhere you intend to push the new
38 repository.
38 repository.
39
39
40 Use --to-normal to convert largefiles back to normal files; after
40 Use --to-normal to convert largefiles back to normal files; after
41 this, the DEST repository can be used without largefiles at all.'''
41 this, the DEST repository can be used without largefiles at all.'''
42
42
43 if opts['to_normal']:
43 if opts['to_normal']:
44 tolfile = False
44 tolfile = False
45 else:
45 else:
46 tolfile = True
46 tolfile = True
47 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
47 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
48
48
49 if not hg.islocal(src):
49 if not hg.islocal(src):
50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
51 if not hg.islocal(dest):
51 if not hg.islocal(dest):
52 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
52 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
53
53
54 rsrc = hg.repository(ui, src)
54 rsrc = hg.repository(ui, src)
55 ui.status(_('initializing destination %s\n') % dest)
55 ui.status(_('initializing destination %s\n') % dest)
56 rdst = hg.repository(ui, dest, create=True)
56 rdst = hg.repository(ui, dest, create=True)
57
57
58 success = False
58 success = False
59 dstwlock = dstlock = None
59 dstwlock = dstlock = None
60 try:
60 try:
61 # Lock destination to prevent modification while it is converted to.
61 # Lock destination to prevent modification while it is converted to.
62 # Don't need to lock src because we are just reading from its history
62 # Don't need to lock src because we are just reading from its history
63 # which can't change.
63 # which can't change.
64 dstwlock = rdst.wlock()
64 dstwlock = rdst.wlock()
65 dstlock = rdst.lock()
65 dstlock = rdst.lock()
66
66
67 # Get a list of all changesets in the source. The easy way to do this
67 # Get a list of all changesets in the source. The easy way to do this
68 # is to simply walk the changelog, using changelog.nodesbetween().
68 # is to simply walk the changelog, using changelog.nodesbetween().
69 # Take a look at mercurial/revlog.py:639 for more details.
69 # Take a look at mercurial/revlog.py:639 for more details.
70 # Use a generator instead of a list to decrease memory usage
70 # Use a generator instead of a list to decrease memory usage
71 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
71 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
72 rsrc.heads())[0])
72 rsrc.heads())[0])
73 revmap = {node.nullid: node.nullid}
73 revmap = {node.nullid: node.nullid}
74 if tolfile:
74 if tolfile:
75 lfiles = set()
75 lfiles = set()
76 normalfiles = set()
76 normalfiles = set()
77 if not pats:
77 if not pats:
78 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
78 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
79 if pats:
79 if pats:
80 matcher = match_.match(rsrc.root, '', list(pats))
80 matcher = match_.match(rsrc.root, '', list(pats))
81 else:
81 else:
82 matcher = None
82 matcher = None
83
83
84 lfiletohash = {}
84 lfiletohash = {}
85 for ctx in ctxs:
85 for ctx in ctxs:
86 ui.progress(_('converting revisions'), ctx.rev(),
86 ui.progress(_('converting revisions'), ctx.rev(),
87 unit=_('revision'), total=rsrc['tip'].rev())
87 unit=_('revision'), total=rsrc['tip'].rev())
88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
89 lfiles, normalfiles, matcher, size, lfiletohash)
89 lfiles, normalfiles, matcher, size, lfiletohash)
90 ui.progress(_('converting revisions'), None)
90 ui.progress(_('converting revisions'), None)
91
91
92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
94
94
95 for f in lfiletohash.keys():
95 for f in lfiletohash.keys():
96 if os.path.isfile(rdst.wjoin(f)):
96 if os.path.isfile(rdst.wjoin(f)):
97 os.unlink(rdst.wjoin(f))
97 os.unlink(rdst.wjoin(f))
98 try:
98 try:
99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
100 except OSError:
100 except OSError:
101 pass
101 pass
102
102
103 # If there were any files converted to largefiles, add largefiles
103 # If there were any files converted to largefiles, add largefiles
104 # to the destination repository's requirements.
104 # to the destination repository's requirements.
105 if lfiles:
105 if lfiles:
106 rdst.requirements.add('largefiles')
106 rdst.requirements.add('largefiles')
107 rdst._writerequirements()
107 rdst._writerequirements()
108 else:
108 else:
109 for ctx in ctxs:
109 for ctx in ctxs:
110 ui.progress(_('converting revisions'), ctx.rev(),
110 ui.progress(_('converting revisions'), ctx.rev(),
111 unit=_('revision'), total=rsrc['tip'].rev())
111 unit=_('revision'), total=rsrc['tip'].rev())
112 _addchangeset(ui, rsrc, rdst, ctx, revmap)
112 _addchangeset(ui, rsrc, rdst, ctx, revmap)
113
113
114 ui.progress(_('converting revisions'), None)
114 ui.progress(_('converting revisions'), None)
115 success = True
115 success = True
116 finally:
116 finally:
117 rdst.dirstate.clear()
117 rdst.dirstate.clear()
118 release(dstlock, dstwlock)
118 release(dstlock, dstwlock)
119 if not success:
119 if not success:
120 # we failed, remove the new directory
120 # we failed, remove the new directory
121 shutil.rmtree(rdst.root)
121 shutil.rmtree(rdst.root)
122
122
123 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
123 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
124 # Convert src parents to dst parents
124 # Convert src parents to dst parents
125 parents = _convertparents(ctx, revmap)
125 parents = _convertparents(ctx, revmap)
126
126
127 # Generate list of changed files
127 # Generate list of changed files
128 files = _getchangedfiles(ctx, parents)
128 files = _getchangedfiles(ctx, parents)
129
129
130 def getfilectx(repo, memctx, f):
130 def getfilectx(repo, memctx, f):
131 if lfutil.standin(f) in files:
131 if lfutil.standin(f) in files:
132 # if the file isn't in the manifest then it was removed
132 # if the file isn't in the manifest then it was removed
133 # or renamed, raise IOError to indicate this
133 # or renamed, raise IOError to indicate this
134 try:
134 try:
135 fctx = ctx.filectx(lfutil.standin(f))
135 fctx = ctx.filectx(lfutil.standin(f))
136 except error.LookupError:
136 except error.LookupError:
137 raise IOError
137 raise IOError
138 renamed = fctx.renamed()
138 renamed = fctx.renamed()
139 if renamed:
139 if renamed:
140 renamed = lfutil.splitstandin(renamed[0])
140 renamed = lfutil.splitstandin(renamed[0])
141
141
142 hash = fctx.data().strip()
142 hash = fctx.data().strip()
143 path = lfutil.findfile(rsrc, hash)
143 path = lfutil.findfile(rsrc, hash)
144
144
145 # If one file is missing, likely all files from this rev are
145 # If one file is missing, likely all files from this rev are
146 if path is None:
146 if path is None:
147 cachelfiles(ui, rsrc, ctx.node())
147 cachelfiles(ui, rsrc, ctx.node())
148 path = lfutil.findfile(rsrc, hash)
148 path = lfutil.findfile(rsrc, hash)
149
149
150 if path is None:
150 if path is None:
151 raise util.Abort(
151 raise util.Abort(
152 _("missing largefile \'%s\' from revision %s")
152 _("missing largefile \'%s\' from revision %s")
153 % (f, node.hex(ctx.node())))
153 % (f, node.hex(ctx.node())))
154
154
155 data = ''
155 data = ''
156 fd = None
156 fd = None
157 try:
157 try:
158 fd = open(path, 'rb')
158 fd = open(path, 'rb')
159 data = fd.read()
159 data = fd.read()
160 finally:
160 finally:
161 if fd:
161 if fd:
162 fd.close()
162 fd.close()
163 return context.memfilectx(f, data, 'l' in fctx.flags(),
163 return context.memfilectx(f, data, 'l' in fctx.flags(),
164 'x' in fctx.flags(), renamed)
164 'x' in fctx.flags(), renamed)
165 else:
165 else:
166 return _getnormalcontext(repo.ui, ctx, f, revmap)
166 return _getnormalcontext(repo.ui, ctx, f, revmap)
167
167
168 dstfiles = []
168 dstfiles = []
169 for file in files:
169 for file in files:
170 if lfutil.isstandin(file):
170 if lfutil.isstandin(file):
171 dstfiles.append(lfutil.splitstandin(file))
171 dstfiles.append(lfutil.splitstandin(file))
172 else:
172 else:
173 dstfiles.append(file)
173 dstfiles.append(file)
174 # Commit
174 # Commit
175 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
175 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
176
176
177 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
177 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
178 matcher, size, lfiletohash):
178 matcher, size, lfiletohash):
179 # Convert src parents to dst parents
179 # Convert src parents to dst parents
180 parents = _convertparents(ctx, revmap)
180 parents = _convertparents(ctx, revmap)
181
181
182 # Generate list of changed files
182 # Generate list of changed files
183 files = _getchangedfiles(ctx, parents)
183 files = _getchangedfiles(ctx, parents)
184
184
185 dstfiles = []
185 dstfiles = []
186 for f in files:
186 for f in files:
187 if f not in lfiles and f not in normalfiles:
187 if f not in lfiles and f not in normalfiles:
188 islfile = _islfile(f, ctx, matcher, size)
188 islfile = _islfile(f, ctx, matcher, size)
189 # If this file was renamed or copied then copy
189 # If this file was renamed or copied then copy
190 # the largefile-ness of its predecessor
190 # the largefile-ness of its predecessor
191 if f in ctx.manifest():
191 if f in ctx.manifest():
192 fctx = ctx.filectx(f)
192 fctx = ctx.filectx(f)
193 renamed = fctx.renamed()
193 renamed = fctx.renamed()
194 renamedlfile = renamed and renamed[0] in lfiles
194 renamedlfile = renamed and renamed[0] in lfiles
195 islfile |= renamedlfile
195 islfile |= renamedlfile
196 if 'l' in fctx.flags():
196 if 'l' in fctx.flags():
197 if renamedlfile:
197 if renamedlfile:
198 raise util.Abort(
198 raise util.Abort(
199 _('renamed/copied largefile %s becomes symlink')
199 _('renamed/copied largefile %s becomes symlink')
200 % f)
200 % f)
201 islfile = False
201 islfile = False
202 if islfile:
202 if islfile:
203 lfiles.add(f)
203 lfiles.add(f)
204 else:
204 else:
205 normalfiles.add(f)
205 normalfiles.add(f)
206
206
207 if f in lfiles:
207 if f in lfiles:
208 dstfiles.append(lfutil.standin(f))
208 dstfiles.append(lfutil.standin(f))
209 # largefile in manifest if it has not been removed/renamed
209 # largefile in manifest if it has not been removed/renamed
210 if f in ctx.manifest():
210 if f in ctx.manifest():
211 fctx = ctx.filectx(f)
211 fctx = ctx.filectx(f)
212 if 'l' in fctx.flags():
212 if 'l' in fctx.flags():
213 renamed = fctx.renamed()
213 renamed = fctx.renamed()
214 if renamed and renamed[0] in lfiles:
214 if renamed and renamed[0] in lfiles:
215 raise util.Abort(_('largefile %s becomes symlink') % f)
215 raise util.Abort(_('largefile %s becomes symlink') % f)
216
216
217 # largefile was modified, update standins
217 # largefile was modified, update standins
218 m = util.sha1('')
218 m = util.sha1('')
219 m.update(ctx[f].data())
219 m.update(ctx[f].data())
220 hash = m.hexdigest()
220 hash = m.hexdigest()
221 if f not in lfiletohash or lfiletohash[f] != hash:
221 if f not in lfiletohash or lfiletohash[f] != hash:
222 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
222 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
223 executable = 'x' in ctx[f].flags()
223 executable = 'x' in ctx[f].flags()
224 lfutil.writestandin(rdst, lfutil.standin(f), hash,
224 lfutil.writestandin(rdst, lfutil.standin(f), hash,
225 executable)
225 executable)
226 lfiletohash[f] = hash
226 lfiletohash[f] = hash
227 else:
227 else:
228 # normal file
228 # normal file
229 dstfiles.append(f)
229 dstfiles.append(f)
230
230
231 def getfilectx(repo, memctx, f):
231 def getfilectx(repo, memctx, f):
232 if lfutil.isstandin(f):
232 if lfutil.isstandin(f):
233 # if the file isn't in the manifest then it was removed
233 # if the file isn't in the manifest then it was removed
234 # or renamed, raise IOError to indicate this
234 # or renamed, raise IOError to indicate this
235 srcfname = lfutil.splitstandin(f)
235 srcfname = lfutil.splitstandin(f)
236 try:
236 try:
237 fctx = ctx.filectx(srcfname)
237 fctx = ctx.filectx(srcfname)
238 except error.LookupError:
238 except error.LookupError:
239 raise IOError
239 raise IOError
240 renamed = fctx.renamed()
240 renamed = fctx.renamed()
241 if renamed:
241 if renamed:
242 # standin is always a largefile because largefile-ness
242 # standin is always a largefile because largefile-ness
243 # doesn't change after rename or copy
243 # doesn't change after rename or copy
244 renamed = lfutil.standin(renamed[0])
244 renamed = lfutil.standin(renamed[0])
245
245
246 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
246 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
247 fctx.flags(), 'x' in fctx.flags(), renamed)
247 fctx.flags(), 'x' in fctx.flags(), renamed)
248 else:
248 else:
249 return _getnormalcontext(repo.ui, ctx, f, revmap)
249 return _getnormalcontext(repo.ui, ctx, f, revmap)
250
250
251 # Commit
251 # Commit
252 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
252 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
253
253
254 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
254 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
255 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
255 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
256 getfilectx, ctx.user(), ctx.date(), ctx.extra())
256 getfilectx, ctx.user(), ctx.date(), ctx.extra())
257 ret = rdst.commitctx(mctx)
257 ret = rdst.commitctx(mctx)
258 rdst.setparents(ret)
258 rdst.setparents(ret)
259 revmap[ctx.node()] = rdst.changelog.tip()
259 revmap[ctx.node()] = rdst.changelog.tip()
260
260
261 # Generate list of changed files
261 # Generate list of changed files
262 def _getchangedfiles(ctx, parents):
262 def _getchangedfiles(ctx, parents):
263 files = set(ctx.files())
263 files = set(ctx.files())
264 if node.nullid not in parents:
264 if node.nullid not in parents:
265 mc = ctx.manifest()
265 mc = ctx.manifest()
266 mp1 = ctx.parents()[0].manifest()
266 mp1 = ctx.parents()[0].manifest()
267 mp2 = ctx.parents()[1].manifest()
267 mp2 = ctx.parents()[1].manifest()
268 files |= (set(mp1) | set(mp2)) - set(mc)
268 files |= (set(mp1) | set(mp2)) - set(mc)
269 for f in mc:
269 for f in mc:
270 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
270 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
271 files.add(f)
271 files.add(f)
272 return files
272 return files
273
273
274 # Convert src parents to dst parents
274 # Convert src parents to dst parents
275 def _convertparents(ctx, revmap):
275 def _convertparents(ctx, revmap):
276 parents = []
276 parents = []
277 for p in ctx.parents():
277 for p in ctx.parents():
278 parents.append(revmap[p.node()])
278 parents.append(revmap[p.node()])
279 while len(parents) < 2:
279 while len(parents) < 2:
280 parents.append(node.nullid)
280 parents.append(node.nullid)
281 return parents
281 return parents
282
282
283 # Get memfilectx for a normal file
283 # Get memfilectx for a normal file
284 def _getnormalcontext(ui, ctx, f, revmap):
284 def _getnormalcontext(ui, ctx, f, revmap):
285 try:
285 try:
286 fctx = ctx.filectx(f)
286 fctx = ctx.filectx(f)
287 except error.LookupError:
287 except error.LookupError:
288 raise IOError
288 raise IOError
289 renamed = fctx.renamed()
289 renamed = fctx.renamed()
290 if renamed:
290 if renamed:
291 renamed = renamed[0]
291 renamed = renamed[0]
292
292
293 data = fctx.data()
293 data = fctx.data()
294 if f == '.hgtags':
294 if f == '.hgtags':
295 data = _converttags (ui, revmap, data)
295 data = _converttags (ui, revmap, data)
296 return context.memfilectx(f, data, 'l' in fctx.flags(),
296 return context.memfilectx(f, data, 'l' in fctx.flags(),
297 'x' in fctx.flags(), renamed)
297 'x' in fctx.flags(), renamed)
298
298
299 # Remap tag data using a revision map
299 # Remap tag data using a revision map
300 def _converttags(ui, revmap, data):
300 def _converttags(ui, revmap, data):
301 newdata = []
301 newdata = []
302 for line in data.splitlines():
302 for line in data.splitlines():
303 try:
303 try:
304 id, name = line.split(' ', 1)
304 id, name = line.split(' ', 1)
305 except ValueError:
305 except ValueError:
306 ui.warn(_('skipping incorrectly formatted tag %s\n'
306 ui.warn(_('skipping incorrectly formatted tag %s\n')
307 % line))
307 % line)
308 continue
308 continue
309 try:
309 try:
310 newid = node.bin(id)
310 newid = node.bin(id)
311 except TypeError:
311 except TypeError:
312 ui.warn(_('skipping incorrectly formatted id %s\n'
312 ui.warn(_('skipping incorrectly formatted id %s\n')
313 % id))
313 % id)
314 continue
314 continue
315 try:
315 try:
316 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
316 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
317 name))
317 name))
318 except KeyError:
318 except KeyError:
319 ui.warn(_('no mapping for id %s\n') % id)
319 ui.warn(_('no mapping for id %s\n') % id)
320 continue
320 continue
321 return ''.join(newdata)
321 return ''.join(newdata)
322
322
323 def _islfile(file, ctx, matcher, size):
323 def _islfile(file, ctx, matcher, size):
324 '''Return true if file should be considered a largefile, i.e.
324 '''Return true if file should be considered a largefile, i.e.
325 matcher matches it or it is larger than size.'''
325 matcher matches it or it is larger than size.'''
326 # never store special .hg* files as largefiles
326 # never store special .hg* files as largefiles
327 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
327 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
328 return False
328 return False
329 if matcher and matcher(file):
329 if matcher and matcher(file):
330 return True
330 return True
331 try:
331 try:
332 return ctx.filectx(file).size() >= size * 1024 * 1024
332 return ctx.filectx(file).size() >= size * 1024 * 1024
333 except error.LookupError:
333 except error.LookupError:
334 return False
334 return False
335
335
336 def uploadlfiles(ui, rsrc, rdst, files):
336 def uploadlfiles(ui, rsrc, rdst, files):
337 '''upload largefiles to the central store'''
337 '''upload largefiles to the central store'''
338
338
339 if not files:
339 if not files:
340 return
340 return
341
341
342 store = basestore._openstore(rsrc, rdst, put=True)
342 store = basestore._openstore(rsrc, rdst, put=True)
343
343
344 at = 0
344 at = 0
345 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
345 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
346 retval = store.exists(files)
346 retval = store.exists(files)
347 files = filter(lambda h: not retval[h], files)
347 files = filter(lambda h: not retval[h], files)
348 ui.debug("%d largefiles need to be uploaded\n" % len(files))
348 ui.debug("%d largefiles need to be uploaded\n" % len(files))
349
349
350 for hash in files:
350 for hash in files:
351 ui.progress(_('uploading largefiles'), at, unit='largefile',
351 ui.progress(_('uploading largefiles'), at, unit='largefile',
352 total=len(files))
352 total=len(files))
353 source = lfutil.findfile(rsrc, hash)
353 source = lfutil.findfile(rsrc, hash)
354 if not source:
354 if not source:
355 raise util.Abort(_('largefile %s missing from store'
355 raise util.Abort(_('largefile %s missing from store'
356 ' (needs to be uploaded)') % hash)
356 ' (needs to be uploaded)') % hash)
357 # XXX check for errors here
357 # XXX check for errors here
358 store.put(source, hash)
358 store.put(source, hash)
359 at += 1
359 at += 1
360 ui.progress(_('uploading largefiles'), None)
360 ui.progress(_('uploading largefiles'), None)
361
361
362 def verifylfiles(ui, repo, all=False, contents=False):
362 def verifylfiles(ui, repo, all=False, contents=False):
363 '''Verify that every largefile revision in the current changeset
363 '''Verify that every largefile revision in the current changeset
364 exists in the central store. With --contents, also verify that
364 exists in the central store. With --contents, also verify that
365 the contents of each local largefile file revision are correct (SHA-1 hash
365 the contents of each local largefile file revision are correct (SHA-1 hash
366 matches the revision ID). With --all, check every changeset in
366 matches the revision ID). With --all, check every changeset in
367 this repository.'''
367 this repository.'''
368 if all:
368 if all:
369 # Pass a list to the function rather than an iterator because we know a
369 # Pass a list to the function rather than an iterator because we know a
370 # list will work.
370 # list will work.
371 revs = range(len(repo))
371 revs = range(len(repo))
372 else:
372 else:
373 revs = ['.']
373 revs = ['.']
374
374
375 store = basestore._openstore(repo)
375 store = basestore._openstore(repo)
376 return store.verify(revs, contents=contents)
376 return store.verify(revs, contents=contents)
377
377
378 def debugdirstate(ui, repo):
378 def debugdirstate(ui, repo):
379 '''Show basic information for the largefiles dirstate'''
379 '''Show basic information for the largefiles dirstate'''
380 lfdirstate = lfutil.openlfdirstate(ui, repo)
380 lfdirstate = lfutil.openlfdirstate(ui, repo)
381 for file_, ent in sorted(lfdirstate._map.iteritems()):
381 for file_, ent in sorted(lfdirstate._map.iteritems()):
382 mode = '%3o' % (ent[1] & 0777 & ~util.umask)
382 mode = '%3o' % (ent[1] & 0777 & ~util.umask)
383 ui.write("%c %s %10d %s\n" % (ent[0], mode, ent[2], file_))
383 ui.write("%c %s %10d %s\n" % (ent[0], mode, ent[2], file_))
384
384
385 def cachelfiles(ui, repo, node, filelist=None):
385 def cachelfiles(ui, repo, node, filelist=None):
386 '''cachelfiles ensures that all largefiles needed by the specified revision
386 '''cachelfiles ensures that all largefiles needed by the specified revision
387 are present in the repository's largefile cache.
387 are present in the repository's largefile cache.
388
388
389 returns a tuple (cached, missing). cached is the list of files downloaded
389 returns a tuple (cached, missing). cached is the list of files downloaded
390 by this operation; missing is the list of files that were needed but could
390 by this operation; missing is the list of files that were needed but could
391 not be found.'''
391 not be found.'''
392 lfiles = lfutil.listlfiles(repo, node)
392 lfiles = lfutil.listlfiles(repo, node)
393 if filelist:
393 if filelist:
394 lfiles = set(lfiles) & set(filelist)
394 lfiles = set(lfiles) & set(filelist)
395 toget = []
395 toget = []
396
396
397 for lfile in lfiles:
397 for lfile in lfiles:
398 try:
398 try:
399 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
399 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
400 except IOError, err:
400 except IOError, err:
401 if err.errno == errno.ENOENT:
401 if err.errno == errno.ENOENT:
402 continue # node must be None and standin wasn't found in wctx
402 continue # node must be None and standin wasn't found in wctx
403 raise
403 raise
404 if not lfutil.findfile(repo, expectedhash):
404 if not lfutil.findfile(repo, expectedhash):
405 toget.append((lfile, expectedhash))
405 toget.append((lfile, expectedhash))
406
406
407 if toget:
407 if toget:
408 store = basestore._openstore(repo)
408 store = basestore._openstore(repo)
409 ret = store.get(toget)
409 ret = store.get(toget)
410 return ret
410 return ret
411
411
412 return ([], [])
412 return ([], [])
413
413
414 def downloadlfiles(ui, repo, rev=None):
414 def downloadlfiles(ui, repo, rev=None):
415 matchfn = scmutil.match(repo[None],
415 matchfn = scmutil.match(repo[None],
416 [repo.wjoin(lfutil.shortname)], {})
416 [repo.wjoin(lfutil.shortname)], {})
417 def prepare(ctx, fns):
417 def prepare(ctx, fns):
418 pass
418 pass
419 totalsuccess = 0
419 totalsuccess = 0
420 totalmissing = 0
420 totalmissing = 0
421 if rev != []: # walkchangerevs on empty list would return all revs
421 if rev != []: # walkchangerevs on empty list would return all revs
422 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
422 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
423 prepare):
423 prepare):
424 success, missing = cachelfiles(ui, repo, ctx.node())
424 success, missing = cachelfiles(ui, repo, ctx.node())
425 totalsuccess += len(success)
425 totalsuccess += len(success)
426 totalmissing += len(missing)
426 totalmissing += len(missing)
427 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
427 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
428 if totalmissing > 0:
428 if totalmissing > 0:
429 ui.status(_("%d largefiles failed to download\n") % totalmissing)
429 ui.status(_("%d largefiles failed to download\n") % totalmissing)
430 return totalsuccess, totalmissing
430 return totalsuccess, totalmissing
431
431
432 def updatelfiles(ui, repo, filelist=None, printmessage=True):
432 def updatelfiles(ui, repo, filelist=None, printmessage=True):
433 wlock = repo.wlock()
433 wlock = repo.wlock()
434 try:
434 try:
435 lfdirstate = lfutil.openlfdirstate(ui, repo)
435 lfdirstate = lfutil.openlfdirstate(ui, repo)
436 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
436 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
437
437
438 if filelist is not None:
438 if filelist is not None:
439 lfiles = [f for f in lfiles if f in filelist]
439 lfiles = [f for f in lfiles if f in filelist]
440
440
441 update = {}
441 update = {}
442 updated, removed = 0, 0
442 updated, removed = 0, 0
443 for lfile in lfiles:
443 for lfile in lfiles:
444 abslfile = repo.wjoin(lfile)
444 abslfile = repo.wjoin(lfile)
445 absstandin = repo.wjoin(lfutil.standin(lfile))
445 absstandin = repo.wjoin(lfutil.standin(lfile))
446 if os.path.exists(absstandin):
446 if os.path.exists(absstandin):
447 if (os.path.exists(absstandin + '.orig') and
447 if (os.path.exists(absstandin + '.orig') and
448 os.path.exists(abslfile)):
448 os.path.exists(abslfile)):
449 shutil.copyfile(abslfile, abslfile + '.orig')
449 shutil.copyfile(abslfile, abslfile + '.orig')
450 expecthash = lfutil.readstandin(repo, lfile)
450 expecthash = lfutil.readstandin(repo, lfile)
451 if (expecthash != '' and
451 if (expecthash != '' and
452 (not os.path.exists(abslfile) or
452 (not os.path.exists(abslfile) or
453 expecthash != lfutil.hashfile(abslfile))):
453 expecthash != lfutil.hashfile(abslfile))):
454 if lfile not in repo[None]: # not switched to normal file
454 if lfile not in repo[None]: # not switched to normal file
455 util.unlinkpath(abslfile, ignoremissing=True)
455 util.unlinkpath(abslfile, ignoremissing=True)
456 # use normallookup() to allocate entry in largefiles
456 # use normallookup() to allocate entry in largefiles
457 # dirstate, because lack of it misleads
457 # dirstate, because lack of it misleads
458 # lfilesrepo.status() into recognition that such cache
458 # lfilesrepo.status() into recognition that such cache
459 # missing files are REMOVED.
459 # missing files are REMOVED.
460 lfdirstate.normallookup(lfile)
460 lfdirstate.normallookup(lfile)
461 update[lfile] = expecthash
461 update[lfile] = expecthash
462 else:
462 else:
463 # Remove lfiles for which the standin is deleted, unless the
463 # Remove lfiles for which the standin is deleted, unless the
464 # lfile is added to the repository again. This happens when a
464 # lfile is added to the repository again. This happens when a
465 # largefile is converted back to a normal file: the standin
465 # largefile is converted back to a normal file: the standin
466 # disappears, but a new (normal) file appears as the lfile.
466 # disappears, but a new (normal) file appears as the lfile.
467 if (os.path.exists(abslfile) and
467 if (os.path.exists(abslfile) and
468 repo.dirstate.normalize(lfile) not in repo[None]):
468 repo.dirstate.normalize(lfile) not in repo[None]):
469 util.unlinkpath(abslfile)
469 util.unlinkpath(abslfile)
470 removed += 1
470 removed += 1
471
471
472 # largefile processing might be slow and be interrupted - be prepared
472 # largefile processing might be slow and be interrupted - be prepared
473 lfdirstate.write()
473 lfdirstate.write()
474
474
475 if lfiles:
475 if lfiles:
476 if printmessage:
476 if printmessage:
477 ui.status(_('getting changed largefiles\n'))
477 ui.status(_('getting changed largefiles\n'))
478 cachelfiles(ui, repo, None, lfiles)
478 cachelfiles(ui, repo, None, lfiles)
479
479
480 for lfile in lfiles:
480 for lfile in lfiles:
481 update1 = 0
481 update1 = 0
482
482
483 expecthash = update.get(lfile)
483 expecthash = update.get(lfile)
484 if expecthash:
484 if expecthash:
485 if not lfutil.copyfromcache(repo, expecthash, lfile):
485 if not lfutil.copyfromcache(repo, expecthash, lfile):
486 # failed ... but already removed and set to normallookup
486 # failed ... but already removed and set to normallookup
487 continue
487 continue
488 # Synchronize largefile dirstate to the last modified
488 # Synchronize largefile dirstate to the last modified
489 # time of the file
489 # time of the file
490 lfdirstate.normal(lfile)
490 lfdirstate.normal(lfile)
491 update1 = 1
491 update1 = 1
492
492
493 # copy the state of largefile standin from the repository's
493 # copy the state of largefile standin from the repository's
494 # dirstate to its state in the lfdirstate.
494 # dirstate to its state in the lfdirstate.
495 abslfile = repo.wjoin(lfile)
495 abslfile = repo.wjoin(lfile)
496 absstandin = repo.wjoin(lfutil.standin(lfile))
496 absstandin = repo.wjoin(lfutil.standin(lfile))
497 if os.path.exists(absstandin):
497 if os.path.exists(absstandin):
498 mode = os.stat(absstandin).st_mode
498 mode = os.stat(absstandin).st_mode
499 if mode != os.stat(abslfile).st_mode:
499 if mode != os.stat(abslfile).st_mode:
500 os.chmod(abslfile, mode)
500 os.chmod(abslfile, mode)
501 update1 = 1
501 update1 = 1
502
502
503 updated += update1
503 updated += update1
504
504
505 state = repo.dirstate[lfutil.standin(lfile)]
505 state = repo.dirstate[lfutil.standin(lfile)]
506 if state == 'n':
506 if state == 'n':
507 # When rebasing, we need to synchronize the standin and the
507 # When rebasing, we need to synchronize the standin and the
508 # largefile, because otherwise the largefile will get reverted.
508 # largefile, because otherwise the largefile will get reverted.
509 # But for commit's sake, we have to mark the file as unclean.
509 # But for commit's sake, we have to mark the file as unclean.
510 if getattr(repo, "_isrebasing", False):
510 if getattr(repo, "_isrebasing", False):
511 lfdirstate.normallookup(lfile)
511 lfdirstate.normallookup(lfile)
512 else:
512 else:
513 lfdirstate.normal(lfile)
513 lfdirstate.normal(lfile)
514 elif state == 'r':
514 elif state == 'r':
515 lfdirstate.remove(lfile)
515 lfdirstate.remove(lfile)
516 elif state == 'a':
516 elif state == 'a':
517 lfdirstate.add(lfile)
517 lfdirstate.add(lfile)
518 elif state == '?':
518 elif state == '?':
519 lfdirstate.drop(lfile)
519 lfdirstate.drop(lfile)
520
520
521 lfdirstate.write()
521 lfdirstate.write()
522 if printmessage and lfiles:
522 if printmessage and lfiles:
523 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
523 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
524 removed))
524 removed))
525 finally:
525 finally:
526 wlock.release()
526 wlock.release()
527
527
528 def lfpull(ui, repo, source="default", **opts):
528 def lfpull(ui, repo, source="default", **opts):
529 """pull largefiles for the specified revisions from the specified source
529 """pull largefiles for the specified revisions from the specified source
530
530
531 Pull largefiles that are referenced from local changesets but missing
531 Pull largefiles that are referenced from local changesets but missing
532 locally, pulling from a remote repository to the local cache.
532 locally, pulling from a remote repository to the local cache.
533
533
534 If SOURCE is omitted, the 'default' path will be used.
534 If SOURCE is omitted, the 'default' path will be used.
535 See :hg:`help urls` for more information.
535 See :hg:`help urls` for more information.
536
536
537 .. container:: verbose
537 .. container:: verbose
538
538
539 Some examples:
539 Some examples:
540
540
541 - pull largefiles for all branch heads::
541 - pull largefiles for all branch heads::
542
542
543 hg lfpull -r "head() and not closed()"
543 hg lfpull -r "head() and not closed()"
544
544
545 - pull largefiles on the default branch::
545 - pull largefiles on the default branch::
546
546
547 hg lfpull -r "branch(default)"
547 hg lfpull -r "branch(default)"
548 """
548 """
549 repo.lfpullsource = source
549 repo.lfpullsource = source
550
550
551 revs = opts.get('rev', [])
551 revs = opts.get('rev', [])
552 if not revs:
552 if not revs:
553 raise util.Abort(_('no revisions specified'))
553 raise util.Abort(_('no revisions specified'))
554 revs = scmutil.revrange(repo, revs)
554 revs = scmutil.revrange(repo, revs)
555
555
556 numcached = 0
556 numcached = 0
557 for rev in revs:
557 for rev in revs:
558 ui.note(_('pulling largefiles for revision %s\n') % rev)
558 ui.note(_('pulling largefiles for revision %s\n') % rev)
559 (cached, missing) = cachelfiles(ui, repo, rev)
559 (cached, missing) = cachelfiles(ui, repo, rev)
560 numcached += len(cached)
560 numcached += len(cached)
561 ui.status(_("%d largefiles cached\n") % numcached)
561 ui.status(_("%d largefiles cached\n") % numcached)
562
562
563 # -- hg commands declarations ------------------------------------------------
563 # -- hg commands declarations ------------------------------------------------
564
564
565 cmdtable = {
565 cmdtable = {
566 'lfconvert': (lfconvert,
566 'lfconvert': (lfconvert,
567 [('s', 'size', '',
567 [('s', 'size', '',
568 _('minimum size (MB) for files to be converted '
568 _('minimum size (MB) for files to be converted '
569 'as largefiles'),
569 'as largefiles'),
570 'SIZE'),
570 'SIZE'),
571 ('', 'to-normal', False,
571 ('', 'to-normal', False,
572 _('convert from a largefiles repo to a normal repo')),
572 _('convert from a largefiles repo to a normal repo')),
573 ],
573 ],
574 _('hg lfconvert SOURCE DEST [FILE ...]')),
574 _('hg lfconvert SOURCE DEST [FILE ...]')),
575 'lfpull': (lfpull,
575 'lfpull': (lfpull,
576 [('r', 'rev', [], _('pull largefiles for these revisions'))
576 [('r', 'rev', [], _('pull largefiles for these revisions'))
577 ] + commands.remoteopts,
577 ] + commands.remoteopts,
578 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]')
578 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]')
579 ),
579 ),
580 }
580 }
581
581
582 commands.inferrepo += " lfconvert"
582 commands.inferrepo += " lfconvert"
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
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