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