##// END OF EJS Templates
parsedate: allow '' for epoch
Matt Mackall -
r3807:e43b48f0 default
parent child Browse files
Show More
@@ -1,1180 +1,1182 b''
1 """
1 """
2 util.py - Mercurial utility functions and platform specfic implementations
2 util.py - Mercurial utility functions and platform specfic implementations
3
3
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7
7
8 This software may be used and distributed according to the terms
8 This software may be used and distributed according to the terms
9 of the GNU General Public License, incorporated herein by reference.
9 of the GNU General Public License, incorporated herein by reference.
10
10
11 This contains helper routines that are independent of the SCM core and hide
11 This contains helper routines that are independent of the SCM core and hide
12 platform-specific details from the core.
12 platform-specific details from the core.
13 """
13 """
14
14
15 from i18n import gettext as _
15 from i18n import gettext as _
16 from demandload import *
16 from demandload import *
17 demandload(globals(), "cStringIO errno getpass popen2 re shutil sys tempfile")
17 demandload(globals(), "cStringIO errno getpass popen2 re shutil sys tempfile")
18 demandload(globals(), "os threading time calendar ConfigParser locale")
18 demandload(globals(), "os threading time calendar ConfigParser locale")
19
19
20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding()
20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding()
21 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
21 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
22
22
23 def tolocal(s):
23 def tolocal(s):
24 """
24 """
25 Convert a string from internal UTF-8 to local encoding
25 Convert a string from internal UTF-8 to local encoding
26
26
27 All internal strings should be UTF-8 but some repos before the
27 All internal strings should be UTF-8 but some repos before the
28 implementation of locale support may contain latin1 or possibly
28 implementation of locale support may contain latin1 or possibly
29 other character sets. We attempt to decode everything strictly
29 other character sets. We attempt to decode everything strictly
30 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
30 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
31 replace unknown characters.
31 replace unknown characters.
32 """
32 """
33 for e in "utf-8 latin1".split():
33 for e in "utf-8 latin1".split():
34 try:
34 try:
35 u = s.decode(e) # attempt strict decoding
35 u = s.decode(e) # attempt strict decoding
36 return u.encode(_encoding, "replace")
36 return u.encode(_encoding, "replace")
37 except UnicodeDecodeError:
37 except UnicodeDecodeError:
38 pass
38 pass
39 u = s.decode("utf-8", "replace") # last ditch
39 u = s.decode("utf-8", "replace") # last ditch
40 return u.encode(_encoding, "replace")
40 return u.encode(_encoding, "replace")
41
41
42 def fromlocal(s):
42 def fromlocal(s):
43 """
43 """
44 Convert a string from the local character encoding to UTF-8
44 Convert a string from the local character encoding to UTF-8
45
45
46 We attempt to decode strings using the encoding mode set by
46 We attempt to decode strings using the encoding mode set by
47 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
47 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
48 characters will cause an error message. Other modes include
48 characters will cause an error message. Other modes include
49 'replace', which replaces unknown characters with a special
49 'replace', which replaces unknown characters with a special
50 Unicode character, and 'ignore', which drops the character.
50 Unicode character, and 'ignore', which drops the character.
51 """
51 """
52 try:
52 try:
53 return s.decode(_encoding, _encodingmode).encode("utf-8")
53 return s.decode(_encoding, _encodingmode).encode("utf-8")
54 except UnicodeDecodeError, inst:
54 except UnicodeDecodeError, inst:
55 sub = s[max(0, inst.start-10):inst.start+10]
55 sub = s[max(0, inst.start-10):inst.start+10]
56 raise Abort("decoding near '%s': %s!\n" % (sub, inst))
56 raise Abort("decoding near '%s': %s!\n" % (sub, inst))
57
57
58 def locallen(s):
58 def locallen(s):
59 """Find the length in characters of a local string"""
59 """Find the length in characters of a local string"""
60 return len(s.decode(_encoding, "replace"))
60 return len(s.decode(_encoding, "replace"))
61
61
62 def localsub(s, a, b=None):
62 def localsub(s, a, b=None):
63 try:
63 try:
64 u = s.decode(_encoding, _encodingmode)
64 u = s.decode(_encoding, _encodingmode)
65 if b is not None:
65 if b is not None:
66 u = u[a:b]
66 u = u[a:b]
67 else:
67 else:
68 u = u[:a]
68 u = u[:a]
69 return u.encode(_encoding, _encodingmode)
69 return u.encode(_encoding, _encodingmode)
70 except UnicodeDecodeError, inst:
70 except UnicodeDecodeError, inst:
71 sub = s[max(0, inst.start-10), inst.start+10]
71 sub = s[max(0, inst.start-10), inst.start+10]
72 raise Abort("decoding near '%s': %s!\n" % (sub, inst))
72 raise Abort("decoding near '%s': %s!\n" % (sub, inst))
73
73
74 # used by parsedate
74 # used by parsedate
75 defaultdateformats = ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
75 defaultdateformats = ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
76 '%a %b %d %H:%M:%S %Y')
76 '%a %b %d %H:%M:%S %Y')
77
77
78 class SignalInterrupt(Exception):
78 class SignalInterrupt(Exception):
79 """Exception raised on SIGTERM and SIGHUP."""
79 """Exception raised on SIGTERM and SIGHUP."""
80
80
81 # like SafeConfigParser but with case-sensitive keys
81 # like SafeConfigParser but with case-sensitive keys
82 class configparser(ConfigParser.SafeConfigParser):
82 class configparser(ConfigParser.SafeConfigParser):
83 def optionxform(self, optionstr):
83 def optionxform(self, optionstr):
84 return optionstr
84 return optionstr
85
85
86 def cachefunc(func):
86 def cachefunc(func):
87 '''cache the result of function calls'''
87 '''cache the result of function calls'''
88 # XXX doesn't handle keywords args
88 # XXX doesn't handle keywords args
89 cache = {}
89 cache = {}
90 if func.func_code.co_argcount == 1:
90 if func.func_code.co_argcount == 1:
91 # we gain a small amount of time because
91 # we gain a small amount of time because
92 # we don't need to pack/unpack the list
92 # we don't need to pack/unpack the list
93 def f(arg):
93 def f(arg):
94 if arg not in cache:
94 if arg not in cache:
95 cache[arg] = func(arg)
95 cache[arg] = func(arg)
96 return cache[arg]
96 return cache[arg]
97 else:
97 else:
98 def f(*args):
98 def f(*args):
99 if args not in cache:
99 if args not in cache:
100 cache[args] = func(*args)
100 cache[args] = func(*args)
101 return cache[args]
101 return cache[args]
102
102
103 return f
103 return f
104
104
105 def pipefilter(s, cmd):
105 def pipefilter(s, cmd):
106 '''filter string S through command CMD, returning its output'''
106 '''filter string S through command CMD, returning its output'''
107 (pout, pin) = popen2.popen2(cmd, -1, 'b')
107 (pout, pin) = popen2.popen2(cmd, -1, 'b')
108 def writer():
108 def writer():
109 try:
109 try:
110 pin.write(s)
110 pin.write(s)
111 pin.close()
111 pin.close()
112 except IOError, inst:
112 except IOError, inst:
113 if inst.errno != errno.EPIPE:
113 if inst.errno != errno.EPIPE:
114 raise
114 raise
115
115
116 # we should use select instead on UNIX, but this will work on most
116 # we should use select instead on UNIX, but this will work on most
117 # systems, including Windows
117 # systems, including Windows
118 w = threading.Thread(target=writer)
118 w = threading.Thread(target=writer)
119 w.start()
119 w.start()
120 f = pout.read()
120 f = pout.read()
121 pout.close()
121 pout.close()
122 w.join()
122 w.join()
123 return f
123 return f
124
124
125 def tempfilter(s, cmd):
125 def tempfilter(s, cmd):
126 '''filter string S through a pair of temporary files with CMD.
126 '''filter string S through a pair of temporary files with CMD.
127 CMD is used as a template to create the real command to be run,
127 CMD is used as a template to create the real command to be run,
128 with the strings INFILE and OUTFILE replaced by the real names of
128 with the strings INFILE and OUTFILE replaced by the real names of
129 the temporary files generated.'''
129 the temporary files generated.'''
130 inname, outname = None, None
130 inname, outname = None, None
131 try:
131 try:
132 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
132 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
133 fp = os.fdopen(infd, 'wb')
133 fp = os.fdopen(infd, 'wb')
134 fp.write(s)
134 fp.write(s)
135 fp.close()
135 fp.close()
136 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
136 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
137 os.close(outfd)
137 os.close(outfd)
138 cmd = cmd.replace('INFILE', inname)
138 cmd = cmd.replace('INFILE', inname)
139 cmd = cmd.replace('OUTFILE', outname)
139 cmd = cmd.replace('OUTFILE', outname)
140 code = os.system(cmd)
140 code = os.system(cmd)
141 if code: raise Abort(_("command '%s' failed: %s") %
141 if code: raise Abort(_("command '%s' failed: %s") %
142 (cmd, explain_exit(code)))
142 (cmd, explain_exit(code)))
143 return open(outname, 'rb').read()
143 return open(outname, 'rb').read()
144 finally:
144 finally:
145 try:
145 try:
146 if inname: os.unlink(inname)
146 if inname: os.unlink(inname)
147 except: pass
147 except: pass
148 try:
148 try:
149 if outname: os.unlink(outname)
149 if outname: os.unlink(outname)
150 except: pass
150 except: pass
151
151
152 filtertable = {
152 filtertable = {
153 'tempfile:': tempfilter,
153 'tempfile:': tempfilter,
154 'pipe:': pipefilter,
154 'pipe:': pipefilter,
155 }
155 }
156
156
157 def filter(s, cmd):
157 def filter(s, cmd):
158 "filter a string through a command that transforms its input to its output"
158 "filter a string through a command that transforms its input to its output"
159 for name, fn in filtertable.iteritems():
159 for name, fn in filtertable.iteritems():
160 if cmd.startswith(name):
160 if cmd.startswith(name):
161 return fn(s, cmd[len(name):].lstrip())
161 return fn(s, cmd[len(name):].lstrip())
162 return pipefilter(s, cmd)
162 return pipefilter(s, cmd)
163
163
164 def find_in_path(name, path, default=None):
164 def find_in_path(name, path, default=None):
165 '''find name in search path. path can be string (will be split
165 '''find name in search path. path can be string (will be split
166 with os.pathsep), or iterable thing that returns strings. if name
166 with os.pathsep), or iterable thing that returns strings. if name
167 found, return path to name. else return default.'''
167 found, return path to name. else return default.'''
168 if isinstance(path, str):
168 if isinstance(path, str):
169 path = path.split(os.pathsep)
169 path = path.split(os.pathsep)
170 for p in path:
170 for p in path:
171 p_name = os.path.join(p, name)
171 p_name = os.path.join(p, name)
172 if os.path.exists(p_name):
172 if os.path.exists(p_name):
173 return p_name
173 return p_name
174 return default
174 return default
175
175
176 def binary(s):
176 def binary(s):
177 """return true if a string is binary data using diff's heuristic"""
177 """return true if a string is binary data using diff's heuristic"""
178 if s and '\0' in s[:4096]:
178 if s and '\0' in s[:4096]:
179 return True
179 return True
180 return False
180 return False
181
181
182 def unique(g):
182 def unique(g):
183 """return the uniq elements of iterable g"""
183 """return the uniq elements of iterable g"""
184 seen = {}
184 seen = {}
185 l = []
185 l = []
186 for f in g:
186 for f in g:
187 if f not in seen:
187 if f not in seen:
188 seen[f] = 1
188 seen[f] = 1
189 l.append(f)
189 l.append(f)
190 return l
190 return l
191
191
192 class Abort(Exception):
192 class Abort(Exception):
193 """Raised if a command needs to print an error and exit."""
193 """Raised if a command needs to print an error and exit."""
194
194
195 class UnexpectedOutput(Abort):
195 class UnexpectedOutput(Abort):
196 """Raised to print an error with part of output and exit."""
196 """Raised to print an error with part of output and exit."""
197
197
198 def always(fn): return True
198 def always(fn): return True
199 def never(fn): return False
199 def never(fn): return False
200
200
201 def patkind(name, dflt_pat='glob'):
201 def patkind(name, dflt_pat='glob'):
202 """Split a string into an optional pattern kind prefix and the
202 """Split a string into an optional pattern kind prefix and the
203 actual pattern."""
203 actual pattern."""
204 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
204 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
205 if name.startswith(prefix + ':'): return name.split(':', 1)
205 if name.startswith(prefix + ':'): return name.split(':', 1)
206 return dflt_pat, name
206 return dflt_pat, name
207
207
208 def globre(pat, head='^', tail='$'):
208 def globre(pat, head='^', tail='$'):
209 "convert a glob pattern into a regexp"
209 "convert a glob pattern into a regexp"
210 i, n = 0, len(pat)
210 i, n = 0, len(pat)
211 res = ''
211 res = ''
212 group = False
212 group = False
213 def peek(): return i < n and pat[i]
213 def peek(): return i < n and pat[i]
214 while i < n:
214 while i < n:
215 c = pat[i]
215 c = pat[i]
216 i = i+1
216 i = i+1
217 if c == '*':
217 if c == '*':
218 if peek() == '*':
218 if peek() == '*':
219 i += 1
219 i += 1
220 res += '.*'
220 res += '.*'
221 else:
221 else:
222 res += '[^/]*'
222 res += '[^/]*'
223 elif c == '?':
223 elif c == '?':
224 res += '.'
224 res += '.'
225 elif c == '[':
225 elif c == '[':
226 j = i
226 j = i
227 if j < n and pat[j] in '!]':
227 if j < n and pat[j] in '!]':
228 j += 1
228 j += 1
229 while j < n and pat[j] != ']':
229 while j < n and pat[j] != ']':
230 j += 1
230 j += 1
231 if j >= n:
231 if j >= n:
232 res += '\\['
232 res += '\\['
233 else:
233 else:
234 stuff = pat[i:j].replace('\\','\\\\')
234 stuff = pat[i:j].replace('\\','\\\\')
235 i = j + 1
235 i = j + 1
236 if stuff[0] == '!':
236 if stuff[0] == '!':
237 stuff = '^' + stuff[1:]
237 stuff = '^' + stuff[1:]
238 elif stuff[0] == '^':
238 elif stuff[0] == '^':
239 stuff = '\\' + stuff
239 stuff = '\\' + stuff
240 res = '%s[%s]' % (res, stuff)
240 res = '%s[%s]' % (res, stuff)
241 elif c == '{':
241 elif c == '{':
242 group = True
242 group = True
243 res += '(?:'
243 res += '(?:'
244 elif c == '}' and group:
244 elif c == '}' and group:
245 res += ')'
245 res += ')'
246 group = False
246 group = False
247 elif c == ',' and group:
247 elif c == ',' and group:
248 res += '|'
248 res += '|'
249 elif c == '\\':
249 elif c == '\\':
250 p = peek()
250 p = peek()
251 if p:
251 if p:
252 i += 1
252 i += 1
253 res += re.escape(p)
253 res += re.escape(p)
254 else:
254 else:
255 res += re.escape(c)
255 res += re.escape(c)
256 else:
256 else:
257 res += re.escape(c)
257 res += re.escape(c)
258 return head + res + tail
258 return head + res + tail
259
259
260 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
260 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
261
261
262 def pathto(n1, n2):
262 def pathto(n1, n2):
263 '''return the relative path from one place to another.
263 '''return the relative path from one place to another.
264 n1 should use os.sep to separate directories
264 n1 should use os.sep to separate directories
265 n2 should use "/" to separate directories
265 n2 should use "/" to separate directories
266 returns an os.sep-separated path.
266 returns an os.sep-separated path.
267 '''
267 '''
268 if not n1: return localpath(n2)
268 if not n1: return localpath(n2)
269 a, b = n1.split(os.sep), n2.split('/')
269 a, b = n1.split(os.sep), n2.split('/')
270 a.reverse()
270 a.reverse()
271 b.reverse()
271 b.reverse()
272 while a and b and a[-1] == b[-1]:
272 while a and b and a[-1] == b[-1]:
273 a.pop()
273 a.pop()
274 b.pop()
274 b.pop()
275 b.reverse()
275 b.reverse()
276 return os.sep.join((['..'] * len(a)) + b)
276 return os.sep.join((['..'] * len(a)) + b)
277
277
278 def canonpath(root, cwd, myname):
278 def canonpath(root, cwd, myname):
279 """return the canonical path of myname, given cwd and root"""
279 """return the canonical path of myname, given cwd and root"""
280 if root == os.sep:
280 if root == os.sep:
281 rootsep = os.sep
281 rootsep = os.sep
282 elif root.endswith(os.sep):
282 elif root.endswith(os.sep):
283 rootsep = root
283 rootsep = root
284 else:
284 else:
285 rootsep = root + os.sep
285 rootsep = root + os.sep
286 name = myname
286 name = myname
287 if not os.path.isabs(name):
287 if not os.path.isabs(name):
288 name = os.path.join(root, cwd, name)
288 name = os.path.join(root, cwd, name)
289 name = os.path.normpath(name)
289 name = os.path.normpath(name)
290 if name != rootsep and name.startswith(rootsep):
290 if name != rootsep and name.startswith(rootsep):
291 name = name[len(rootsep):]
291 name = name[len(rootsep):]
292 audit_path(name)
292 audit_path(name)
293 return pconvert(name)
293 return pconvert(name)
294 elif name == root:
294 elif name == root:
295 return ''
295 return ''
296 else:
296 else:
297 # Determine whether `name' is in the hierarchy at or beneath `root',
297 # Determine whether `name' is in the hierarchy at or beneath `root',
298 # by iterating name=dirname(name) until that causes no change (can't
298 # by iterating name=dirname(name) until that causes no change (can't
299 # check name == '/', because that doesn't work on windows). For each
299 # check name == '/', because that doesn't work on windows). For each
300 # `name', compare dev/inode numbers. If they match, the list `rel'
300 # `name', compare dev/inode numbers. If they match, the list `rel'
301 # holds the reversed list of components making up the relative file
301 # holds the reversed list of components making up the relative file
302 # name we want.
302 # name we want.
303 root_st = os.stat(root)
303 root_st = os.stat(root)
304 rel = []
304 rel = []
305 while True:
305 while True:
306 try:
306 try:
307 name_st = os.stat(name)
307 name_st = os.stat(name)
308 except OSError:
308 except OSError:
309 break
309 break
310 if samestat(name_st, root_st):
310 if samestat(name_st, root_st):
311 rel.reverse()
311 rel.reverse()
312 name = os.path.join(*rel)
312 name = os.path.join(*rel)
313 audit_path(name)
313 audit_path(name)
314 return pconvert(name)
314 return pconvert(name)
315 dirname, basename = os.path.split(name)
315 dirname, basename = os.path.split(name)
316 rel.append(basename)
316 rel.append(basename)
317 if dirname == name:
317 if dirname == name:
318 break
318 break
319 name = dirname
319 name = dirname
320
320
321 raise Abort('%s not under root' % myname)
321 raise Abort('%s not under root' % myname)
322
322
323 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
323 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
324 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
324 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
325
325
326 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
326 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
327 if os.name == 'nt':
327 if os.name == 'nt':
328 dflt_pat = 'glob'
328 dflt_pat = 'glob'
329 else:
329 else:
330 dflt_pat = 'relpath'
330 dflt_pat = 'relpath'
331 return _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src)
331 return _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src)
332
332
333 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
333 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
334 """build a function to match a set of file patterns
334 """build a function to match a set of file patterns
335
335
336 arguments:
336 arguments:
337 canonroot - the canonical root of the tree you're matching against
337 canonroot - the canonical root of the tree you're matching against
338 cwd - the current working directory, if relevant
338 cwd - the current working directory, if relevant
339 names - patterns to find
339 names - patterns to find
340 inc - patterns to include
340 inc - patterns to include
341 exc - patterns to exclude
341 exc - patterns to exclude
342 head - a regex to prepend to patterns to control whether a match is rooted
342 head - a regex to prepend to patterns to control whether a match is rooted
343
343
344 a pattern is one of:
344 a pattern is one of:
345 'glob:<rooted glob>'
345 'glob:<rooted glob>'
346 're:<rooted regexp>'
346 're:<rooted regexp>'
347 'path:<rooted path>'
347 'path:<rooted path>'
348 'relglob:<relative glob>'
348 'relglob:<relative glob>'
349 'relpath:<relative path>'
349 'relpath:<relative path>'
350 'relre:<relative regexp>'
350 'relre:<relative regexp>'
351 '<rooted path or regexp>'
351 '<rooted path or regexp>'
352
352
353 returns:
353 returns:
354 a 3-tuple containing
354 a 3-tuple containing
355 - list of explicit non-pattern names passed in
355 - list of explicit non-pattern names passed in
356 - a bool match(filename) function
356 - a bool match(filename) function
357 - a bool indicating if any patterns were passed in
357 - a bool indicating if any patterns were passed in
358
358
359 todo:
359 todo:
360 make head regex a rooted bool
360 make head regex a rooted bool
361 """
361 """
362
362
363 def contains_glob(name):
363 def contains_glob(name):
364 for c in name:
364 for c in name:
365 if c in _globchars: return True
365 if c in _globchars: return True
366 return False
366 return False
367
367
368 def regex(kind, name, tail):
368 def regex(kind, name, tail):
369 '''convert a pattern into a regular expression'''
369 '''convert a pattern into a regular expression'''
370 if kind == 're':
370 if kind == 're':
371 return name
371 return name
372 elif kind == 'path':
372 elif kind == 'path':
373 return '^' + re.escape(name) + '(?:/|$)'
373 return '^' + re.escape(name) + '(?:/|$)'
374 elif kind == 'relglob':
374 elif kind == 'relglob':
375 return head + globre(name, '(?:|.*/)', tail)
375 return head + globre(name, '(?:|.*/)', tail)
376 elif kind == 'relpath':
376 elif kind == 'relpath':
377 return head + re.escape(name) + tail
377 return head + re.escape(name) + tail
378 elif kind == 'relre':
378 elif kind == 'relre':
379 if name.startswith('^'):
379 if name.startswith('^'):
380 return name
380 return name
381 return '.*' + name
381 return '.*' + name
382 return head + globre(name, '', tail)
382 return head + globre(name, '', tail)
383
383
384 def matchfn(pats, tail):
384 def matchfn(pats, tail):
385 """build a matching function from a set of patterns"""
385 """build a matching function from a set of patterns"""
386 if not pats:
386 if not pats:
387 return
387 return
388 matches = []
388 matches = []
389 for k, p in pats:
389 for k, p in pats:
390 try:
390 try:
391 pat = '(?:%s)' % regex(k, p, tail)
391 pat = '(?:%s)' % regex(k, p, tail)
392 matches.append(re.compile(pat).match)
392 matches.append(re.compile(pat).match)
393 except re.error:
393 except re.error:
394 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
394 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
395 else: raise Abort("invalid pattern (%s): %s" % (k, p))
395 else: raise Abort("invalid pattern (%s): %s" % (k, p))
396
396
397 def buildfn(text):
397 def buildfn(text):
398 for m in matches:
398 for m in matches:
399 r = m(text)
399 r = m(text)
400 if r:
400 if r:
401 return r
401 return r
402
402
403 return buildfn
403 return buildfn
404
404
405 def globprefix(pat):
405 def globprefix(pat):
406 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
406 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
407 root = []
407 root = []
408 for p in pat.split(os.sep):
408 for p in pat.split(os.sep):
409 if contains_glob(p): break
409 if contains_glob(p): break
410 root.append(p)
410 root.append(p)
411 return '/'.join(root)
411 return '/'.join(root)
412
412
413 pats = []
413 pats = []
414 files = []
414 files = []
415 roots = []
415 roots = []
416 for kind, name in [patkind(p, dflt_pat) for p in names]:
416 for kind, name in [patkind(p, dflt_pat) for p in names]:
417 if kind in ('glob', 'relpath'):
417 if kind in ('glob', 'relpath'):
418 name = canonpath(canonroot, cwd, name)
418 name = canonpath(canonroot, cwd, name)
419 if name == '':
419 if name == '':
420 kind, name = 'glob', '**'
420 kind, name = 'glob', '**'
421 if kind in ('glob', 'path', 're'):
421 if kind in ('glob', 'path', 're'):
422 pats.append((kind, name))
422 pats.append((kind, name))
423 if kind == 'glob':
423 if kind == 'glob':
424 root = globprefix(name)
424 root = globprefix(name)
425 if root: roots.append(root)
425 if root: roots.append(root)
426 elif kind == 'relpath':
426 elif kind == 'relpath':
427 files.append((kind, name))
427 files.append((kind, name))
428 roots.append(name)
428 roots.append(name)
429
429
430 patmatch = matchfn(pats, '$') or always
430 patmatch = matchfn(pats, '$') or always
431 filematch = matchfn(files, '(?:/|$)') or always
431 filematch = matchfn(files, '(?:/|$)') or always
432 incmatch = always
432 incmatch = always
433 if inc:
433 if inc:
434 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
434 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
435 incmatch = matchfn(inckinds, '(?:/|$)')
435 incmatch = matchfn(inckinds, '(?:/|$)')
436 excmatch = lambda fn: False
436 excmatch = lambda fn: False
437 if exc:
437 if exc:
438 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
438 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
439 excmatch = matchfn(exckinds, '(?:/|$)')
439 excmatch = matchfn(exckinds, '(?:/|$)')
440
440
441 return (roots,
441 return (roots,
442 lambda fn: (incmatch(fn) and not excmatch(fn) and
442 lambda fn: (incmatch(fn) and not excmatch(fn) and
443 (fn.endswith('/') or
443 (fn.endswith('/') or
444 (not pats and not files) or
444 (not pats and not files) or
445 (pats and patmatch(fn)) or
445 (pats and patmatch(fn)) or
446 (files and filematch(fn)))),
446 (files and filematch(fn)))),
447 (inc or exc or (pats and pats != [('glob', '**')])) and True)
447 (inc or exc or (pats and pats != [('glob', '**')])) and True)
448
448
449 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
449 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
450 '''enhanced shell command execution.
450 '''enhanced shell command execution.
451 run with environment maybe modified, maybe in different dir.
451 run with environment maybe modified, maybe in different dir.
452
452
453 if command fails and onerr is None, return status. if ui object,
453 if command fails and onerr is None, return status. if ui object,
454 print error message and return status, else raise onerr object as
454 print error message and return status, else raise onerr object as
455 exception.'''
455 exception.'''
456 def py2shell(val):
456 def py2shell(val):
457 'convert python object into string that is useful to shell'
457 'convert python object into string that is useful to shell'
458 if val in (None, False):
458 if val in (None, False):
459 return '0'
459 return '0'
460 if val == True:
460 if val == True:
461 return '1'
461 return '1'
462 return str(val)
462 return str(val)
463 oldenv = {}
463 oldenv = {}
464 for k in environ:
464 for k in environ:
465 oldenv[k] = os.environ.get(k)
465 oldenv[k] = os.environ.get(k)
466 if cwd is not None:
466 if cwd is not None:
467 oldcwd = os.getcwd()
467 oldcwd = os.getcwd()
468 try:
468 try:
469 for k, v in environ.iteritems():
469 for k, v in environ.iteritems():
470 os.environ[k] = py2shell(v)
470 os.environ[k] = py2shell(v)
471 if cwd is not None and oldcwd != cwd:
471 if cwd is not None and oldcwd != cwd:
472 os.chdir(cwd)
472 os.chdir(cwd)
473 rc = os.system(cmd)
473 rc = os.system(cmd)
474 if rc and onerr:
474 if rc and onerr:
475 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
475 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
476 explain_exit(rc)[0])
476 explain_exit(rc)[0])
477 if errprefix:
477 if errprefix:
478 errmsg = '%s: %s' % (errprefix, errmsg)
478 errmsg = '%s: %s' % (errprefix, errmsg)
479 try:
479 try:
480 onerr.warn(errmsg + '\n')
480 onerr.warn(errmsg + '\n')
481 except AttributeError:
481 except AttributeError:
482 raise onerr(errmsg)
482 raise onerr(errmsg)
483 return rc
483 return rc
484 finally:
484 finally:
485 for k, v in oldenv.iteritems():
485 for k, v in oldenv.iteritems():
486 if v is None:
486 if v is None:
487 del os.environ[k]
487 del os.environ[k]
488 else:
488 else:
489 os.environ[k] = v
489 os.environ[k] = v
490 if cwd is not None and oldcwd != cwd:
490 if cwd is not None and oldcwd != cwd:
491 os.chdir(oldcwd)
491 os.chdir(oldcwd)
492
492
493 def rename(src, dst):
493 def rename(src, dst):
494 """forcibly rename a file"""
494 """forcibly rename a file"""
495 try:
495 try:
496 os.rename(src, dst)
496 os.rename(src, dst)
497 except OSError, err:
497 except OSError, err:
498 # on windows, rename to existing file is not allowed, so we
498 # on windows, rename to existing file is not allowed, so we
499 # must delete destination first. but if file is open, unlink
499 # must delete destination first. but if file is open, unlink
500 # schedules it for delete but does not delete it. rename
500 # schedules it for delete but does not delete it. rename
501 # happens immediately even for open files, so we create
501 # happens immediately even for open files, so we create
502 # temporary file, delete it, rename destination to that name,
502 # temporary file, delete it, rename destination to that name,
503 # then delete that. then rename is safe to do.
503 # then delete that. then rename is safe to do.
504 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
504 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
505 os.close(fd)
505 os.close(fd)
506 os.unlink(temp)
506 os.unlink(temp)
507 os.rename(dst, temp)
507 os.rename(dst, temp)
508 os.unlink(temp)
508 os.unlink(temp)
509 os.rename(src, dst)
509 os.rename(src, dst)
510
510
511 def unlink(f):
511 def unlink(f):
512 """unlink and remove the directory if it is empty"""
512 """unlink and remove the directory if it is empty"""
513 os.unlink(f)
513 os.unlink(f)
514 # try removing directories that might now be empty
514 # try removing directories that might now be empty
515 try:
515 try:
516 os.removedirs(os.path.dirname(f))
516 os.removedirs(os.path.dirname(f))
517 except OSError:
517 except OSError:
518 pass
518 pass
519
519
520 def copyfile(src, dest):
520 def copyfile(src, dest):
521 "copy a file, preserving mode"
521 "copy a file, preserving mode"
522 try:
522 try:
523 shutil.copyfile(src, dest)
523 shutil.copyfile(src, dest)
524 shutil.copymode(src, dest)
524 shutil.copymode(src, dest)
525 except shutil.Error, inst:
525 except shutil.Error, inst:
526 raise util.Abort(str(inst))
526 raise util.Abort(str(inst))
527
527
528 def copyfiles(src, dst, hardlink=None):
528 def copyfiles(src, dst, hardlink=None):
529 """Copy a directory tree using hardlinks if possible"""
529 """Copy a directory tree using hardlinks if possible"""
530
530
531 if hardlink is None:
531 if hardlink is None:
532 hardlink = (os.stat(src).st_dev ==
532 hardlink = (os.stat(src).st_dev ==
533 os.stat(os.path.dirname(dst)).st_dev)
533 os.stat(os.path.dirname(dst)).st_dev)
534
534
535 if os.path.isdir(src):
535 if os.path.isdir(src):
536 os.mkdir(dst)
536 os.mkdir(dst)
537 for name in os.listdir(src):
537 for name in os.listdir(src):
538 srcname = os.path.join(src, name)
538 srcname = os.path.join(src, name)
539 dstname = os.path.join(dst, name)
539 dstname = os.path.join(dst, name)
540 copyfiles(srcname, dstname, hardlink)
540 copyfiles(srcname, dstname, hardlink)
541 else:
541 else:
542 if hardlink:
542 if hardlink:
543 try:
543 try:
544 os_link(src, dst)
544 os_link(src, dst)
545 except (IOError, OSError):
545 except (IOError, OSError):
546 hardlink = False
546 hardlink = False
547 shutil.copy(src, dst)
547 shutil.copy(src, dst)
548 else:
548 else:
549 shutil.copy(src, dst)
549 shutil.copy(src, dst)
550
550
551 def audit_path(path):
551 def audit_path(path):
552 """Abort if path contains dangerous components"""
552 """Abort if path contains dangerous components"""
553 parts = os.path.normcase(path).split(os.sep)
553 parts = os.path.normcase(path).split(os.sep)
554 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
554 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
555 or os.pardir in parts):
555 or os.pardir in parts):
556 raise Abort(_("path contains illegal component: %s\n") % path)
556 raise Abort(_("path contains illegal component: %s\n") % path)
557
557
558 def _makelock_file(info, pathname):
558 def _makelock_file(info, pathname):
559 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
559 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
560 os.write(ld, info)
560 os.write(ld, info)
561 os.close(ld)
561 os.close(ld)
562
562
563 def _readlock_file(pathname):
563 def _readlock_file(pathname):
564 return posixfile(pathname).read()
564 return posixfile(pathname).read()
565
565
566 def nlinks(pathname):
566 def nlinks(pathname):
567 """Return number of hardlinks for the given file."""
567 """Return number of hardlinks for the given file."""
568 return os.lstat(pathname).st_nlink
568 return os.lstat(pathname).st_nlink
569
569
570 if hasattr(os, 'link'):
570 if hasattr(os, 'link'):
571 os_link = os.link
571 os_link = os.link
572 else:
572 else:
573 def os_link(src, dst):
573 def os_link(src, dst):
574 raise OSError(0, _("Hardlinks not supported"))
574 raise OSError(0, _("Hardlinks not supported"))
575
575
576 def fstat(fp):
576 def fstat(fp):
577 '''stat file object that may not have fileno method.'''
577 '''stat file object that may not have fileno method.'''
578 try:
578 try:
579 return os.fstat(fp.fileno())
579 return os.fstat(fp.fileno())
580 except AttributeError:
580 except AttributeError:
581 return os.stat(fp.name)
581 return os.stat(fp.name)
582
582
583 posixfile = file
583 posixfile = file
584
584
585 def is_win_9x():
585 def is_win_9x():
586 '''return true if run on windows 95, 98 or me.'''
586 '''return true if run on windows 95, 98 or me.'''
587 try:
587 try:
588 return sys.getwindowsversion()[3] == 1
588 return sys.getwindowsversion()[3] == 1
589 except AttributeError:
589 except AttributeError:
590 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
590 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
591
591
592 getuser_fallback = None
592 getuser_fallback = None
593
593
594 def getuser():
594 def getuser():
595 '''return name of current user'''
595 '''return name of current user'''
596 try:
596 try:
597 return getpass.getuser()
597 return getpass.getuser()
598 except ImportError:
598 except ImportError:
599 # import of pwd will fail on windows - try fallback
599 # import of pwd will fail on windows - try fallback
600 if getuser_fallback:
600 if getuser_fallback:
601 return getuser_fallback()
601 return getuser_fallback()
602 # raised if win32api not available
602 # raised if win32api not available
603 raise Abort(_('user name not available - set USERNAME '
603 raise Abort(_('user name not available - set USERNAME '
604 'environment variable'))
604 'environment variable'))
605
605
606 def username(uid=None):
606 def username(uid=None):
607 """Return the name of the user with the given uid.
607 """Return the name of the user with the given uid.
608
608
609 If uid is None, return the name of the current user."""
609 If uid is None, return the name of the current user."""
610 try:
610 try:
611 import pwd
611 import pwd
612 if uid is None:
612 if uid is None:
613 uid = os.getuid()
613 uid = os.getuid()
614 try:
614 try:
615 return pwd.getpwuid(uid)[0]
615 return pwd.getpwuid(uid)[0]
616 except KeyError:
616 except KeyError:
617 return str(uid)
617 return str(uid)
618 except ImportError:
618 except ImportError:
619 return None
619 return None
620
620
621 def groupname(gid=None):
621 def groupname(gid=None):
622 """Return the name of the group with the given gid.
622 """Return the name of the group with the given gid.
623
623
624 If gid is None, return the name of the current group."""
624 If gid is None, return the name of the current group."""
625 try:
625 try:
626 import grp
626 import grp
627 if gid is None:
627 if gid is None:
628 gid = os.getgid()
628 gid = os.getgid()
629 try:
629 try:
630 return grp.getgrgid(gid)[0]
630 return grp.getgrgid(gid)[0]
631 except KeyError:
631 except KeyError:
632 return str(gid)
632 return str(gid)
633 except ImportError:
633 except ImportError:
634 return None
634 return None
635
635
636 # File system features
636 # File system features
637
637
638 def checkfolding(path):
638 def checkfolding(path):
639 """
639 """
640 Check whether the given path is on a case-sensitive filesystem
640 Check whether the given path is on a case-sensitive filesystem
641
641
642 Requires a path (like /foo/.hg) ending with a foldable final
642 Requires a path (like /foo/.hg) ending with a foldable final
643 directory component.
643 directory component.
644 """
644 """
645 s1 = os.stat(path)
645 s1 = os.stat(path)
646 d, b = os.path.split(path)
646 d, b = os.path.split(path)
647 p2 = os.path.join(d, b.upper())
647 p2 = os.path.join(d, b.upper())
648 if path == p2:
648 if path == p2:
649 p2 = os.path.join(d, b.lower())
649 p2 = os.path.join(d, b.lower())
650 try:
650 try:
651 s2 = os.stat(p2)
651 s2 = os.stat(p2)
652 if s2 == s1:
652 if s2 == s1:
653 return False
653 return False
654 return True
654 return True
655 except:
655 except:
656 return True
656 return True
657
657
658 # Platform specific variants
658 # Platform specific variants
659 if os.name == 'nt':
659 if os.name == 'nt':
660 demandload(globals(), "msvcrt")
660 demandload(globals(), "msvcrt")
661 nulldev = 'NUL:'
661 nulldev = 'NUL:'
662
662
663 class winstdout:
663 class winstdout:
664 '''stdout on windows misbehaves if sent through a pipe'''
664 '''stdout on windows misbehaves if sent through a pipe'''
665
665
666 def __init__(self, fp):
666 def __init__(self, fp):
667 self.fp = fp
667 self.fp = fp
668
668
669 def __getattr__(self, key):
669 def __getattr__(self, key):
670 return getattr(self.fp, key)
670 return getattr(self.fp, key)
671
671
672 def close(self):
672 def close(self):
673 try:
673 try:
674 self.fp.close()
674 self.fp.close()
675 except: pass
675 except: pass
676
676
677 def write(self, s):
677 def write(self, s):
678 try:
678 try:
679 return self.fp.write(s)
679 return self.fp.write(s)
680 except IOError, inst:
680 except IOError, inst:
681 if inst.errno != 0: raise
681 if inst.errno != 0: raise
682 self.close()
682 self.close()
683 raise IOError(errno.EPIPE, 'Broken pipe')
683 raise IOError(errno.EPIPE, 'Broken pipe')
684
684
685 sys.stdout = winstdout(sys.stdout)
685 sys.stdout = winstdout(sys.stdout)
686
686
687 def system_rcpath():
687 def system_rcpath():
688 try:
688 try:
689 return system_rcpath_win32()
689 return system_rcpath_win32()
690 except:
690 except:
691 return [r'c:\mercurial\mercurial.ini']
691 return [r'c:\mercurial\mercurial.ini']
692
692
693 def os_rcpath():
693 def os_rcpath():
694 '''return default os-specific hgrc search path'''
694 '''return default os-specific hgrc search path'''
695 path = system_rcpath()
695 path = system_rcpath()
696 path.append(user_rcpath())
696 path.append(user_rcpath())
697 userprofile = os.environ.get('USERPROFILE')
697 userprofile = os.environ.get('USERPROFILE')
698 if userprofile:
698 if userprofile:
699 path.append(os.path.join(userprofile, 'mercurial.ini'))
699 path.append(os.path.join(userprofile, 'mercurial.ini'))
700 return path
700 return path
701
701
702 def user_rcpath():
702 def user_rcpath():
703 '''return os-specific hgrc search path to the user dir'''
703 '''return os-specific hgrc search path to the user dir'''
704 return os.path.join(os.path.expanduser('~'), 'mercurial.ini')
704 return os.path.join(os.path.expanduser('~'), 'mercurial.ini')
705
705
706 def parse_patch_output(output_line):
706 def parse_patch_output(output_line):
707 """parses the output produced by patch and returns the file name"""
707 """parses the output produced by patch and returns the file name"""
708 pf = output_line[14:]
708 pf = output_line[14:]
709 if pf[0] == '`':
709 if pf[0] == '`':
710 pf = pf[1:-1] # Remove the quotes
710 pf = pf[1:-1] # Remove the quotes
711 return pf
711 return pf
712
712
713 def testpid(pid):
713 def testpid(pid):
714 '''return False if pid dead, True if running or not known'''
714 '''return False if pid dead, True if running or not known'''
715 return True
715 return True
716
716
717 def is_exec(f, last):
717 def is_exec(f, last):
718 return last
718 return last
719
719
720 def set_exec(f, mode):
720 def set_exec(f, mode):
721 pass
721 pass
722
722
723 def set_binary(fd):
723 def set_binary(fd):
724 msvcrt.setmode(fd.fileno(), os.O_BINARY)
724 msvcrt.setmode(fd.fileno(), os.O_BINARY)
725
725
726 def pconvert(path):
726 def pconvert(path):
727 return path.replace("\\", "/")
727 return path.replace("\\", "/")
728
728
729 def localpath(path):
729 def localpath(path):
730 return path.replace('/', '\\')
730 return path.replace('/', '\\')
731
731
732 def normpath(path):
732 def normpath(path):
733 return pconvert(os.path.normpath(path))
733 return pconvert(os.path.normpath(path))
734
734
735 makelock = _makelock_file
735 makelock = _makelock_file
736 readlock = _readlock_file
736 readlock = _readlock_file
737
737
738 def samestat(s1, s2):
738 def samestat(s1, s2):
739 return False
739 return False
740
740
741 def shellquote(s):
741 def shellquote(s):
742 return '"%s"' % s.replace('"', '\\"')
742 return '"%s"' % s.replace('"', '\\"')
743
743
744 def explain_exit(code):
744 def explain_exit(code):
745 return _("exited with status %d") % code, code
745 return _("exited with status %d") % code, code
746
746
747 # if you change this stub into a real check, please try to implement the
747 # if you change this stub into a real check, please try to implement the
748 # username and groupname functions above, too.
748 # username and groupname functions above, too.
749 def isowner(fp, st=None):
749 def isowner(fp, st=None):
750 return True
750 return True
751
751
752 try:
752 try:
753 # override functions with win32 versions if possible
753 # override functions with win32 versions if possible
754 from util_win32 import *
754 from util_win32 import *
755 if not is_win_9x():
755 if not is_win_9x():
756 posixfile = posixfile_nt
756 posixfile = posixfile_nt
757 except ImportError:
757 except ImportError:
758 pass
758 pass
759
759
760 else:
760 else:
761 nulldev = '/dev/null'
761 nulldev = '/dev/null'
762
762
763 def rcfiles(path):
763 def rcfiles(path):
764 rcs = [os.path.join(path, 'hgrc')]
764 rcs = [os.path.join(path, 'hgrc')]
765 rcdir = os.path.join(path, 'hgrc.d')
765 rcdir = os.path.join(path, 'hgrc.d')
766 try:
766 try:
767 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
767 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
768 if f.endswith(".rc")])
768 if f.endswith(".rc")])
769 except OSError:
769 except OSError:
770 pass
770 pass
771 return rcs
771 return rcs
772
772
773 def os_rcpath():
773 def os_rcpath():
774 '''return default os-specific hgrc search path'''
774 '''return default os-specific hgrc search path'''
775 path = []
775 path = []
776 # old mod_python does not set sys.argv
776 # old mod_python does not set sys.argv
777 if len(getattr(sys, 'argv', [])) > 0:
777 if len(getattr(sys, 'argv', [])) > 0:
778 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
778 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
779 '/../etc/mercurial'))
779 '/../etc/mercurial'))
780 path.extend(rcfiles('/etc/mercurial'))
780 path.extend(rcfiles('/etc/mercurial'))
781 path.append(os.path.expanduser('~/.hgrc'))
781 path.append(os.path.expanduser('~/.hgrc'))
782 path = [os.path.normpath(f) for f in path]
782 path = [os.path.normpath(f) for f in path]
783 return path
783 return path
784
784
785 def parse_patch_output(output_line):
785 def parse_patch_output(output_line):
786 """parses the output produced by patch and returns the file name"""
786 """parses the output produced by patch and returns the file name"""
787 pf = output_line[14:]
787 pf = output_line[14:]
788 if pf.startswith("'") and pf.endswith("'") and " " in pf:
788 if pf.startswith("'") and pf.endswith("'") and " " in pf:
789 pf = pf[1:-1] # Remove the quotes
789 pf = pf[1:-1] # Remove the quotes
790 return pf
790 return pf
791
791
792 def is_exec(f, last):
792 def is_exec(f, last):
793 """check whether a file is executable"""
793 """check whether a file is executable"""
794 return (os.lstat(f).st_mode & 0100 != 0)
794 return (os.lstat(f).st_mode & 0100 != 0)
795
795
796 def set_exec(f, mode):
796 def set_exec(f, mode):
797 s = os.lstat(f).st_mode
797 s = os.lstat(f).st_mode
798 if (s & 0100 != 0) == mode:
798 if (s & 0100 != 0) == mode:
799 return
799 return
800 if mode:
800 if mode:
801 # Turn on +x for every +r bit when making a file executable
801 # Turn on +x for every +r bit when making a file executable
802 # and obey umask.
802 # and obey umask.
803 umask = os.umask(0)
803 umask = os.umask(0)
804 os.umask(umask)
804 os.umask(umask)
805 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
805 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
806 else:
806 else:
807 os.chmod(f, s & 0666)
807 os.chmod(f, s & 0666)
808
808
809 def set_binary(fd):
809 def set_binary(fd):
810 pass
810 pass
811
811
812 def pconvert(path):
812 def pconvert(path):
813 return path
813 return path
814
814
815 def localpath(path):
815 def localpath(path):
816 return path
816 return path
817
817
818 normpath = os.path.normpath
818 normpath = os.path.normpath
819 samestat = os.path.samestat
819 samestat = os.path.samestat
820
820
821 def makelock(info, pathname):
821 def makelock(info, pathname):
822 try:
822 try:
823 os.symlink(info, pathname)
823 os.symlink(info, pathname)
824 except OSError, why:
824 except OSError, why:
825 if why.errno == errno.EEXIST:
825 if why.errno == errno.EEXIST:
826 raise
826 raise
827 else:
827 else:
828 _makelock_file(info, pathname)
828 _makelock_file(info, pathname)
829
829
830 def readlock(pathname):
830 def readlock(pathname):
831 try:
831 try:
832 return os.readlink(pathname)
832 return os.readlink(pathname)
833 except OSError, why:
833 except OSError, why:
834 if why.errno == errno.EINVAL:
834 if why.errno == errno.EINVAL:
835 return _readlock_file(pathname)
835 return _readlock_file(pathname)
836 else:
836 else:
837 raise
837 raise
838
838
839 def shellquote(s):
839 def shellquote(s):
840 return "'%s'" % s.replace("'", "'\\''")
840 return "'%s'" % s.replace("'", "'\\''")
841
841
842 def testpid(pid):
842 def testpid(pid):
843 '''return False if pid dead, True if running or not sure'''
843 '''return False if pid dead, True if running or not sure'''
844 try:
844 try:
845 os.kill(pid, 0)
845 os.kill(pid, 0)
846 return True
846 return True
847 except OSError, inst:
847 except OSError, inst:
848 return inst.errno != errno.ESRCH
848 return inst.errno != errno.ESRCH
849
849
850 def explain_exit(code):
850 def explain_exit(code):
851 """return a 2-tuple (desc, code) describing a process's status"""
851 """return a 2-tuple (desc, code) describing a process's status"""
852 if os.WIFEXITED(code):
852 if os.WIFEXITED(code):
853 val = os.WEXITSTATUS(code)
853 val = os.WEXITSTATUS(code)
854 return _("exited with status %d") % val, val
854 return _("exited with status %d") % val, val
855 elif os.WIFSIGNALED(code):
855 elif os.WIFSIGNALED(code):
856 val = os.WTERMSIG(code)
856 val = os.WTERMSIG(code)
857 return _("killed by signal %d") % val, val
857 return _("killed by signal %d") % val, val
858 elif os.WIFSTOPPED(code):
858 elif os.WIFSTOPPED(code):
859 val = os.WSTOPSIG(code)
859 val = os.WSTOPSIG(code)
860 return _("stopped by signal %d") % val, val
860 return _("stopped by signal %d") % val, val
861 raise ValueError(_("invalid exit code"))
861 raise ValueError(_("invalid exit code"))
862
862
863 def isowner(fp, st=None):
863 def isowner(fp, st=None):
864 """Return True if the file object f belongs to the current user.
864 """Return True if the file object f belongs to the current user.
865
865
866 The return value of a util.fstat(f) may be passed as the st argument.
866 The return value of a util.fstat(f) may be passed as the st argument.
867 """
867 """
868 if st is None:
868 if st is None:
869 st = fstat(f)
869 st = fstat(f)
870 return st.st_uid == os.getuid()
870 return st.st_uid == os.getuid()
871
871
872
872
873 def opener(base, audit=True):
873 def opener(base, audit=True):
874 """
874 """
875 return a function that opens files relative to base
875 return a function that opens files relative to base
876
876
877 this function is used to hide the details of COW semantics and
877 this function is used to hide the details of COW semantics and
878 remote file access from higher level code.
878 remote file access from higher level code.
879 """
879 """
880 p = base
880 p = base
881 audit_p = audit
881 audit_p = audit
882
882
883 def mktempcopy(name):
883 def mktempcopy(name):
884 d, fn = os.path.split(name)
884 d, fn = os.path.split(name)
885 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
885 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
886 os.close(fd)
886 os.close(fd)
887 ofp = posixfile(temp, "wb")
887 ofp = posixfile(temp, "wb")
888 try:
888 try:
889 try:
889 try:
890 ifp = posixfile(name, "rb")
890 ifp = posixfile(name, "rb")
891 except IOError, inst:
891 except IOError, inst:
892 if not getattr(inst, 'filename', None):
892 if not getattr(inst, 'filename', None):
893 inst.filename = name
893 inst.filename = name
894 raise
894 raise
895 for chunk in filechunkiter(ifp):
895 for chunk in filechunkiter(ifp):
896 ofp.write(chunk)
896 ofp.write(chunk)
897 ifp.close()
897 ifp.close()
898 ofp.close()
898 ofp.close()
899 except:
899 except:
900 try: os.unlink(temp)
900 try: os.unlink(temp)
901 except: pass
901 except: pass
902 raise
902 raise
903 st = os.lstat(name)
903 st = os.lstat(name)
904 os.chmod(temp, st.st_mode)
904 os.chmod(temp, st.st_mode)
905 return temp
905 return temp
906
906
907 class atomictempfile(posixfile):
907 class atomictempfile(posixfile):
908 """the file will only be copied when rename is called"""
908 """the file will only be copied when rename is called"""
909 def __init__(self, name, mode):
909 def __init__(self, name, mode):
910 self.__name = name
910 self.__name = name
911 self.temp = mktempcopy(name)
911 self.temp = mktempcopy(name)
912 posixfile.__init__(self, self.temp, mode)
912 posixfile.__init__(self, self.temp, mode)
913 def rename(self):
913 def rename(self):
914 if not self.closed:
914 if not self.closed:
915 posixfile.close(self)
915 posixfile.close(self)
916 rename(self.temp, localpath(self.__name))
916 rename(self.temp, localpath(self.__name))
917 def __del__(self):
917 def __del__(self):
918 if not self.closed:
918 if not self.closed:
919 try:
919 try:
920 os.unlink(self.temp)
920 os.unlink(self.temp)
921 except: pass
921 except: pass
922 posixfile.close(self)
922 posixfile.close(self)
923
923
924 class atomicfile(atomictempfile):
924 class atomicfile(atomictempfile):
925 """the file will only be copied on close"""
925 """the file will only be copied on close"""
926 def __init__(self, name, mode):
926 def __init__(self, name, mode):
927 atomictempfile.__init__(self, name, mode)
927 atomictempfile.__init__(self, name, mode)
928 def close(self):
928 def close(self):
929 self.rename()
929 self.rename()
930 def __del__(self):
930 def __del__(self):
931 self.rename()
931 self.rename()
932
932
933 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
933 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
934 if audit_p:
934 if audit_p:
935 audit_path(path)
935 audit_path(path)
936 f = os.path.join(p, path)
936 f = os.path.join(p, path)
937
937
938 if not text:
938 if not text:
939 mode += "b" # for that other OS
939 mode += "b" # for that other OS
940
940
941 if mode[0] != "r":
941 if mode[0] != "r":
942 try:
942 try:
943 nlink = nlinks(f)
943 nlink = nlinks(f)
944 except OSError:
944 except OSError:
945 d = os.path.dirname(f)
945 d = os.path.dirname(f)
946 if not os.path.isdir(d):
946 if not os.path.isdir(d):
947 os.makedirs(d)
947 os.makedirs(d)
948 else:
948 else:
949 if atomic:
949 if atomic:
950 return atomicfile(f, mode)
950 return atomicfile(f, mode)
951 elif atomictemp:
951 elif atomictemp:
952 return atomictempfile(f, mode)
952 return atomictempfile(f, mode)
953 if nlink > 1:
953 if nlink > 1:
954 rename(mktempcopy(f), f)
954 rename(mktempcopy(f), f)
955 return posixfile(f, mode)
955 return posixfile(f, mode)
956
956
957 return o
957 return o
958
958
959 class chunkbuffer(object):
959 class chunkbuffer(object):
960 """Allow arbitrary sized chunks of data to be efficiently read from an
960 """Allow arbitrary sized chunks of data to be efficiently read from an
961 iterator over chunks of arbitrary size."""
961 iterator over chunks of arbitrary size."""
962
962
963 def __init__(self, in_iter, targetsize = 2**16):
963 def __init__(self, in_iter, targetsize = 2**16):
964 """in_iter is the iterator that's iterating over the input chunks.
964 """in_iter is the iterator that's iterating over the input chunks.
965 targetsize is how big a buffer to try to maintain."""
965 targetsize is how big a buffer to try to maintain."""
966 self.in_iter = iter(in_iter)
966 self.in_iter = iter(in_iter)
967 self.buf = ''
967 self.buf = ''
968 self.targetsize = int(targetsize)
968 self.targetsize = int(targetsize)
969 if self.targetsize <= 0:
969 if self.targetsize <= 0:
970 raise ValueError(_("targetsize must be greater than 0, was %d") %
970 raise ValueError(_("targetsize must be greater than 0, was %d") %
971 targetsize)
971 targetsize)
972 self.iterempty = False
972 self.iterempty = False
973
973
974 def fillbuf(self):
974 def fillbuf(self):
975 """Ignore target size; read every chunk from iterator until empty."""
975 """Ignore target size; read every chunk from iterator until empty."""
976 if not self.iterempty:
976 if not self.iterempty:
977 collector = cStringIO.StringIO()
977 collector = cStringIO.StringIO()
978 collector.write(self.buf)
978 collector.write(self.buf)
979 for ch in self.in_iter:
979 for ch in self.in_iter:
980 collector.write(ch)
980 collector.write(ch)
981 self.buf = collector.getvalue()
981 self.buf = collector.getvalue()
982 self.iterempty = True
982 self.iterempty = True
983
983
984 def read(self, l):
984 def read(self, l):
985 """Read L bytes of data from the iterator of chunks of data.
985 """Read L bytes of data from the iterator of chunks of data.
986 Returns less than L bytes if the iterator runs dry."""
986 Returns less than L bytes if the iterator runs dry."""
987 if l > len(self.buf) and not self.iterempty:
987 if l > len(self.buf) and not self.iterempty:
988 # Clamp to a multiple of self.targetsize
988 # Clamp to a multiple of self.targetsize
989 targetsize = self.targetsize * ((l // self.targetsize) + 1)
989 targetsize = self.targetsize * ((l // self.targetsize) + 1)
990 collector = cStringIO.StringIO()
990 collector = cStringIO.StringIO()
991 collector.write(self.buf)
991 collector.write(self.buf)
992 collected = len(self.buf)
992 collected = len(self.buf)
993 for chunk in self.in_iter:
993 for chunk in self.in_iter:
994 collector.write(chunk)
994 collector.write(chunk)
995 collected += len(chunk)
995 collected += len(chunk)
996 if collected >= targetsize:
996 if collected >= targetsize:
997 break
997 break
998 if collected < targetsize:
998 if collected < targetsize:
999 self.iterempty = True
999 self.iterempty = True
1000 self.buf = collector.getvalue()
1000 self.buf = collector.getvalue()
1001 s, self.buf = self.buf[:l], buffer(self.buf, l)
1001 s, self.buf = self.buf[:l], buffer(self.buf, l)
1002 return s
1002 return s
1003
1003
1004 def filechunkiter(f, size=65536, limit=None):
1004 def filechunkiter(f, size=65536, limit=None):
1005 """Create a generator that produces the data in the file size
1005 """Create a generator that produces the data in the file size
1006 (default 65536) bytes at a time, up to optional limit (default is
1006 (default 65536) bytes at a time, up to optional limit (default is
1007 to read all data). Chunks may be less than size bytes if the
1007 to read all data). Chunks may be less than size bytes if the
1008 chunk is the last chunk in the file, or the file is a socket or
1008 chunk is the last chunk in the file, or the file is a socket or
1009 some other type of file that sometimes reads less data than is
1009 some other type of file that sometimes reads less data than is
1010 requested."""
1010 requested."""
1011 assert size >= 0
1011 assert size >= 0
1012 assert limit is None or limit >= 0
1012 assert limit is None or limit >= 0
1013 while True:
1013 while True:
1014 if limit is None: nbytes = size
1014 if limit is None: nbytes = size
1015 else: nbytes = min(limit, size)
1015 else: nbytes = min(limit, size)
1016 s = nbytes and f.read(nbytes)
1016 s = nbytes and f.read(nbytes)
1017 if not s: break
1017 if not s: break
1018 if limit: limit -= len(s)
1018 if limit: limit -= len(s)
1019 yield s
1019 yield s
1020
1020
1021 def makedate():
1021 def makedate():
1022 lt = time.localtime()
1022 lt = time.localtime()
1023 if lt[8] == 1 and time.daylight:
1023 if lt[8] == 1 and time.daylight:
1024 tz = time.altzone
1024 tz = time.altzone
1025 else:
1025 else:
1026 tz = time.timezone
1026 tz = time.timezone
1027 return time.mktime(lt), tz
1027 return time.mktime(lt), tz
1028
1028
1029 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1029 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1030 """represent a (unixtime, offset) tuple as a localized time.
1030 """represent a (unixtime, offset) tuple as a localized time.
1031 unixtime is seconds since the epoch, and offset is the time zone's
1031 unixtime is seconds since the epoch, and offset is the time zone's
1032 number of seconds away from UTC. if timezone is false, do not
1032 number of seconds away from UTC. if timezone is false, do not
1033 append time zone to string."""
1033 append time zone to string."""
1034 t, tz = date or makedate()
1034 t, tz = date or makedate()
1035 s = time.strftime(format, time.gmtime(float(t) - tz))
1035 s = time.strftime(format, time.gmtime(float(t) - tz))
1036 if timezone:
1036 if timezone:
1037 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1037 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1038 return s
1038 return s
1039
1039
1040 def strdate(string, format='%a %b %d %H:%M:%S %Y'):
1040 def strdate(string, format='%a %b %d %H:%M:%S %Y'):
1041 """parse a localized time string and return a (unixtime, offset) tuple.
1041 """parse a localized time string and return a (unixtime, offset) tuple.
1042 if the string cannot be parsed, ValueError is raised."""
1042 if the string cannot be parsed, ValueError is raised."""
1043 def hastimezone(string):
1043 def hastimezone(string):
1044 return (string[-4:].isdigit() and
1044 return (string[-4:].isdigit() and
1045 (string[-5] == '+' or string[-5] == '-') and
1045 (string[-5] == '+' or string[-5] == '-') and
1046 string[-6].isspace())
1046 string[-6].isspace())
1047
1047
1048 # NOTE: unixtime = localunixtime + offset
1048 # NOTE: unixtime = localunixtime + offset
1049 if hastimezone(string):
1049 if hastimezone(string):
1050 date, tz = string[:-6], string[-5:]
1050 date, tz = string[:-6], string[-5:]
1051 tz = int(tz)
1051 tz = int(tz)
1052 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1052 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1053 else:
1053 else:
1054 date, offset = string, None
1054 date, offset = string, None
1055 timetuple = time.strptime(date, format)
1055 timetuple = time.strptime(date, format)
1056 localunixtime = int(calendar.timegm(timetuple))
1056 localunixtime = int(calendar.timegm(timetuple))
1057 if offset is None:
1057 if offset is None:
1058 # local timezone
1058 # local timezone
1059 unixtime = int(time.mktime(timetuple))
1059 unixtime = int(time.mktime(timetuple))
1060 offset = unixtime - localunixtime
1060 offset = unixtime - localunixtime
1061 else:
1061 else:
1062 unixtime = localunixtime + offset
1062 unixtime = localunixtime + offset
1063 return unixtime, offset
1063 return unixtime, offset
1064
1064
1065 def parsedate(string, formats=None):
1065 def parsedate(string, formats=None):
1066 """parse a localized time string and return a (unixtime, offset) tuple.
1066 """parse a localized time string and return a (unixtime, offset) tuple.
1067 The date may be a "unixtime offset" string or in one of the specified
1067 The date may be a "unixtime offset" string or in one of the specified
1068 formats."""
1068 formats."""
1069 if not string:
1070 return 0, 0
1069 if not formats:
1071 if not formats:
1070 formats = defaultdateformats
1072 formats = defaultdateformats
1071 try:
1073 try:
1072 when, offset = map(int, string.split(' '))
1074 when, offset = map(int, string.split(' '))
1073 except ValueError:
1075 except ValueError:
1074 for format in formats:
1076 for format in formats:
1075 try:
1077 try:
1076 when, offset = strdate(string, format)
1078 when, offset = strdate(string, format)
1077 except ValueError:
1079 except ValueError:
1078 pass
1080 pass
1079 else:
1081 else:
1080 break
1082 break
1081 else:
1083 else:
1082 raise Abort(_('invalid date: %r ') % string)
1084 raise Abort(_('invalid date: %r ') % string)
1083 # validate explicit (probably user-specified) date and
1085 # validate explicit (probably user-specified) date and
1084 # time zone offset. values must fit in signed 32 bits for
1086 # time zone offset. values must fit in signed 32 bits for
1085 # current 32-bit linux runtimes. timezones go from UTC-12
1087 # current 32-bit linux runtimes. timezones go from UTC-12
1086 # to UTC+14
1088 # to UTC+14
1087 if abs(when) > 0x7fffffff:
1089 if abs(when) > 0x7fffffff:
1088 raise Abort(_('date exceeds 32 bits: %d') % when)
1090 raise Abort(_('date exceeds 32 bits: %d') % when)
1089 if offset < -50400 or offset > 43200:
1091 if offset < -50400 or offset > 43200:
1090 raise Abort(_('impossible time zone offset: %d') % offset)
1092 raise Abort(_('impossible time zone offset: %d') % offset)
1091 return when, offset
1093 return when, offset
1092
1094
1093 def shortuser(user):
1095 def shortuser(user):
1094 """Return a short representation of a user name or email address."""
1096 """Return a short representation of a user name or email address."""
1095 f = user.find('@')
1097 f = user.find('@')
1096 if f >= 0:
1098 if f >= 0:
1097 user = user[:f]
1099 user = user[:f]
1098 f = user.find('<')
1100 f = user.find('<')
1099 if f >= 0:
1101 if f >= 0:
1100 user = user[f+1:]
1102 user = user[f+1:]
1101 f = user.find(' ')
1103 f = user.find(' ')
1102 if f >= 0:
1104 if f >= 0:
1103 user = user[:f]
1105 user = user[:f]
1104 f = user.find('.')
1106 f = user.find('.')
1105 if f >= 0:
1107 if f >= 0:
1106 user = user[:f]
1108 user = user[:f]
1107 return user
1109 return user
1108
1110
1109 def ellipsis(text, maxlength=400):
1111 def ellipsis(text, maxlength=400):
1110 """Trim string to at most maxlength (default: 400) characters."""
1112 """Trim string to at most maxlength (default: 400) characters."""
1111 if len(text) <= maxlength:
1113 if len(text) <= maxlength:
1112 return text
1114 return text
1113 else:
1115 else:
1114 return "%s..." % (text[:maxlength-3])
1116 return "%s..." % (text[:maxlength-3])
1115
1117
1116 def walkrepos(path):
1118 def walkrepos(path):
1117 '''yield every hg repository under path, recursively.'''
1119 '''yield every hg repository under path, recursively.'''
1118 def errhandler(err):
1120 def errhandler(err):
1119 if err.filename == path:
1121 if err.filename == path:
1120 raise err
1122 raise err
1121
1123
1122 for root, dirs, files in os.walk(path, onerror=errhandler):
1124 for root, dirs, files in os.walk(path, onerror=errhandler):
1123 for d in dirs:
1125 for d in dirs:
1124 if d == '.hg':
1126 if d == '.hg':
1125 yield root
1127 yield root
1126 dirs[:] = []
1128 dirs[:] = []
1127 break
1129 break
1128
1130
1129 _rcpath = None
1131 _rcpath = None
1130
1132
1131 def rcpath():
1133 def rcpath():
1132 '''return hgrc search path. if env var HGRCPATH is set, use it.
1134 '''return hgrc search path. if env var HGRCPATH is set, use it.
1133 for each item in path, if directory, use files ending in .rc,
1135 for each item in path, if directory, use files ending in .rc,
1134 else use item.
1136 else use item.
1135 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1137 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1136 if no HGRCPATH, use default os-specific path.'''
1138 if no HGRCPATH, use default os-specific path.'''
1137 global _rcpath
1139 global _rcpath
1138 if _rcpath is None:
1140 if _rcpath is None:
1139 if 'HGRCPATH' in os.environ:
1141 if 'HGRCPATH' in os.environ:
1140 _rcpath = []
1142 _rcpath = []
1141 for p in os.environ['HGRCPATH'].split(os.pathsep):
1143 for p in os.environ['HGRCPATH'].split(os.pathsep):
1142 if not p: continue
1144 if not p: continue
1143 if os.path.isdir(p):
1145 if os.path.isdir(p):
1144 for f in os.listdir(p):
1146 for f in os.listdir(p):
1145 if f.endswith('.rc'):
1147 if f.endswith('.rc'):
1146 _rcpath.append(os.path.join(p, f))
1148 _rcpath.append(os.path.join(p, f))
1147 else:
1149 else:
1148 _rcpath.append(p)
1150 _rcpath.append(p)
1149 else:
1151 else:
1150 _rcpath = os_rcpath()
1152 _rcpath = os_rcpath()
1151 return _rcpath
1153 return _rcpath
1152
1154
1153 def bytecount(nbytes):
1155 def bytecount(nbytes):
1154 '''return byte count formatted as readable string, with units'''
1156 '''return byte count formatted as readable string, with units'''
1155
1157
1156 units = (
1158 units = (
1157 (100, 1<<30, _('%.0f GB')),
1159 (100, 1<<30, _('%.0f GB')),
1158 (10, 1<<30, _('%.1f GB')),
1160 (10, 1<<30, _('%.1f GB')),
1159 (1, 1<<30, _('%.2f GB')),
1161 (1, 1<<30, _('%.2f GB')),
1160 (100, 1<<20, _('%.0f MB')),
1162 (100, 1<<20, _('%.0f MB')),
1161 (10, 1<<20, _('%.1f MB')),
1163 (10, 1<<20, _('%.1f MB')),
1162 (1, 1<<20, _('%.2f MB')),
1164 (1, 1<<20, _('%.2f MB')),
1163 (100, 1<<10, _('%.0f KB')),
1165 (100, 1<<10, _('%.0f KB')),
1164 (10, 1<<10, _('%.1f KB')),
1166 (10, 1<<10, _('%.1f KB')),
1165 (1, 1<<10, _('%.2f KB')),
1167 (1, 1<<10, _('%.2f KB')),
1166 (1, 1, _('%.0f bytes')),
1168 (1, 1, _('%.0f bytes')),
1167 )
1169 )
1168
1170
1169 for multiplier, divisor, format in units:
1171 for multiplier, divisor, format in units:
1170 if nbytes >= divisor * multiplier:
1172 if nbytes >= divisor * multiplier:
1171 return format % (nbytes / float(divisor))
1173 return format % (nbytes / float(divisor))
1172 return units[-1][2] % nbytes
1174 return units[-1][2] % nbytes
1173
1175
1174 def drop_scheme(scheme, path):
1176 def drop_scheme(scheme, path):
1175 sc = scheme + ':'
1177 sc = scheme + ':'
1176 if path.startswith(sc):
1178 if path.startswith(sc):
1177 path = path[len(sc):]
1179 path = path[len(sc):]
1178 if path.startswith('//'):
1180 if path.startswith('//'):
1179 path = path[2:]
1181 path = path[2:]
1180 return path
1182 return path
General Comments 0
You need to be logged in to leave comments. Login now