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