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