##// END OF EJS Templates
check-code: catch "except as"
Matt Mackall -
r13160:07d08c13 default
parent child Browse files
Show More
@@ -1,303 +1,304 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 import re, glob, os, sys
10 import re, glob, os, sys
11 import keyword
11 import keyword
12 import optparse
12 import optparse
13
13
14 def repquote(m):
14 def repquote(m):
15 t = re.sub(r"\w", "x", m.group('text'))
15 t = re.sub(r"\w", "x", m.group('text'))
16 t = re.sub(r"[^\sx]", "o", t)
16 t = re.sub(r"[^\sx]", "o", t)
17 return m.group('quote') + t + m.group('quote')
17 return m.group('quote') + t + m.group('quote')
18
18
19 def reppython(m):
19 def reppython(m):
20 comment = m.group('comment')
20 comment = m.group('comment')
21 if comment:
21 if comment:
22 return "#" * len(comment)
22 return "#" * len(comment)
23 return repquote(m)
23 return repquote(m)
24
24
25 def repcomment(m):
25 def repcomment(m):
26 return m.group(1) + "#" * len(m.group(2))
26 return m.group(1) + "#" * len(m.group(2))
27
27
28 def repccomment(m):
28 def repccomment(m):
29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
30 return m.group(1) + t + "*/"
30 return m.group(1) + t + "*/"
31
31
32 def repcallspaces(m):
32 def repcallspaces(m):
33 t = re.sub(r"\n\s+", "\n", m.group(2))
33 t = re.sub(r"\n\s+", "\n", m.group(2))
34 return m.group(1) + t
34 return m.group(1) + t
35
35
36 def repinclude(m):
36 def repinclude(m):
37 return m.group(1) + "<foo>"
37 return m.group(1) + "<foo>"
38
38
39 def rephere(m):
39 def rephere(m):
40 t = re.sub(r"\S", "x", m.group(2))
40 t = re.sub(r"\S", "x", m.group(2))
41 return m.group(1) + t
41 return m.group(1) + t
42
42
43
43
44 testpats = [
44 testpats = [
45 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
45 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
46 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
46 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
47 (r'^function', "don't use 'function', use old style"),
47 (r'^function', "don't use 'function', use old style"),
48 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
48 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
49 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
49 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
50 (r'echo -n', "don't use 'echo -n', use printf"),
50 (r'echo -n', "don't use 'echo -n', use printf"),
51 (r'^diff.*-\w*N', "don't use 'diff -N'"),
51 (r'^diff.*-\w*N', "don't use 'diff -N'"),
52 (r'(^| )wc[^|]*$', "filter wc output"),
52 (r'(^| )wc[^|]*$', "filter wc output"),
53 (r'head -c', "don't use 'head -c', use 'dd'"),
53 (r'head -c', "don't use 'head -c', use 'dd'"),
54 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
54 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
55 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
55 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
56 (r'printf.*\\x', "don't use printf \\x, use Python"),
56 (r'printf.*\\x', "don't use printf \\x, use Python"),
57 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
57 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
58 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
58 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
59 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
59 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
60 "use egrep for extended grep syntax"),
60 "use egrep for extended grep syntax"),
61 (r'/bin/', "don't use explicit paths for tools"),
61 (r'/bin/', "don't use explicit paths for tools"),
62 (r'\$PWD', "don't use $PWD, use `pwd`"),
62 (r'\$PWD', "don't use $PWD, use `pwd`"),
63 (r'[^\n]\Z', "no trailing newline"),
63 (r'[^\n]\Z', "no trailing newline"),
64 (r'export.*=', "don't export and assign at once"),
64 (r'export.*=', "don't export and assign at once"),
65 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
65 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
66 (r'^source\b', "don't use 'source', use '.'"),
66 (r'^source\b', "don't use 'source', use '.'"),
67 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
67 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
68 (r'ls\s+[^-]+\s+-', "options to 'ls' must come before filenames"),
68 (r'ls\s+[^-]+\s+-', "options to 'ls' must come before filenames"),
69 ]
69 ]
70
70
71 testfilters = [
71 testfilters = [
72 (r"( *)(#([^\n]*\S)?)", repcomment),
72 (r"( *)(#([^\n]*\S)?)", repcomment),
73 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
73 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
74 ]
74 ]
75
75
76 uprefix = r"^ \$ "
76 uprefix = r"^ \$ "
77 uprefixc = r"^ > "
77 uprefixc = r"^ > "
78 utestpats = [
78 utestpats = [
79 (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"),
79 (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"),
80 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
80 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
81 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
81 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
82 (uprefix + r'.*\$\?', "explicit exit code checks unnecessary"),
82 (uprefix + r'.*\$\?', "explicit exit code checks unnecessary"),
83 (uprefix + r'.*\|\| echo.*(fail|error)',
83 (uprefix + r'.*\|\| echo.*(fail|error)',
84 "explicit exit code checks unnecessary"),
84 "explicit exit code checks unnecessary"),
85 (uprefix + r'set -e', "don't use set -e"),
85 (uprefix + r'set -e', "don't use set -e"),
86 (uprefixc + r'( *)\t', "don't use tabs to indent"),
86 (uprefixc + r'( *)\t', "don't use tabs to indent"),
87 ]
87 ]
88
88
89 for p, m in testpats:
89 for p, m in testpats:
90 if p.startswith('^'):
90 if p.startswith('^'):
91 p = uprefix + p[1:]
91 p = uprefix + p[1:]
92 else:
92 else:
93 p = uprefix + p
93 p = uprefix + p
94 utestpats.append((p, m))
94 utestpats.append((p, m))
95
95
96 utestfilters = [
96 utestfilters = [
97 (r"( *)(#([^\n]*\S)?)", repcomment),
97 (r"( *)(#([^\n]*\S)?)", repcomment),
98 ]
98 ]
99
99
100 pypats = [
100 pypats = [
101 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
101 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
102 "tuple parameter unpacking not available in Python 3+"),
102 "tuple parameter unpacking not available in Python 3+"),
103 (r'lambda\s*\(.*,.*\)',
103 (r'lambda\s*\(.*,.*\)',
104 "tuple parameter unpacking not available in Python 3+"),
104 "tuple parameter unpacking not available in Python 3+"),
105 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
105 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
106 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
106 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
107 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
107 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
108 (r'^\s*\t', "don't use tabs"),
108 (r'^\s*\t', "don't use tabs"),
109 (r'\S;\s*\n', "semicolon"),
109 (r'\S;\s*\n', "semicolon"),
110 (r'\w,\w', "missing whitespace after ,"),
110 (r'\w,\w', "missing whitespace after ,"),
111 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
111 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
112 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
112 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
113 (r'.{85}', "line too long"),
113 (r'.{85}', "line too long"),
114 (r'.{81}', "warning: line over 80 characters"),
114 (r'.{81}', "warning: line over 80 characters"),
115 (r'[^\n]\Z', "no trailing newline"),
115 (r'[^\n]\Z', "no trailing newline"),
116 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
116 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
117 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
117 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
118 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
118 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
119 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
119 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
120 "linebreak after :"),
120 "linebreak after :"),
121 (r'class\s[^(]:', "old-style class, use class foo(object)"),
121 (r'class\s[^(]:', "old-style class, use class foo(object)"),
122 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
122 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
123 "Python keyword is not a function"),
123 "Python keyword is not a function"),
124 (r',]', "unneeded trailing ',' in list"),
124 (r',]', "unneeded trailing ',' in list"),
125 # (r'class\s[A-Z][^\(]*\((?!Exception)',
125 # (r'class\s[A-Z][^\(]*\((?!Exception)',
126 # "don't capitalize non-exception classes"),
126 # "don't capitalize non-exception classes"),
127 # (r'in range\(', "use xrange"),
127 # (r'in range\(', "use xrange"),
128 # (r'^\s*print\s+', "avoid using print in core and extensions"),
128 # (r'^\s*print\s+', "avoid using print in core and extensions"),
129 (r'[\x80-\xff]', "non-ASCII character literal"),
129 (r'[\x80-\xff]', "non-ASCII character literal"),
130 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
130 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
131 (r'^\s*with\s+', "with not available in Python 2.4"),
131 (r'^\s*with\s+', "with not available in Python 2.4"),
132 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
132 (r'(?<!def)\s+(any|all|format)\(',
133 (r'(?<!def)\s+(any|all|format)\(',
133 "any/all/format not available in Python 2.4"),
134 "any/all/format not available in Python 2.4"),
134 (r'(?<!def)\s+(callable)\(',
135 (r'(?<!def)\s+(callable)\(',
135 "callable not available in Python 3, use hasattr(f, '__call__')"),
136 "callable not available in Python 3, use hasattr(f, '__call__')"),
136 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
137 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
137 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
138 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
138 "gratuitous whitespace after Python keyword"),
139 "gratuitous whitespace after Python keyword"),
139 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
140 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
140 # (r'\s\s=', "gratuitous whitespace before ="),
141 # (r'\s\s=', "gratuitous whitespace before ="),
141 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
142 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
142 "missing whitespace around operator"),
143 "missing whitespace around operator"),
143 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
144 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
144 "missing whitespace around operator"),
145 "missing whitespace around operator"),
145 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
146 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
146 "missing whitespace around operator"),
147 "missing whitespace around operator"),
147 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
148 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
148 "wrong whitespace around ="),
149 "wrong whitespace around ="),
149 (r'raise Exception', "don't raise generic exceptions"),
150 (r'raise Exception', "don't raise generic exceptions"),
150 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
151 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
151 "warning: unwrapped ui message"),
152 "warning: unwrapped ui message"),
152 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
153 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
153 (r' [=!]=\s+(True|False|None)',
154 (r' [=!]=\s+(True|False|None)',
154 "comparison with singleton, use 'is' or 'is not' instead"),
155 "comparison with singleton, use 'is' or 'is not' instead"),
155 ]
156 ]
156
157
157 pyfilters = [
158 pyfilters = [
158 (r"""(?msx)(?P<comment>\#.*?$)|
159 (r"""(?msx)(?P<comment>\#.*?$)|
159 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
160 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
160 (?P<text>(([^\\]|\\.)*?))
161 (?P<text>(([^\\]|\\.)*?))
161 (?P=quote))""", reppython),
162 (?P=quote))""", reppython),
162 ]
163 ]
163
164
164 cpats = [
165 cpats = [
165 (r'//', "don't use //-style comments"),
166 (r'//', "don't use //-style comments"),
166 (r'^ ', "don't use spaces to indent"),
167 (r'^ ', "don't use spaces to indent"),
167 (r'\S\t', "don't use tabs except for indent"),
168 (r'\S\t', "don't use tabs except for indent"),
168 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
169 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
169 (r'.{85}', "line too long"),
170 (r'.{85}', "line too long"),
170 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
171 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
171 (r'return\(', "return is not a function"),
172 (r'return\(', "return is not a function"),
172 (r' ;', "no space before ;"),
173 (r' ;', "no space before ;"),
173 (r'\w+\* \w+', "use int *foo, not int* foo"),
174 (r'\w+\* \w+', "use int *foo, not int* foo"),
174 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
175 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
175 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
176 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
176 (r'\w,\w', "missing whitespace after ,"),
177 (r'\w,\w', "missing whitespace after ,"),
177 (r'\w[+/*]\w', "missing whitespace in expression"),
178 (r'\w[+/*]\w', "missing whitespace in expression"),
178 (r'^#\s+\w', "use #foo, not # foo"),
179 (r'^#\s+\w', "use #foo, not # foo"),
179 (r'[^\n]\Z', "no trailing newline"),
180 (r'[^\n]\Z', "no trailing newline"),
180 ]
181 ]
181
182
182 cfilters = [
183 cfilters = [
183 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
184 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
184 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
185 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
185 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
186 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
186 (r'(\()([^)]+\))', repcallspaces),
187 (r'(\()([^)]+\))', repcallspaces),
187 ]
188 ]
188
189
189 checks = [
190 checks = [
190 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
191 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
191 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
192 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
192 ('c', r'.*\.c$', cfilters, cpats),
193 ('c', r'.*\.c$', cfilters, cpats),
193 ('unified test', r'.*\.t$', utestfilters, utestpats),
194 ('unified test', r'.*\.t$', utestfilters, utestpats),
194 ]
195 ]
195
196
196 class norepeatlogger(object):
197 class norepeatlogger(object):
197 def __init__(self):
198 def __init__(self):
198 self._lastseen = None
199 self._lastseen = None
199
200
200 def log(self, fname, lineno, line, msg, blame):
201 def log(self, fname, lineno, line, msg, blame):
201 """print error related a to given line of a given file.
202 """print error related a to given line of a given file.
202
203
203 The faulty line will also be printed but only once in the case
204 The faulty line will also be printed but only once in the case
204 of multiple errors.
205 of multiple errors.
205
206
206 :fname: filename
207 :fname: filename
207 :lineno: line number
208 :lineno: line number
208 :line: actual content of the line
209 :line: actual content of the line
209 :msg: error message
210 :msg: error message
210 """
211 """
211 msgid = fname, lineno, line
212 msgid = fname, lineno, line
212 if msgid != self._lastseen:
213 if msgid != self._lastseen:
213 if blame:
214 if blame:
214 print "%s:%d (%s):" % (fname, lineno, blame)
215 print "%s:%d (%s):" % (fname, lineno, blame)
215 else:
216 else:
216 print "%s:%d:" % (fname, lineno)
217 print "%s:%d:" % (fname, lineno)
217 print " > %s" % line
218 print " > %s" % line
218 self._lastseen = msgid
219 self._lastseen = msgid
219 print " " + msg
220 print " " + msg
220
221
221 _defaultlogger = norepeatlogger()
222 _defaultlogger = norepeatlogger()
222
223
223 def getblame(f):
224 def getblame(f):
224 lines = []
225 lines = []
225 for l in os.popen('hg annotate -un %s' % f):
226 for l in os.popen('hg annotate -un %s' % f):
226 start, line = l.split(':', 1)
227 start, line = l.split(':', 1)
227 user, rev = start.split()
228 user, rev = start.split()
228 lines.append((line[1:-1], user, rev))
229 lines.append((line[1:-1], user, rev))
229 return lines
230 return lines
230
231
231 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
232 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
232 blame=False):
233 blame=False):
233 """checks style and portability of a given file
234 """checks style and portability of a given file
234
235
235 :f: filepath
236 :f: filepath
236 :logfunc: function used to report error
237 :logfunc: function used to report error
237 logfunc(filename, linenumber, linecontent, errormessage)
238 logfunc(filename, linenumber, linecontent, errormessage)
238 :maxerr: number of error to display before arborting.
239 :maxerr: number of error to display before arborting.
239 Set to None (default) to report all errors
240 Set to None (default) to report all errors
240
241
241 return True if no error is found, False otherwise.
242 return True if no error is found, False otherwise.
242 """
243 """
243 blamecache = None
244 blamecache = None
244 result = True
245 result = True
245 for name, match, filters, pats in checks:
246 for name, match, filters, pats in checks:
246 fc = 0
247 fc = 0
247 if not re.match(match, f):
248 if not re.match(match, f):
248 continue
249 continue
249 pre = post = open(f).read()
250 pre = post = open(f).read()
250 if "no-" + "check-code" in pre:
251 if "no-" + "check-code" in pre:
251 break
252 break
252 for p, r in filters:
253 for p, r in filters:
253 post = re.sub(p, r, post)
254 post = re.sub(p, r, post)
254 # print post # uncomment to show filtered version
255 # print post # uncomment to show filtered version
255 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
256 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
256 for n, l in z:
257 for n, l in z:
257 if "check-code" + "-ignore" in l[0]:
258 if "check-code" + "-ignore" in l[0]:
258 continue
259 continue
259 for p, msg in pats:
260 for p, msg in pats:
260 if not warnings and msg.startswith("warning"):
261 if not warnings and msg.startswith("warning"):
261 continue
262 continue
262 if re.search(p, l[1]):
263 if re.search(p, l[1]):
263 bd = ""
264 bd = ""
264 if blame:
265 if blame:
265 bd = 'working directory'
266 bd = 'working directory'
266 if not blamecache:
267 if not blamecache:
267 blamecache = getblame(f)
268 blamecache = getblame(f)
268 if n < len(blamecache):
269 if n < len(blamecache):
269 bl, bu, br = blamecache[n]
270 bl, bu, br = blamecache[n]
270 if bl == l[0]:
271 if bl == l[0]:
271 bd = '%s@%s' % (bu, br)
272 bd = '%s@%s' % (bu, br)
272 logfunc(f, n + 1, l[0], msg, bd)
273 logfunc(f, n + 1, l[0], msg, bd)
273 fc += 1
274 fc += 1
274 result = False
275 result = False
275 if maxerr is not None and fc >= maxerr:
276 if maxerr is not None and fc >= maxerr:
276 print " (too many errors, giving up)"
277 print " (too many errors, giving up)"
277 break
278 break
278 break
279 break
279 return result
280 return result
280
281
281 if __name__ == "__main__":
282 if __name__ == "__main__":
282 parser = optparse.OptionParser("%prog [options] [files]")
283 parser = optparse.OptionParser("%prog [options] [files]")
283 parser.add_option("-w", "--warnings", action="store_true",
284 parser.add_option("-w", "--warnings", action="store_true",
284 help="include warning-level checks")
285 help="include warning-level checks")
285 parser.add_option("-p", "--per-file", type="int",
286 parser.add_option("-p", "--per-file", type="int",
286 help="max warnings per file")
287 help="max warnings per file")
287 parser.add_option("-b", "--blame", action="store_true",
288 parser.add_option("-b", "--blame", action="store_true",
288 help="use annotate to generate blame info")
289 help="use annotate to generate blame info")
289
290
290 parser.set_defaults(per_file=15, warnings=False, blame=False)
291 parser.set_defaults(per_file=15, warnings=False, blame=False)
291 (options, args) = parser.parse_args()
292 (options, args) = parser.parse_args()
292
293
293 if len(args) == 0:
294 if len(args) == 0:
294 check = glob.glob("*")
295 check = glob.glob("*")
295 else:
296 else:
296 check = args
297 check = args
297
298
298 for f in check:
299 for f in check:
299 ret = 0
300 ret = 0
300 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
301 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
301 blame=options.blame):
302 blame=options.blame):
302 ret = 1
303 ret = 1
303 sys.exit(ret)
304 sys.exit(ret)
@@ -1,891 +1,891 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, util, node, error, cmdutil
11 import config, util, node, error, cmdutil
12 hg = None
12 hg = None
13
13
14 nullstate = ('', '', 'empty')
14 nullstate = ('', '', 'empty')
15
15
16
16
17 def substate(ctx):
17 def substate(ctx):
18 rev = {}
18 rev = {}
19 if '.hgsubstate' in ctx:
19 if '.hgsubstate' in ctx:
20 try:
20 try:
21 for l in ctx['.hgsubstate'].data().splitlines():
21 for l in ctx['.hgsubstate'].data().splitlines():
22 revision, path = l.split(" ", 1)
22 revision, path = l.split(" ", 1)
23 rev[path] = revision
23 rev[path] = revision
24 except IOError as err:
24 except IOError, err:
25 if err.errno != errno.ENOENT:
25 if err.errno != errno.ENOENT:
26 raise
26 raise
27 return rev
27 return rev
28
28
29 def state(ctx, ui):
29 def state(ctx, ui):
30 """return a state dict, mapping subrepo paths configured in .hgsub
30 """return a state dict, mapping subrepo paths configured in .hgsub
31 to tuple: (source from .hgsub, revision from .hgsubstate, kind
31 to tuple: (source from .hgsub, revision from .hgsubstate, kind
32 (key in types dict))
32 (key in types dict))
33 """
33 """
34 p = config.config()
34 p = config.config()
35 def read(f, sections=None, remap=None):
35 def read(f, sections=None, remap=None):
36 if f in ctx:
36 if f in ctx:
37 try:
37 try:
38 data = ctx[f].data()
38 data = ctx[f].data()
39 except IOError, err:
39 except IOError, err:
40 if err.errno != errno.ENOENT:
40 if err.errno != errno.ENOENT:
41 raise
41 raise
42 # handle missing subrepo spec files as removed
42 # handle missing subrepo spec files as removed
43 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
43 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
44 return
44 return
45 p.parse(f, data, sections, remap, read)
45 p.parse(f, data, sections, remap, read)
46 else:
46 else:
47 raise util.Abort(_("subrepo spec file %s not found") % f)
47 raise util.Abort(_("subrepo spec file %s not found") % f)
48
48
49 if '.hgsub' in ctx:
49 if '.hgsub' in ctx:
50 read('.hgsub')
50 read('.hgsub')
51
51
52 for path, src in ui.configitems('subpaths'):
52 for path, src in ui.configitems('subpaths'):
53 p.set('subpaths', path, src, ui.configsource('subpaths', path))
53 p.set('subpaths', path, src, ui.configsource('subpaths', path))
54
54
55 rev = substate(ctx)
55 rev = substate(ctx)
56
56
57 state = {}
57 state = {}
58 for path, src in p[''].items():
58 for path, src in p[''].items():
59 kind = 'hg'
59 kind = 'hg'
60 if src.startswith('['):
60 if src.startswith('['):
61 if ']' not in src:
61 if ']' not in src:
62 raise util.Abort(_('missing ] in subrepo source'))
62 raise util.Abort(_('missing ] in subrepo source'))
63 kind, src = src.split(']', 1)
63 kind, src = src.split(']', 1)
64 kind = kind[1:]
64 kind = kind[1:]
65
65
66 for pattern, repl in p.items('subpaths'):
66 for pattern, repl in p.items('subpaths'):
67 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
67 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
68 # does a string decode.
68 # does a string decode.
69 repl = repl.encode('string-escape')
69 repl = repl.encode('string-escape')
70 # However, we still want to allow back references to go
70 # However, we still want to allow back references to go
71 # through unharmed, so we turn r'\\1' into r'\1'. Again,
71 # through unharmed, so we turn r'\\1' into r'\1'. Again,
72 # extra escapes are needed because re.sub string decodes.
72 # extra escapes are needed because re.sub string decodes.
73 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
73 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
74 try:
74 try:
75 src = re.sub(pattern, repl, src, 1)
75 src = re.sub(pattern, repl, src, 1)
76 except re.error, e:
76 except re.error, e:
77 raise util.Abort(_("bad subrepository pattern in %s: %s")
77 raise util.Abort(_("bad subrepository pattern in %s: %s")
78 % (p.source('subpaths', pattern), e))
78 % (p.source('subpaths', pattern), e))
79
79
80 state[path] = (src.strip(), rev.get(path, ''), kind)
80 state[path] = (src.strip(), rev.get(path, ''), kind)
81
81
82 return state
82 return state
83
83
84 def writestate(repo, state):
84 def writestate(repo, state):
85 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
85 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
86 repo.wwrite('.hgsubstate',
86 repo.wwrite('.hgsubstate',
87 ''.join(['%s %s\n' % (state[s][1], s)
87 ''.join(['%s %s\n' % (state[s][1], s)
88 for s in sorted(state)]), '')
88 for s in sorted(state)]), '')
89
89
90 def submerge(repo, wctx, mctx, actx):
90 def submerge(repo, wctx, mctx, actx):
91 """delegated from merge.applyupdates: merging of .hgsubstate file
91 """delegated from merge.applyupdates: merging of .hgsubstate file
92 in working context, merging context and ancestor context"""
92 in working context, merging context and ancestor context"""
93 if mctx == actx: # backwards?
93 if mctx == actx: # backwards?
94 actx = wctx.p1()
94 actx = wctx.p1()
95 s1 = wctx.substate
95 s1 = wctx.substate
96 s2 = mctx.substate
96 s2 = mctx.substate
97 sa = actx.substate
97 sa = actx.substate
98 sm = {}
98 sm = {}
99
99
100 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
100 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
101
101
102 def debug(s, msg, r=""):
102 def debug(s, msg, r=""):
103 if r:
103 if r:
104 r = "%s:%s:%s" % r
104 r = "%s:%s:%s" % r
105 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
105 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
106
106
107 for s, l in s1.items():
107 for s, l in s1.items():
108 a = sa.get(s, nullstate)
108 a = sa.get(s, nullstate)
109 ld = l # local state with possible dirty flag for compares
109 ld = l # local state with possible dirty flag for compares
110 if wctx.sub(s).dirty():
110 if wctx.sub(s).dirty():
111 ld = (l[0], l[1] + "+")
111 ld = (l[0], l[1] + "+")
112 if wctx == actx: # overwrite
112 if wctx == actx: # overwrite
113 a = ld
113 a = ld
114
114
115 if s in s2:
115 if s in s2:
116 r = s2[s]
116 r = s2[s]
117 if ld == r or r == a: # no change or local is newer
117 if ld == r or r == a: # no change or local is newer
118 sm[s] = l
118 sm[s] = l
119 continue
119 continue
120 elif ld == a: # other side changed
120 elif ld == a: # other side changed
121 debug(s, "other changed, get", r)
121 debug(s, "other changed, get", r)
122 wctx.sub(s).get(r)
122 wctx.sub(s).get(r)
123 sm[s] = r
123 sm[s] = r
124 elif ld[0] != r[0]: # sources differ
124 elif ld[0] != r[0]: # sources differ
125 if repo.ui.promptchoice(
125 if repo.ui.promptchoice(
126 _(' subrepository sources for %s differ\n'
126 _(' subrepository sources for %s differ\n'
127 'use (l)ocal source (%s) or (r)emote source (%s)?')
127 'use (l)ocal source (%s) or (r)emote source (%s)?')
128 % (s, l[0], r[0]),
128 % (s, l[0], r[0]),
129 (_('&Local'), _('&Remote')), 0):
129 (_('&Local'), _('&Remote')), 0):
130 debug(s, "prompt changed, get", r)
130 debug(s, "prompt changed, get", r)
131 wctx.sub(s).get(r)
131 wctx.sub(s).get(r)
132 sm[s] = r
132 sm[s] = r
133 elif ld[1] == a[1]: # local side is unchanged
133 elif ld[1] == a[1]: # local side is unchanged
134 debug(s, "other side changed, get", r)
134 debug(s, "other side changed, get", r)
135 wctx.sub(s).get(r)
135 wctx.sub(s).get(r)
136 sm[s] = r
136 sm[s] = r
137 else:
137 else:
138 debug(s, "both sides changed, merge with", r)
138 debug(s, "both sides changed, merge with", r)
139 wctx.sub(s).merge(r)
139 wctx.sub(s).merge(r)
140 sm[s] = l
140 sm[s] = l
141 elif ld == a: # remote removed, local unchanged
141 elif ld == a: # remote removed, local unchanged
142 debug(s, "remote removed, remove")
142 debug(s, "remote removed, remove")
143 wctx.sub(s).remove()
143 wctx.sub(s).remove()
144 else:
144 else:
145 if repo.ui.promptchoice(
145 if repo.ui.promptchoice(
146 _(' local changed subrepository %s which remote removed\n'
146 _(' local changed subrepository %s which remote removed\n'
147 'use (c)hanged version or (d)elete?') % s,
147 'use (c)hanged version or (d)elete?') % s,
148 (_('&Changed'), _('&Delete')), 0):
148 (_('&Changed'), _('&Delete')), 0):
149 debug(s, "prompt remove")
149 debug(s, "prompt remove")
150 wctx.sub(s).remove()
150 wctx.sub(s).remove()
151
151
152 for s, r in s2.items():
152 for s, r in s2.items():
153 if s in s1:
153 if s in s1:
154 continue
154 continue
155 elif s not in sa:
155 elif s not in sa:
156 debug(s, "remote added, get", r)
156 debug(s, "remote added, get", r)
157 mctx.sub(s).get(r)
157 mctx.sub(s).get(r)
158 sm[s] = r
158 sm[s] = r
159 elif r != sa[s]:
159 elif r != sa[s]:
160 if repo.ui.promptchoice(
160 if repo.ui.promptchoice(
161 _(' remote changed subrepository %s which local removed\n'
161 _(' remote changed subrepository %s which local removed\n'
162 'use (c)hanged version or (d)elete?') % s,
162 'use (c)hanged version or (d)elete?') % s,
163 (_('&Changed'), _('&Delete')), 0) == 0:
163 (_('&Changed'), _('&Delete')), 0) == 0:
164 debug(s, "prompt recreate", r)
164 debug(s, "prompt recreate", r)
165 wctx.sub(s).get(r)
165 wctx.sub(s).get(r)
166 sm[s] = r
166 sm[s] = r
167
167
168 # record merged .hgsubstate
168 # record merged .hgsubstate
169 writestate(repo, sm)
169 writestate(repo, sm)
170
170
171 def reporelpath(repo):
171 def reporelpath(repo):
172 """return path to this (sub)repo as seen from outermost repo"""
172 """return path to this (sub)repo as seen from outermost repo"""
173 parent = repo
173 parent = repo
174 while hasattr(parent, '_subparent'):
174 while hasattr(parent, '_subparent'):
175 parent = parent._subparent
175 parent = parent._subparent
176 return repo.root[len(parent.root)+1:]
176 return repo.root[len(parent.root)+1:]
177
177
178 def subrelpath(sub):
178 def subrelpath(sub):
179 """return path to this subrepo as seen from outermost repo"""
179 """return path to this subrepo as seen from outermost repo"""
180 if not hasattr(sub, '_repo'):
180 if not hasattr(sub, '_repo'):
181 return sub._path
181 return sub._path
182 return reporelpath(sub._repo)
182 return reporelpath(sub._repo)
183
183
184 def _abssource(repo, push=False, abort=True):
184 def _abssource(repo, push=False, abort=True):
185 """return pull/push path of repo - either based on parent repo .hgsub info
185 """return pull/push path of repo - either based on parent repo .hgsub info
186 or on the top repo config. Abort or return None if no source found."""
186 or on the top repo config. Abort or return None if no source found."""
187 if hasattr(repo, '_subparent'):
187 if hasattr(repo, '_subparent'):
188 source = repo._subsource
188 source = repo._subsource
189 if source.startswith('/') or '://' in source:
189 if source.startswith('/') or '://' in source:
190 return source
190 return source
191 parent = _abssource(repo._subparent, push, abort=False)
191 parent = _abssource(repo._subparent, push, abort=False)
192 if parent:
192 if parent:
193 if '://' in parent:
193 if '://' in parent:
194 if parent[-1] == '/':
194 if parent[-1] == '/':
195 parent = parent[:-1]
195 parent = parent[:-1]
196 r = urlparse.urlparse(parent + '/' + source)
196 r = urlparse.urlparse(parent + '/' + source)
197 r = urlparse.urlunparse((r[0], r[1],
197 r = urlparse.urlunparse((r[0], r[1],
198 posixpath.normpath(r[2]),
198 posixpath.normpath(r[2]),
199 r[3], r[4], r[5]))
199 r[3], r[4], r[5]))
200 return r
200 return r
201 else: # plain file system path
201 else: # plain file system path
202 return posixpath.normpath(os.path.join(parent, repo._subsource))
202 return posixpath.normpath(os.path.join(parent, repo._subsource))
203 else: # recursion reached top repo
203 else: # recursion reached top repo
204 if hasattr(repo, '_subtoppath'):
204 if hasattr(repo, '_subtoppath'):
205 return repo._subtoppath
205 return repo._subtoppath
206 if push and repo.ui.config('paths', 'default-push'):
206 if push and repo.ui.config('paths', 'default-push'):
207 return repo.ui.config('paths', 'default-push')
207 return repo.ui.config('paths', 'default-push')
208 if repo.ui.config('paths', 'default'):
208 if repo.ui.config('paths', 'default'):
209 return repo.ui.config('paths', 'default')
209 return repo.ui.config('paths', 'default')
210 if abort:
210 if abort:
211 raise util.Abort(_("default path for subrepository %s not found") %
211 raise util.Abort(_("default path for subrepository %s not found") %
212 reporelpath(repo))
212 reporelpath(repo))
213
213
214 def itersubrepos(ctx1, ctx2):
214 def itersubrepos(ctx1, ctx2):
215 """find subrepos in ctx1 or ctx2"""
215 """find subrepos in ctx1 or ctx2"""
216 # Create a (subpath, ctx) mapping where we prefer subpaths from
216 # Create a (subpath, ctx) mapping where we prefer subpaths from
217 # ctx1. The subpaths from ctx2 are important when the .hgsub file
217 # ctx1. The subpaths from ctx2 are important when the .hgsub file
218 # has been modified (in ctx2) but not yet committed (in ctx1).
218 # has been modified (in ctx2) but not yet committed (in ctx1).
219 subpaths = dict.fromkeys(ctx2.substate, ctx2)
219 subpaths = dict.fromkeys(ctx2.substate, ctx2)
220 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
220 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
221 for subpath, ctx in sorted(subpaths.iteritems()):
221 for subpath, ctx in sorted(subpaths.iteritems()):
222 yield subpath, ctx.sub(subpath)
222 yield subpath, ctx.sub(subpath)
223
223
224 def subrepo(ctx, path):
224 def subrepo(ctx, path):
225 """return instance of the right subrepo class for subrepo in path"""
225 """return instance of the right subrepo class for subrepo in path"""
226 # subrepo inherently violates our import layering rules
226 # subrepo inherently violates our import layering rules
227 # because it wants to make repo objects from deep inside the stack
227 # because it wants to make repo objects from deep inside the stack
228 # so we manually delay the circular imports to not break
228 # so we manually delay the circular imports to not break
229 # scripts that don't use our demand-loading
229 # scripts that don't use our demand-loading
230 global hg
230 global hg
231 import hg as h
231 import hg as h
232 hg = h
232 hg = h
233
233
234 util.path_auditor(ctx._repo.root)(path)
234 util.path_auditor(ctx._repo.root)(path)
235 state = ctx.substate.get(path, nullstate)
235 state = ctx.substate.get(path, nullstate)
236 if state[2] not in types:
236 if state[2] not in types:
237 raise util.Abort(_('unknown subrepo type %s') % state[2])
237 raise util.Abort(_('unknown subrepo type %s') % state[2])
238 return types[state[2]](ctx, path, state[:2])
238 return types[state[2]](ctx, path, state[:2])
239
239
240 # subrepo classes need to implement the following abstract class:
240 # subrepo classes need to implement the following abstract class:
241
241
242 class abstractsubrepo(object):
242 class abstractsubrepo(object):
243
243
244 def dirty(self):
244 def dirty(self):
245 """returns true if the dirstate of the subrepo does not match
245 """returns true if the dirstate of the subrepo does not match
246 current stored state
246 current stored state
247 """
247 """
248 raise NotImplementedError
248 raise NotImplementedError
249
249
250 def checknested(self, path):
250 def checknested(self, path):
251 """check if path is a subrepository within this repository"""
251 """check if path is a subrepository within this repository"""
252 return False
252 return False
253
253
254 def commit(self, text, user, date):
254 def commit(self, text, user, date):
255 """commit the current changes to the subrepo with the given
255 """commit the current changes to the subrepo with the given
256 log message. Use given user and date if possible. Return the
256 log message. Use given user and date if possible. Return the
257 new state of the subrepo.
257 new state of the subrepo.
258 """
258 """
259 raise NotImplementedError
259 raise NotImplementedError
260
260
261 def remove(self):
261 def remove(self):
262 """remove the subrepo
262 """remove the subrepo
263
263
264 (should verify the dirstate is not dirty first)
264 (should verify the dirstate is not dirty first)
265 """
265 """
266 raise NotImplementedError
266 raise NotImplementedError
267
267
268 def get(self, state):
268 def get(self, state):
269 """run whatever commands are needed to put the subrepo into
269 """run whatever commands are needed to put the subrepo into
270 this state
270 this state
271 """
271 """
272 raise NotImplementedError
272 raise NotImplementedError
273
273
274 def merge(self, state):
274 def merge(self, state):
275 """merge currently-saved state with the new state."""
275 """merge currently-saved state with the new state."""
276 raise NotImplementedError
276 raise NotImplementedError
277
277
278 def push(self, force):
278 def push(self, force):
279 """perform whatever action is analogous to 'hg push'
279 """perform whatever action is analogous to 'hg push'
280
280
281 This may be a no-op on some systems.
281 This may be a no-op on some systems.
282 """
282 """
283 raise NotImplementedError
283 raise NotImplementedError
284
284
285 def add(self, ui, match, dryrun, prefix):
285 def add(self, ui, match, dryrun, prefix):
286 return []
286 return []
287
287
288 def status(self, rev2, **opts):
288 def status(self, rev2, **opts):
289 return [], [], [], [], [], [], []
289 return [], [], [], [], [], [], []
290
290
291 def diff(self, diffopts, node2, match, prefix, **opts):
291 def diff(self, diffopts, node2, match, prefix, **opts):
292 pass
292 pass
293
293
294 def outgoing(self, ui, dest, opts):
294 def outgoing(self, ui, dest, opts):
295 return 1
295 return 1
296
296
297 def incoming(self, ui, source, opts):
297 def incoming(self, ui, source, opts):
298 return 1
298 return 1
299
299
300 def files(self):
300 def files(self):
301 """return filename iterator"""
301 """return filename iterator"""
302 raise NotImplementedError
302 raise NotImplementedError
303
303
304 def filedata(self, name):
304 def filedata(self, name):
305 """return file data"""
305 """return file data"""
306 raise NotImplementedError
306 raise NotImplementedError
307
307
308 def fileflags(self, name):
308 def fileflags(self, name):
309 """return file flags"""
309 """return file flags"""
310 return ''
310 return ''
311
311
312 def archive(self, ui, archiver, prefix):
312 def archive(self, ui, archiver, prefix):
313 files = self.files()
313 files = self.files()
314 total = len(files)
314 total = len(files)
315 relpath = subrelpath(self)
315 relpath = subrelpath(self)
316 ui.progress(_('archiving (%s)') % relpath, 0,
316 ui.progress(_('archiving (%s)') % relpath, 0,
317 unit=_('files'), total=total)
317 unit=_('files'), total=total)
318 for i, name in enumerate(files):
318 for i, name in enumerate(files):
319 flags = self.fileflags(name)
319 flags = self.fileflags(name)
320 mode = 'x' in flags and 0755 or 0644
320 mode = 'x' in flags and 0755 or 0644
321 symlink = 'l' in flags
321 symlink = 'l' in flags
322 archiver.addfile(os.path.join(prefix, self._path, name),
322 archiver.addfile(os.path.join(prefix, self._path, name),
323 mode, symlink, self.filedata(name))
323 mode, symlink, self.filedata(name))
324 ui.progress(_('archiving (%s)') % relpath, i + 1,
324 ui.progress(_('archiving (%s)') % relpath, i + 1,
325 unit=_('files'), total=total)
325 unit=_('files'), total=total)
326 ui.progress(_('archiving (%s)') % relpath, None)
326 ui.progress(_('archiving (%s)') % relpath, None)
327
327
328
328
329 class hgsubrepo(abstractsubrepo):
329 class hgsubrepo(abstractsubrepo):
330 def __init__(self, ctx, path, state):
330 def __init__(self, ctx, path, state):
331 self._path = path
331 self._path = path
332 self._state = state
332 self._state = state
333 r = ctx._repo
333 r = ctx._repo
334 root = r.wjoin(path)
334 root = r.wjoin(path)
335 create = False
335 create = False
336 if not os.path.exists(os.path.join(root, '.hg')):
336 if not os.path.exists(os.path.join(root, '.hg')):
337 create = True
337 create = True
338 util.makedirs(root)
338 util.makedirs(root)
339 self._repo = hg.repository(r.ui, root, create=create)
339 self._repo = hg.repository(r.ui, root, create=create)
340 self._repo._subparent = r
340 self._repo._subparent = r
341 self._repo._subsource = state[0]
341 self._repo._subsource = state[0]
342
342
343 if create:
343 if create:
344 fp = self._repo.opener("hgrc", "w", text=True)
344 fp = self._repo.opener("hgrc", "w", text=True)
345 fp.write('[paths]\n')
345 fp.write('[paths]\n')
346
346
347 def addpathconfig(key, value):
347 def addpathconfig(key, value):
348 if value:
348 if value:
349 fp.write('%s = %s\n' % (key, value))
349 fp.write('%s = %s\n' % (key, value))
350 self._repo.ui.setconfig('paths', key, value)
350 self._repo.ui.setconfig('paths', key, value)
351
351
352 defpath = _abssource(self._repo, abort=False)
352 defpath = _abssource(self._repo, abort=False)
353 defpushpath = _abssource(self._repo, True, abort=False)
353 defpushpath = _abssource(self._repo, True, abort=False)
354 addpathconfig('default', defpath)
354 addpathconfig('default', defpath)
355 if defpath != defpushpath:
355 if defpath != defpushpath:
356 addpathconfig('default-push', defpushpath)
356 addpathconfig('default-push', defpushpath)
357 fp.close()
357 fp.close()
358
358
359 def add(self, ui, match, dryrun, prefix):
359 def add(self, ui, match, dryrun, prefix):
360 return cmdutil.add(ui, self._repo, match, dryrun, True,
360 return cmdutil.add(ui, self._repo, match, dryrun, True,
361 os.path.join(prefix, self._path))
361 os.path.join(prefix, self._path))
362
362
363 def status(self, rev2, **opts):
363 def status(self, rev2, **opts):
364 try:
364 try:
365 rev1 = self._state[1]
365 rev1 = self._state[1]
366 ctx1 = self._repo[rev1]
366 ctx1 = self._repo[rev1]
367 ctx2 = self._repo[rev2]
367 ctx2 = self._repo[rev2]
368 return self._repo.status(ctx1, ctx2, **opts)
368 return self._repo.status(ctx1, ctx2, **opts)
369 except error.RepoLookupError, inst:
369 except error.RepoLookupError, inst:
370 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
370 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
371 % (inst, subrelpath(self)))
371 % (inst, subrelpath(self)))
372 return [], [], [], [], [], [], []
372 return [], [], [], [], [], [], []
373
373
374 def diff(self, diffopts, node2, match, prefix, **opts):
374 def diff(self, diffopts, node2, match, prefix, **opts):
375 try:
375 try:
376 node1 = node.bin(self._state[1])
376 node1 = node.bin(self._state[1])
377 # We currently expect node2 to come from substate and be
377 # We currently expect node2 to come from substate and be
378 # in hex format
378 # in hex format
379 if node2 is not None:
379 if node2 is not None:
380 node2 = node.bin(node2)
380 node2 = node.bin(node2)
381 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
381 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
382 node1, node2, match,
382 node1, node2, match,
383 prefix=os.path.join(prefix, self._path),
383 prefix=os.path.join(prefix, self._path),
384 listsubrepos=True, **opts)
384 listsubrepos=True, **opts)
385 except error.RepoLookupError, inst:
385 except error.RepoLookupError, inst:
386 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
386 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
387 % (inst, subrelpath(self)))
387 % (inst, subrelpath(self)))
388
388
389 def archive(self, ui, archiver, prefix):
389 def archive(self, ui, archiver, prefix):
390 abstractsubrepo.archive(self, ui, archiver, prefix)
390 abstractsubrepo.archive(self, ui, archiver, prefix)
391
391
392 rev = self._state[1]
392 rev = self._state[1]
393 ctx = self._repo[rev]
393 ctx = self._repo[rev]
394 for subpath in ctx.substate:
394 for subpath in ctx.substate:
395 s = subrepo(ctx, subpath)
395 s = subrepo(ctx, subpath)
396 s.archive(ui, archiver, os.path.join(prefix, self._path))
396 s.archive(ui, archiver, os.path.join(prefix, self._path))
397
397
398 def dirty(self):
398 def dirty(self):
399 r = self._state[1]
399 r = self._state[1]
400 if r == '':
400 if r == '':
401 return True
401 return True
402 w = self._repo[None]
402 w = self._repo[None]
403 if w.p1() != self._repo[r]: # version checked out change
403 if w.p1() != self._repo[r]: # version checked out change
404 return True
404 return True
405 return w.dirty() # working directory changed
405 return w.dirty() # working directory changed
406
406
407 def checknested(self, path):
407 def checknested(self, path):
408 return self._repo._checknested(self._repo.wjoin(path))
408 return self._repo._checknested(self._repo.wjoin(path))
409
409
410 def commit(self, text, user, date):
410 def commit(self, text, user, date):
411 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
411 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
412 n = self._repo.commit(text, user, date)
412 n = self._repo.commit(text, user, date)
413 if not n:
413 if not n:
414 return self._repo['.'].hex() # different version checked out
414 return self._repo['.'].hex() # different version checked out
415 return node.hex(n)
415 return node.hex(n)
416
416
417 def remove(self):
417 def remove(self):
418 # we can't fully delete the repository as it may contain
418 # we can't fully delete the repository as it may contain
419 # local-only history
419 # local-only history
420 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
420 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
421 hg.clean(self._repo, node.nullid, False)
421 hg.clean(self._repo, node.nullid, False)
422
422
423 def _get(self, state):
423 def _get(self, state):
424 source, revision, kind = state
424 source, revision, kind = state
425 try:
425 try:
426 self._repo.lookup(revision)
426 self._repo.lookup(revision)
427 except error.RepoError:
427 except error.RepoError:
428 self._repo._subsource = source
428 self._repo._subsource = source
429 srcurl = _abssource(self._repo)
429 srcurl = _abssource(self._repo)
430 self._repo.ui.status(_('pulling subrepo %s from %s\n')
430 self._repo.ui.status(_('pulling subrepo %s from %s\n')
431 % (subrelpath(self), srcurl))
431 % (subrelpath(self), srcurl))
432 other = hg.repository(self._repo.ui, srcurl)
432 other = hg.repository(self._repo.ui, srcurl)
433 self._repo.pull(other)
433 self._repo.pull(other)
434
434
435 def get(self, state):
435 def get(self, state):
436 self._get(state)
436 self._get(state)
437 source, revision, kind = state
437 source, revision, kind = state
438 self._repo.ui.debug("getting subrepo %s\n" % self._path)
438 self._repo.ui.debug("getting subrepo %s\n" % self._path)
439 hg.clean(self._repo, revision, False)
439 hg.clean(self._repo, revision, False)
440
440
441 def merge(self, state):
441 def merge(self, state):
442 self._get(state)
442 self._get(state)
443 cur = self._repo['.']
443 cur = self._repo['.']
444 dst = self._repo[state[1]]
444 dst = self._repo[state[1]]
445 anc = dst.ancestor(cur)
445 anc = dst.ancestor(cur)
446 if anc == cur:
446 if anc == cur:
447 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
447 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
448 hg.update(self._repo, state[1])
448 hg.update(self._repo, state[1])
449 elif anc == dst:
449 elif anc == dst:
450 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
450 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
451 else:
451 else:
452 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
452 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
453 hg.merge(self._repo, state[1], remind=False)
453 hg.merge(self._repo, state[1], remind=False)
454
454
455 def push(self, force):
455 def push(self, force):
456 # push subrepos depth-first for coherent ordering
456 # push subrepos depth-first for coherent ordering
457 c = self._repo['']
457 c = self._repo['']
458 subs = c.substate # only repos that are committed
458 subs = c.substate # only repos that are committed
459 for s in sorted(subs):
459 for s in sorted(subs):
460 if not c.sub(s).push(force):
460 if not c.sub(s).push(force):
461 return False
461 return False
462
462
463 dsturl = _abssource(self._repo, True)
463 dsturl = _abssource(self._repo, True)
464 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
464 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
465 (subrelpath(self), dsturl))
465 (subrelpath(self), dsturl))
466 other = hg.repository(self._repo.ui, dsturl)
466 other = hg.repository(self._repo.ui, dsturl)
467 return self._repo.push(other, force)
467 return self._repo.push(other, force)
468
468
469 def outgoing(self, ui, dest, opts):
469 def outgoing(self, ui, dest, opts):
470 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
470 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
471
471
472 def incoming(self, ui, source, opts):
472 def incoming(self, ui, source, opts):
473 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
473 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
474
474
475 def files(self):
475 def files(self):
476 rev = self._state[1]
476 rev = self._state[1]
477 ctx = self._repo[rev]
477 ctx = self._repo[rev]
478 return ctx.manifest()
478 return ctx.manifest()
479
479
480 def filedata(self, name):
480 def filedata(self, name):
481 rev = self._state[1]
481 rev = self._state[1]
482 return self._repo[rev][name].data()
482 return self._repo[rev][name].data()
483
483
484 def fileflags(self, name):
484 def fileflags(self, name):
485 rev = self._state[1]
485 rev = self._state[1]
486 ctx = self._repo[rev]
486 ctx = self._repo[rev]
487 return ctx.flags(name)
487 return ctx.flags(name)
488
488
489
489
490 class svnsubrepo(abstractsubrepo):
490 class svnsubrepo(abstractsubrepo):
491 def __init__(self, ctx, path, state):
491 def __init__(self, ctx, path, state):
492 self._path = path
492 self._path = path
493 self._state = state
493 self._state = state
494 self._ctx = ctx
494 self._ctx = ctx
495 self._ui = ctx._repo.ui
495 self._ui = ctx._repo.ui
496
496
497 def _svncommand(self, commands, filename=''):
497 def _svncommand(self, commands, filename=''):
498 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
498 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
499 cmd = ['svn'] + commands + [path]
499 cmd = ['svn'] + commands + [path]
500 env = dict(os.environ)
500 env = dict(os.environ)
501 # Avoid localized output, preserve current locale for everything else.
501 # Avoid localized output, preserve current locale for everything else.
502 env['LC_MESSAGES'] = 'C'
502 env['LC_MESSAGES'] = 'C'
503 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
503 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
504 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
504 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
505 universal_newlines=True, env=env)
505 universal_newlines=True, env=env)
506 stdout, stderr = p.communicate()
506 stdout, stderr = p.communicate()
507 stderr = stderr.strip()
507 stderr = stderr.strip()
508 if stderr:
508 if stderr:
509 raise util.Abort(stderr)
509 raise util.Abort(stderr)
510 return stdout
510 return stdout
511
511
512 def _wcrev(self):
512 def _wcrev(self):
513 output = self._svncommand(['info', '--xml'])
513 output = self._svncommand(['info', '--xml'])
514 doc = xml.dom.minidom.parseString(output)
514 doc = xml.dom.minidom.parseString(output)
515 entries = doc.getElementsByTagName('entry')
515 entries = doc.getElementsByTagName('entry')
516 if not entries:
516 if not entries:
517 return '0'
517 return '0'
518 return str(entries[0].getAttribute('revision')) or '0'
518 return str(entries[0].getAttribute('revision')) or '0'
519
519
520 def _wcchanged(self):
520 def _wcchanged(self):
521 """Return (changes, extchanges) where changes is True
521 """Return (changes, extchanges) where changes is True
522 if the working directory was changed, and extchanges is
522 if the working directory was changed, and extchanges is
523 True if any of these changes concern an external entry.
523 True if any of these changes concern an external entry.
524 """
524 """
525 output = self._svncommand(['status', '--xml'])
525 output = self._svncommand(['status', '--xml'])
526 externals, changes = [], []
526 externals, changes = [], []
527 doc = xml.dom.minidom.parseString(output)
527 doc = xml.dom.minidom.parseString(output)
528 for e in doc.getElementsByTagName('entry'):
528 for e in doc.getElementsByTagName('entry'):
529 s = e.getElementsByTagName('wc-status')
529 s = e.getElementsByTagName('wc-status')
530 if not s:
530 if not s:
531 continue
531 continue
532 item = s[0].getAttribute('item')
532 item = s[0].getAttribute('item')
533 props = s[0].getAttribute('props')
533 props = s[0].getAttribute('props')
534 path = e.getAttribute('path')
534 path = e.getAttribute('path')
535 if item == 'external':
535 if item == 'external':
536 externals.append(path)
536 externals.append(path)
537 if (item not in ('', 'normal', 'unversioned', 'external')
537 if (item not in ('', 'normal', 'unversioned', 'external')
538 or props not in ('', 'none')):
538 or props not in ('', 'none')):
539 changes.append(path)
539 changes.append(path)
540 for path in changes:
540 for path in changes:
541 for ext in externals:
541 for ext in externals:
542 if path == ext or path.startswith(ext + os.sep):
542 if path == ext or path.startswith(ext + os.sep):
543 return True, True
543 return True, True
544 return bool(changes), False
544 return bool(changes), False
545
545
546 def dirty(self):
546 def dirty(self):
547 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
547 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
548 return False
548 return False
549 return True
549 return True
550
550
551 def commit(self, text, user, date):
551 def commit(self, text, user, date):
552 # user and date are out of our hands since svn is centralized
552 # user and date are out of our hands since svn is centralized
553 changed, extchanged = self._wcchanged()
553 changed, extchanged = self._wcchanged()
554 if not changed:
554 if not changed:
555 return self._wcrev()
555 return self._wcrev()
556 if extchanged:
556 if extchanged:
557 # Do not try to commit externals
557 # Do not try to commit externals
558 raise util.Abort(_('cannot commit svn externals'))
558 raise util.Abort(_('cannot commit svn externals'))
559 commitinfo = self._svncommand(['commit', '-m', text])
559 commitinfo = self._svncommand(['commit', '-m', text])
560 self._ui.status(commitinfo)
560 self._ui.status(commitinfo)
561 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
561 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
562 if not newrev:
562 if not newrev:
563 raise util.Abort(commitinfo.splitlines()[-1])
563 raise util.Abort(commitinfo.splitlines()[-1])
564 newrev = newrev.groups()[0]
564 newrev = newrev.groups()[0]
565 self._ui.status(self._svncommand(['update', '-r', newrev]))
565 self._ui.status(self._svncommand(['update', '-r', newrev]))
566 return newrev
566 return newrev
567
567
568 def remove(self):
568 def remove(self):
569 if self.dirty():
569 if self.dirty():
570 self._ui.warn(_('not removing repo %s because '
570 self._ui.warn(_('not removing repo %s because '
571 'it has changes.\n' % self._path))
571 'it has changes.\n' % self._path))
572 return
572 return
573 self._ui.note(_('removing subrepo %s\n') % self._path)
573 self._ui.note(_('removing subrepo %s\n') % self._path)
574
574
575 def onerror(function, path, excinfo):
575 def onerror(function, path, excinfo):
576 if function is not os.remove:
576 if function is not os.remove:
577 raise
577 raise
578 # read-only files cannot be unlinked under Windows
578 # read-only files cannot be unlinked under Windows
579 s = os.stat(path)
579 s = os.stat(path)
580 if (s.st_mode & stat.S_IWRITE) != 0:
580 if (s.st_mode & stat.S_IWRITE) != 0:
581 raise
581 raise
582 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
582 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
583 os.remove(path)
583 os.remove(path)
584
584
585 path = self._ctx._repo.wjoin(self._path)
585 path = self._ctx._repo.wjoin(self._path)
586 shutil.rmtree(path, onerror=onerror)
586 shutil.rmtree(path, onerror=onerror)
587 try:
587 try:
588 os.removedirs(os.path.dirname(path))
588 os.removedirs(os.path.dirname(path))
589 except OSError:
589 except OSError:
590 pass
590 pass
591
591
592 def get(self, state):
592 def get(self, state):
593 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
593 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
594 if not re.search('Checked out revision [0-9]+.', status):
594 if not re.search('Checked out revision [0-9]+.', status):
595 raise util.Abort(status.splitlines()[-1])
595 raise util.Abort(status.splitlines()[-1])
596 self._ui.status(status)
596 self._ui.status(status)
597
597
598 def merge(self, state):
598 def merge(self, state):
599 old = int(self._state[1])
599 old = int(self._state[1])
600 new = int(state[1])
600 new = int(state[1])
601 if new > old:
601 if new > old:
602 self.get(state)
602 self.get(state)
603
603
604 def push(self, force):
604 def push(self, force):
605 # push is a no-op for SVN
605 # push is a no-op for SVN
606 return True
606 return True
607
607
608 def files(self):
608 def files(self):
609 output = self._svncommand(['list'])
609 output = self._svncommand(['list'])
610 # This works because svn forbids \n in filenames.
610 # This works because svn forbids \n in filenames.
611 return output.splitlines()
611 return output.splitlines()
612
612
613 def filedata(self, name):
613 def filedata(self, name):
614 return self._svncommand(['cat'], name)
614 return self._svncommand(['cat'], name)
615
615
616
616
617 class gitsubrepo(abstractsubrepo):
617 class gitsubrepo(abstractsubrepo):
618 def __init__(self, ctx, path, state):
618 def __init__(self, ctx, path, state):
619 # TODO add git version check.
619 # TODO add git version check.
620 self._state = state
620 self._state = state
621 self._ctx = ctx
621 self._ctx = ctx
622 self._relpath = path
622 self._relpath = path
623 self._path = ctx._repo.wjoin(path)
623 self._path = ctx._repo.wjoin(path)
624 self._ui = ctx._repo.ui
624 self._ui = ctx._repo.ui
625
625
626 def _gitcommand(self, commands, env=None, stream=False):
626 def _gitcommand(self, commands, env=None, stream=False):
627 return self._gitdir(commands, env=env, stream=stream)[0]
627 return self._gitdir(commands, env=env, stream=stream)[0]
628
628
629 def _gitdir(self, commands, env=None, stream=False):
629 def _gitdir(self, commands, env=None, stream=False):
630 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
630 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
631
631
632 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
632 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
633 """Calls the git command
633 """Calls the git command
634
634
635 The methods tries to call the git command. versions previor to 1.6.0
635 The methods tries to call the git command. versions previor to 1.6.0
636 are not supported and very probably fail.
636 are not supported and very probably fail.
637 """
637 """
638 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
638 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
639 # unless ui.quiet is set, print git's stderr,
639 # unless ui.quiet is set, print git's stderr,
640 # which is mostly progress and useful info
640 # which is mostly progress and useful info
641 errpipe = None
641 errpipe = None
642 if self._ui.quiet:
642 if self._ui.quiet:
643 errpipe = open(os.devnull, 'w')
643 errpipe = open(os.devnull, 'w')
644 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
644 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
645 close_fds=util.closefds,
645 close_fds=util.closefds,
646 stdout=subprocess.PIPE, stderr=errpipe)
646 stdout=subprocess.PIPE, stderr=errpipe)
647 if stream:
647 if stream:
648 return p.stdout, None
648 return p.stdout, None
649
649
650 retdata = p.stdout.read().strip()
650 retdata = p.stdout.read().strip()
651 # wait for the child to exit to avoid race condition.
651 # wait for the child to exit to avoid race condition.
652 p.wait()
652 p.wait()
653
653
654 if p.returncode != 0 and p.returncode != 1:
654 if p.returncode != 0 and p.returncode != 1:
655 # there are certain error codes that are ok
655 # there are certain error codes that are ok
656 command = commands[0]
656 command = commands[0]
657 if command in ('cat-file', 'symbolic-ref'):
657 if command in ('cat-file', 'symbolic-ref'):
658 return retdata, p.returncode
658 return retdata, p.returncode
659 # for all others, abort
659 # for all others, abort
660 raise util.Abort('git %s error %d in %s' %
660 raise util.Abort('git %s error %d in %s' %
661 (command, p.returncode, self._relpath))
661 (command, p.returncode, self._relpath))
662
662
663 return retdata, p.returncode
663 return retdata, p.returncode
664
664
665 def _gitstate(self):
665 def _gitstate(self):
666 return self._gitcommand(['rev-parse', 'HEAD'])
666 return self._gitcommand(['rev-parse', 'HEAD'])
667
667
668 def _gitcurrentbranch(self):
668 def _gitcurrentbranch(self):
669 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
669 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
670 if err:
670 if err:
671 current = None
671 current = None
672 return current
672 return current
673
673
674 def _githavelocally(self, revision):
674 def _githavelocally(self, revision):
675 out, code = self._gitdir(['cat-file', '-e', revision])
675 out, code = self._gitdir(['cat-file', '-e', revision])
676 return code == 0
676 return code == 0
677
677
678 def _gitisancestor(self, r1, r2):
678 def _gitisancestor(self, r1, r2):
679 base = self._gitcommand(['merge-base', r1, r2])
679 base = self._gitcommand(['merge-base', r1, r2])
680 return base == r1
680 return base == r1
681
681
682 def _gitbranchmap(self):
682 def _gitbranchmap(self):
683 '''returns 3 things:
683 '''returns 3 things:
684 a map from git branch to revision
684 a map from git branch to revision
685 a map from revision to branches
685 a map from revision to branches
686 a map from remote branch to local tracking branch'''
686 a map from remote branch to local tracking branch'''
687 branch2rev = {}
687 branch2rev = {}
688 rev2branch = {}
688 rev2branch = {}
689 tracking = {}
689 tracking = {}
690 out = self._gitcommand(['for-each-ref', '--format',
690 out = self._gitcommand(['for-each-ref', '--format',
691 '%(objectname) %(refname) %(upstream) end'])
691 '%(objectname) %(refname) %(upstream) end'])
692 for line in out.split('\n'):
692 for line in out.split('\n'):
693 revision, ref, upstream = line.split(' ')[:3]
693 revision, ref, upstream = line.split(' ')[:3]
694 if ref.startswith('refs/tags/'):
694 if ref.startswith('refs/tags/'):
695 continue
695 continue
696 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
696 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
697 continue # ignore remote/HEAD redirects
697 continue # ignore remote/HEAD redirects
698 branch2rev[ref] = revision
698 branch2rev[ref] = revision
699 rev2branch.setdefault(revision, []).append(ref)
699 rev2branch.setdefault(revision, []).append(ref)
700 if upstream:
700 if upstream:
701 # assumes no more than one local tracking branch for a remote
701 # assumes no more than one local tracking branch for a remote
702 tracking[upstream] = ref
702 tracking[upstream] = ref
703 return branch2rev, rev2branch, tracking
703 return branch2rev, rev2branch, tracking
704
704
705 def _fetch(self, source, revision):
705 def _fetch(self, source, revision):
706 if not os.path.exists('%s/.git' % self._path):
706 if not os.path.exists('%s/.git' % self._path):
707 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
707 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
708 self._gitnodir(['clone', source, self._path])
708 self._gitnodir(['clone', source, self._path])
709 if self._githavelocally(revision):
709 if self._githavelocally(revision):
710 return
710 return
711 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
711 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
712 # first try from origin
712 # first try from origin
713 self._gitcommand(['fetch'])
713 self._gitcommand(['fetch'])
714 if self._githavelocally(revision):
714 if self._githavelocally(revision):
715 return
715 return
716 # then try from known subrepo source
716 # then try from known subrepo source
717 self._gitcommand(['fetch', source])
717 self._gitcommand(['fetch', source])
718 if not self._githavelocally(revision):
718 if not self._githavelocally(revision):
719 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
719 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
720 (revision, self._path))
720 (revision, self._path))
721
721
722 def dirty(self):
722 def dirty(self):
723 if self._state[1] != self._gitstate(): # version checked out changed?
723 if self._state[1] != self._gitstate(): # version checked out changed?
724 return True
724 return True
725 # check for staged changes or modified files; ignore untracked files
725 # check for staged changes or modified files; ignore untracked files
726 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
726 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
727 return code == 1
727 return code == 1
728
728
729 def get(self, state):
729 def get(self, state):
730 source, revision, kind = state
730 source, revision, kind = state
731 self._fetch(source, revision)
731 self._fetch(source, revision)
732 # if the repo was set to be bare, unbare it
732 # if the repo was set to be bare, unbare it
733 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
733 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
734 self._gitcommand(['config', 'core.bare', 'false'])
734 self._gitcommand(['config', 'core.bare', 'false'])
735 if self._gitstate() == revision:
735 if self._gitstate() == revision:
736 self._gitcommand(['reset', '--hard', 'HEAD'])
736 self._gitcommand(['reset', '--hard', 'HEAD'])
737 return
737 return
738 elif self._gitstate() == revision:
738 elif self._gitstate() == revision:
739 return
739 return
740 branch2rev, rev2branch, tracking = self._gitbranchmap()
740 branch2rev, rev2branch, tracking = self._gitbranchmap()
741
741
742 def rawcheckout():
742 def rawcheckout():
743 # no branch to checkout, check it out with no branch
743 # no branch to checkout, check it out with no branch
744 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
744 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
745 self._relpath)
745 self._relpath)
746 self._ui.warn(_('check out a git branch if you intend '
746 self._ui.warn(_('check out a git branch if you intend '
747 'to make changes\n'))
747 'to make changes\n'))
748 self._gitcommand(['checkout', '-q', revision])
748 self._gitcommand(['checkout', '-q', revision])
749
749
750 if revision not in rev2branch:
750 if revision not in rev2branch:
751 rawcheckout()
751 rawcheckout()
752 return
752 return
753 branches = rev2branch[revision]
753 branches = rev2branch[revision]
754 firstlocalbranch = None
754 firstlocalbranch = None
755 for b in branches:
755 for b in branches:
756 if b == 'refs/heads/master':
756 if b == 'refs/heads/master':
757 # master trumps all other branches
757 # master trumps all other branches
758 self._gitcommand(['checkout', 'refs/heads/master'])
758 self._gitcommand(['checkout', 'refs/heads/master'])
759 return
759 return
760 if not firstlocalbranch and not b.startswith('refs/remotes/'):
760 if not firstlocalbranch and not b.startswith('refs/remotes/'):
761 firstlocalbranch = b
761 firstlocalbranch = b
762 if firstlocalbranch:
762 if firstlocalbranch:
763 self._gitcommand(['checkout', firstlocalbranch])
763 self._gitcommand(['checkout', firstlocalbranch])
764 return
764 return
765
765
766 # choose a remote branch already tracked if possible
766 # choose a remote branch already tracked if possible
767 remote = branches[0]
767 remote = branches[0]
768 if remote not in tracking:
768 if remote not in tracking:
769 for b in branches:
769 for b in branches:
770 if b in tracking:
770 if b in tracking:
771 remote = b
771 remote = b
772 break
772 break
773
773
774 if remote not in tracking:
774 if remote not in tracking:
775 # create a new local tracking branch
775 # create a new local tracking branch
776 local = remote.split('/', 2)[2]
776 local = remote.split('/', 2)[2]
777 self._gitcommand(['checkout', '-b', local, remote])
777 self._gitcommand(['checkout', '-b', local, remote])
778 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
778 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
779 # When updating to a tracked remote branch,
779 # When updating to a tracked remote branch,
780 # if the local tracking branch is downstream of it,
780 # if the local tracking branch is downstream of it,
781 # a normal `git pull` would have performed a "fast-forward merge"
781 # a normal `git pull` would have performed a "fast-forward merge"
782 # which is equivalent to updating the local branch to the remote.
782 # which is equivalent to updating the local branch to the remote.
783 # Since we are only looking at branching at update, we need to
783 # Since we are only looking at branching at update, we need to
784 # detect this situation and perform this action lazily.
784 # detect this situation and perform this action lazily.
785 if tracking[remote] != self._gitcurrentbranch():
785 if tracking[remote] != self._gitcurrentbranch():
786 self._gitcommand(['checkout', tracking[remote]])
786 self._gitcommand(['checkout', tracking[remote]])
787 self._gitcommand(['merge', '--ff', remote])
787 self._gitcommand(['merge', '--ff', remote])
788 else:
788 else:
789 # a real merge would be required, just checkout the revision
789 # a real merge would be required, just checkout the revision
790 rawcheckout()
790 rawcheckout()
791
791
792 def commit(self, text, user, date):
792 def commit(self, text, user, date):
793 cmd = ['commit', '-a', '-m', text]
793 cmd = ['commit', '-a', '-m', text]
794 env = os.environ.copy()
794 env = os.environ.copy()
795 if user:
795 if user:
796 cmd += ['--author', user]
796 cmd += ['--author', user]
797 if date:
797 if date:
798 # git's date parser silently ignores when seconds < 1e9
798 # git's date parser silently ignores when seconds < 1e9
799 # convert to ISO8601
799 # convert to ISO8601
800 env['GIT_AUTHOR_DATE'] = util.datestr(date,
800 env['GIT_AUTHOR_DATE'] = util.datestr(date,
801 '%Y-%m-%dT%H:%M:%S %1%2')
801 '%Y-%m-%dT%H:%M:%S %1%2')
802 self._gitcommand(cmd, env=env)
802 self._gitcommand(cmd, env=env)
803 # make sure commit works otherwise HEAD might not exist under certain
803 # make sure commit works otherwise HEAD might not exist under certain
804 # circumstances
804 # circumstances
805 return self._gitstate()
805 return self._gitstate()
806
806
807 def merge(self, state):
807 def merge(self, state):
808 source, revision, kind = state
808 source, revision, kind = state
809 self._fetch(source, revision)
809 self._fetch(source, revision)
810 base = self._gitcommand(['merge-base', revision, self._state[1]])
810 base = self._gitcommand(['merge-base', revision, self._state[1]])
811 if base == revision:
811 if base == revision:
812 self.get(state) # fast forward merge
812 self.get(state) # fast forward merge
813 elif base != self._state[1]:
813 elif base != self._state[1]:
814 self._gitcommand(['merge', '--no-commit', revision])
814 self._gitcommand(['merge', '--no-commit', revision])
815
815
816 def push(self, force):
816 def push(self, force):
817 # if a branch in origin contains the revision, nothing to do
817 # if a branch in origin contains the revision, nothing to do
818 branch2rev, rev2branch, tracking = self._gitbranchmap()
818 branch2rev, rev2branch, tracking = self._gitbranchmap()
819 if self._state[1] in rev2branch:
819 if self._state[1] in rev2branch:
820 for b in rev2branch[self._state[1]]:
820 for b in rev2branch[self._state[1]]:
821 if b.startswith('refs/remotes/origin/'):
821 if b.startswith('refs/remotes/origin/'):
822 return True
822 return True
823 for b, revision in branch2rev.iteritems():
823 for b, revision in branch2rev.iteritems():
824 if b.startswith('refs/remotes/origin/'):
824 if b.startswith('refs/remotes/origin/'):
825 if self._gitisancestor(self._state[1], revision):
825 if self._gitisancestor(self._state[1], revision):
826 return True
826 return True
827 # otherwise, try to push the currently checked out branch
827 # otherwise, try to push the currently checked out branch
828 cmd = ['push']
828 cmd = ['push']
829 if force:
829 if force:
830 cmd.append('--force')
830 cmd.append('--force')
831
831
832 current = self._gitcurrentbranch()
832 current = self._gitcurrentbranch()
833 if current:
833 if current:
834 # determine if the current branch is even useful
834 # determine if the current branch is even useful
835 if not self._gitisancestor(self._state[1], current):
835 if not self._gitisancestor(self._state[1], current):
836 self._ui.warn(_('unrelated git branch checked out '
836 self._ui.warn(_('unrelated git branch checked out '
837 'in subrepo %s\n') % self._relpath)
837 'in subrepo %s\n') % self._relpath)
838 return False
838 return False
839 self._ui.status(_('pushing branch %s of subrepo %s\n') %
839 self._ui.status(_('pushing branch %s of subrepo %s\n') %
840 (current.split('/', 2)[2], self._relpath))
840 (current.split('/', 2)[2], self._relpath))
841 self._gitcommand(cmd + ['origin', current])
841 self._gitcommand(cmd + ['origin', current])
842 return True
842 return True
843 else:
843 else:
844 self._ui.warn(_('no branch checked out in subrepo %s\n'
844 self._ui.warn(_('no branch checked out in subrepo %s\n'
845 'cannot push revision %s') %
845 'cannot push revision %s') %
846 (self._relpath, self._state[1]))
846 (self._relpath, self._state[1]))
847 return False
847 return False
848
848
849 def remove(self):
849 def remove(self):
850 if self.dirty():
850 if self.dirty():
851 self._ui.warn(_('not removing repo %s because '
851 self._ui.warn(_('not removing repo %s because '
852 'it has changes.\n') % self._path)
852 'it has changes.\n') % self._path)
853 return
853 return
854 # we can't fully delete the repository as it may contain
854 # we can't fully delete the repository as it may contain
855 # local-only history
855 # local-only history
856 self._ui.note(_('removing subrepo %s\n') % self._path)
856 self._ui.note(_('removing subrepo %s\n') % self._path)
857 self._gitcommand(['config', 'core.bare', 'true'])
857 self._gitcommand(['config', 'core.bare', 'true'])
858 for f in os.listdir(self._path):
858 for f in os.listdir(self._path):
859 if f == '.git':
859 if f == '.git':
860 continue
860 continue
861 path = os.path.join(self._path, f)
861 path = os.path.join(self._path, f)
862 if os.path.isdir(path) and not os.path.islink(path):
862 if os.path.isdir(path) and not os.path.islink(path):
863 shutil.rmtree(path)
863 shutil.rmtree(path)
864 else:
864 else:
865 os.remove(path)
865 os.remove(path)
866
866
867 def archive(self, ui, archiver, prefix):
867 def archive(self, ui, archiver, prefix):
868 source, revision = self._state
868 source, revision = self._state
869 self._fetch(source, revision)
869 self._fetch(source, revision)
870
870
871 # Parse git's native archive command.
871 # Parse git's native archive command.
872 # This should be much faster than manually traversing the trees
872 # This should be much faster than manually traversing the trees
873 # and objects with many subprocess calls.
873 # and objects with many subprocess calls.
874 tarstream = self._gitcommand(['archive', revision], stream=True)
874 tarstream = self._gitcommand(['archive', revision], stream=True)
875 tar = tarfile.open(fileobj=tarstream, mode='r|')
875 tar = tarfile.open(fileobj=tarstream, mode='r|')
876 relpath = subrelpath(self)
876 relpath = subrelpath(self)
877 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
877 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
878 for i, info in enumerate(tar):
878 for i, info in enumerate(tar):
879 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
879 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
880 info.mode, info.issym(),
880 info.mode, info.issym(),
881 tar.extractfile(info).read())
881 tar.extractfile(info).read())
882 ui.progress(_('archiving (%s)') % relpath, i + 1,
882 ui.progress(_('archiving (%s)') % relpath, i + 1,
883 unit=_('files'))
883 unit=_('files'))
884 ui.progress(_('archiving (%s)') % relpath, None)
884 ui.progress(_('archiving (%s)') % relpath, None)
885
885
886
886
887 types = {
887 types = {
888 'hg': hgsubrepo,
888 'hg': hgsubrepo,
889 'svn': svnsubrepo,
889 'svn': svnsubrepo,
890 'git': gitsubrepo,
890 'git': gitsubrepo,
891 }
891 }
General Comments 0
You need to be logged in to leave comments. Login now