##// END OF EJS Templates
symlinks: check whether a filesystem supports symlinks
Matt Mackall -
r3998:315d4799 default
parent child Browse files
Show More
@@ -1,1341 +1,1353 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 696 def checkexec(path):
697 697 """
698 698 Check whether the given path is on a filesystem with UNIX-like exec flags
699 699
700 700 Requires a directory (like /foo/.hg)
701 701 """
702 702 fh, fn = tempfile.mkstemp("", "", path)
703 703 os.close(fh)
704 704 m = os.stat(fn).st_mode
705 705 os.chmod(fn, m ^ 0111)
706 706 r = (os.stat(fn).st_mode != m)
707 707 os.unlink(fn)
708 708 return r
709 709
710 710 def execfunc(path, fallback):
711 711 '''return an is_exec() function with default to fallback'''
712 712 if checkexec(path):
713 713 return lambda x: is_exec(os.path.join(path, x))
714 714 return fallback
715 715
716 def checksymlink(path):
717 """check whether the given path is on a symlink-capable filesystem"""
718 # mktemp is not racy because symlink creation will fail if the
719 # file already exists
720 name = tempfile.mktemp(dir=path)
721 try:
722 os.symlink(".", name)
723 os.unlink(name)
724 return True
725 except OSError:
726 return False
727
716 728 # Platform specific variants
717 729 if os.name == 'nt':
718 730 import msvcrt
719 731 nulldev = 'NUL:'
720 732
721 733 class winstdout:
722 734 '''stdout on windows misbehaves if sent through a pipe'''
723 735
724 736 def __init__(self, fp):
725 737 self.fp = fp
726 738
727 739 def __getattr__(self, key):
728 740 return getattr(self.fp, key)
729 741
730 742 def close(self):
731 743 try:
732 744 self.fp.close()
733 745 except: pass
734 746
735 747 def write(self, s):
736 748 try:
737 749 return self.fp.write(s)
738 750 except IOError, inst:
739 751 if inst.errno != 0: raise
740 752 self.close()
741 753 raise IOError(errno.EPIPE, 'Broken pipe')
742 754
743 755 sys.stdout = winstdout(sys.stdout)
744 756
745 757 def system_rcpath():
746 758 try:
747 759 return system_rcpath_win32()
748 760 except:
749 761 return [r'c:\mercurial\mercurial.ini']
750 762
751 763 def os_rcpath():
752 764 '''return default os-specific hgrc search path'''
753 765 path = system_rcpath()
754 766 path.append(user_rcpath())
755 767 userprofile = os.environ.get('USERPROFILE')
756 768 if userprofile:
757 769 path.append(os.path.join(userprofile, 'mercurial.ini'))
758 770 return path
759 771
760 772 def user_rcpath():
761 773 '''return os-specific hgrc search path to the user dir'''
762 774 return os.path.join(os.path.expanduser('~'), 'mercurial.ini')
763 775
764 776 def parse_patch_output(output_line):
765 777 """parses the output produced by patch and returns the file name"""
766 778 pf = output_line[14:]
767 779 if pf[0] == '`':
768 780 pf = pf[1:-1] # Remove the quotes
769 781 return pf
770 782
771 783 def testpid(pid):
772 784 '''return False if pid dead, True if running or not known'''
773 785 return True
774 786
775 787 def set_exec(f, mode):
776 788 pass
777 789
778 790 def set_binary(fd):
779 791 msvcrt.setmode(fd.fileno(), os.O_BINARY)
780 792
781 793 def pconvert(path):
782 794 return path.replace("\\", "/")
783 795
784 796 def localpath(path):
785 797 return path.replace('/', '\\')
786 798
787 799 def normpath(path):
788 800 return pconvert(os.path.normpath(path))
789 801
790 802 makelock = _makelock_file
791 803 readlock = _readlock_file
792 804
793 805 def samestat(s1, s2):
794 806 return False
795 807
796 808 def shellquote(s):
797 809 return '"%s"' % s.replace('"', '\\"')
798 810
799 811 def explain_exit(code):
800 812 return _("exited with status %d") % code, code
801 813
802 814 # if you change this stub into a real check, please try to implement the
803 815 # username and groupname functions above, too.
804 816 def isowner(fp, st=None):
805 817 return True
806 818
807 819 try:
808 820 # override functions with win32 versions if possible
809 821 from util_win32 import *
810 822 if not is_win_9x():
811 823 posixfile = posixfile_nt
812 824 except ImportError:
813 825 pass
814 826
815 827 else:
816 828 nulldev = '/dev/null'
817 829 _umask = os.umask(0)
818 830 os.umask(_umask)
819 831
820 832 def rcfiles(path):
821 833 rcs = [os.path.join(path, 'hgrc')]
822 834 rcdir = os.path.join(path, 'hgrc.d')
823 835 try:
824 836 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
825 837 if f.endswith(".rc")])
826 838 except OSError:
827 839 pass
828 840 return rcs
829 841
830 842 def os_rcpath():
831 843 '''return default os-specific hgrc search path'''
832 844 path = []
833 845 # old mod_python does not set sys.argv
834 846 if len(getattr(sys, 'argv', [])) > 0:
835 847 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
836 848 '/../etc/mercurial'))
837 849 path.extend(rcfiles('/etc/mercurial'))
838 850 path.append(os.path.expanduser('~/.hgrc'))
839 851 path = [os.path.normpath(f) for f in path]
840 852 return path
841 853
842 854 def parse_patch_output(output_line):
843 855 """parses the output produced by patch and returns the file name"""
844 856 pf = output_line[14:]
845 857 if pf.startswith("'") and pf.endswith("'") and " " in pf:
846 858 pf = pf[1:-1] # Remove the quotes
847 859 return pf
848 860
849 861 def is_exec(f):
850 862 """check whether a file is executable"""
851 863 return (os.lstat(f).st_mode & 0100 != 0)
852 864
853 865 def set_exec(f, mode):
854 866 s = os.lstat(f).st_mode
855 867 if (s & 0100 != 0) == mode:
856 868 return
857 869 if mode:
858 870 # Turn on +x for every +r bit when making a file executable
859 871 # and obey umask.
860 872 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
861 873 else:
862 874 os.chmod(f, s & 0666)
863 875
864 876 def set_binary(fd):
865 877 pass
866 878
867 879 def pconvert(path):
868 880 return path
869 881
870 882 def localpath(path):
871 883 return path
872 884
873 885 normpath = os.path.normpath
874 886 samestat = os.path.samestat
875 887
876 888 def makelock(info, pathname):
877 889 try:
878 890 os.symlink(info, pathname)
879 891 except OSError, why:
880 892 if why.errno == errno.EEXIST:
881 893 raise
882 894 else:
883 895 _makelock_file(info, pathname)
884 896
885 897 def readlock(pathname):
886 898 try:
887 899 return os.readlink(pathname)
888 900 except OSError, why:
889 901 if why.errno == errno.EINVAL:
890 902 return _readlock_file(pathname)
891 903 else:
892 904 raise
893 905
894 906 def shellquote(s):
895 907 return "'%s'" % s.replace("'", "'\\''")
896 908
897 909 def testpid(pid):
898 910 '''return False if pid dead, True if running or not sure'''
899 911 try:
900 912 os.kill(pid, 0)
901 913 return True
902 914 except OSError, inst:
903 915 return inst.errno != errno.ESRCH
904 916
905 917 def explain_exit(code):
906 918 """return a 2-tuple (desc, code) describing a process's status"""
907 919 if os.WIFEXITED(code):
908 920 val = os.WEXITSTATUS(code)
909 921 return _("exited with status %d") % val, val
910 922 elif os.WIFSIGNALED(code):
911 923 val = os.WTERMSIG(code)
912 924 return _("killed by signal %d") % val, val
913 925 elif os.WIFSTOPPED(code):
914 926 val = os.WSTOPSIG(code)
915 927 return _("stopped by signal %d") % val, val
916 928 raise ValueError(_("invalid exit code"))
917 929
918 930 def isowner(fp, st=None):
919 931 """Return True if the file object f belongs to the current user.
920 932
921 933 The return value of a util.fstat(f) may be passed as the st argument.
922 934 """
923 935 if st is None:
924 936 st = fstat(fp)
925 937 return st.st_uid == os.getuid()
926 938
927 939 def _buildencodefun():
928 940 e = '_'
929 941 win_reserved = [ord(x) for x in '\\:*?"<>|']
930 942 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
931 943 for x in (range(32) + range(126, 256) + win_reserved):
932 944 cmap[chr(x)] = "~%02x" % x
933 945 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
934 946 cmap[chr(x)] = e + chr(x).lower()
935 947 dmap = {}
936 948 for k, v in cmap.iteritems():
937 949 dmap[v] = k
938 950 def decode(s):
939 951 i = 0
940 952 while i < len(s):
941 953 for l in xrange(1, 4):
942 954 try:
943 955 yield dmap[s[i:i+l]]
944 956 i += l
945 957 break
946 958 except KeyError:
947 959 pass
948 960 else:
949 961 raise KeyError
950 962 return (lambda s: "".join([cmap[c] for c in s]),
951 963 lambda s: "".join(list(decode(s))))
952 964
953 965 encodefilename, decodefilename = _buildencodefun()
954 966
955 967 def encodedopener(openerfn, fn):
956 968 def o(path, *args, **kw):
957 969 return openerfn(fn(path), *args, **kw)
958 970 return o
959 971
960 972 def opener(base, audit=True):
961 973 """
962 974 return a function that opens files relative to base
963 975
964 976 this function is used to hide the details of COW semantics and
965 977 remote file access from higher level code.
966 978 """
967 979 p = base
968 980 audit_p = audit
969 981
970 982 def mktempcopy(name):
971 983 d, fn = os.path.split(name)
972 984 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
973 985 os.close(fd)
974 986 ofp = posixfile(temp, "wb")
975 987 try:
976 988 try:
977 989 ifp = posixfile(name, "rb")
978 990 except IOError, inst:
979 991 if not getattr(inst, 'filename', None):
980 992 inst.filename = name
981 993 raise
982 994 for chunk in filechunkiter(ifp):
983 995 ofp.write(chunk)
984 996 ifp.close()
985 997 ofp.close()
986 998 except:
987 999 try: os.unlink(temp)
988 1000 except: pass
989 1001 raise
990 1002 st = os.lstat(name)
991 1003 os.chmod(temp, st.st_mode)
992 1004 return temp
993 1005
994 1006 class atomictempfile(posixfile):
995 1007 """the file will only be copied when rename is called"""
996 1008 def __init__(self, name, mode):
997 1009 self.__name = name
998 1010 self.temp = mktempcopy(name)
999 1011 posixfile.__init__(self, self.temp, mode)
1000 1012 def rename(self):
1001 1013 if not self.closed:
1002 1014 posixfile.close(self)
1003 1015 rename(self.temp, localpath(self.__name))
1004 1016 def __del__(self):
1005 1017 if not self.closed:
1006 1018 try:
1007 1019 os.unlink(self.temp)
1008 1020 except: pass
1009 1021 posixfile.close(self)
1010 1022
1011 1023 class atomicfile(atomictempfile):
1012 1024 """the file will only be copied on close"""
1013 1025 def __init__(self, name, mode):
1014 1026 atomictempfile.__init__(self, name, mode)
1015 1027 def close(self):
1016 1028 self.rename()
1017 1029 def __del__(self):
1018 1030 self.rename()
1019 1031
1020 1032 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
1021 1033 if audit_p:
1022 1034 audit_path(path)
1023 1035 f = os.path.join(p, path)
1024 1036
1025 1037 if not text:
1026 1038 mode += "b" # for that other OS
1027 1039
1028 1040 if mode[0] != "r":
1029 1041 try:
1030 1042 nlink = nlinks(f)
1031 1043 except OSError:
1032 1044 d = os.path.dirname(f)
1033 1045 if not os.path.isdir(d):
1034 1046 os.makedirs(d)
1035 1047 else:
1036 1048 if atomic:
1037 1049 return atomicfile(f, mode)
1038 1050 elif atomictemp:
1039 1051 return atomictempfile(f, mode)
1040 1052 if nlink > 1:
1041 1053 rename(mktempcopy(f), f)
1042 1054 return posixfile(f, mode)
1043 1055
1044 1056 return o
1045 1057
1046 1058 class chunkbuffer(object):
1047 1059 """Allow arbitrary sized chunks of data to be efficiently read from an
1048 1060 iterator over chunks of arbitrary size."""
1049 1061
1050 1062 def __init__(self, in_iter, targetsize = 2**16):
1051 1063 """in_iter is the iterator that's iterating over the input chunks.
1052 1064 targetsize is how big a buffer to try to maintain."""
1053 1065 self.in_iter = iter(in_iter)
1054 1066 self.buf = ''
1055 1067 self.targetsize = int(targetsize)
1056 1068 if self.targetsize <= 0:
1057 1069 raise ValueError(_("targetsize must be greater than 0, was %d") %
1058 1070 targetsize)
1059 1071 self.iterempty = False
1060 1072
1061 1073 def fillbuf(self):
1062 1074 """Ignore target size; read every chunk from iterator until empty."""
1063 1075 if not self.iterempty:
1064 1076 collector = cStringIO.StringIO()
1065 1077 collector.write(self.buf)
1066 1078 for ch in self.in_iter:
1067 1079 collector.write(ch)
1068 1080 self.buf = collector.getvalue()
1069 1081 self.iterempty = True
1070 1082
1071 1083 def read(self, l):
1072 1084 """Read L bytes of data from the iterator of chunks of data.
1073 1085 Returns less than L bytes if the iterator runs dry."""
1074 1086 if l > len(self.buf) and not self.iterempty:
1075 1087 # Clamp to a multiple of self.targetsize
1076 1088 targetsize = self.targetsize * ((l // self.targetsize) + 1)
1077 1089 collector = cStringIO.StringIO()
1078 1090 collector.write(self.buf)
1079 1091 collected = len(self.buf)
1080 1092 for chunk in self.in_iter:
1081 1093 collector.write(chunk)
1082 1094 collected += len(chunk)
1083 1095 if collected >= targetsize:
1084 1096 break
1085 1097 if collected < targetsize:
1086 1098 self.iterempty = True
1087 1099 self.buf = collector.getvalue()
1088 1100 s, self.buf = self.buf[:l], buffer(self.buf, l)
1089 1101 return s
1090 1102
1091 1103 def filechunkiter(f, size=65536, limit=None):
1092 1104 """Create a generator that produces the data in the file size
1093 1105 (default 65536) bytes at a time, up to optional limit (default is
1094 1106 to read all data). Chunks may be less than size bytes if the
1095 1107 chunk is the last chunk in the file, or the file is a socket or
1096 1108 some other type of file that sometimes reads less data than is
1097 1109 requested."""
1098 1110 assert size >= 0
1099 1111 assert limit is None or limit >= 0
1100 1112 while True:
1101 1113 if limit is None: nbytes = size
1102 1114 else: nbytes = min(limit, size)
1103 1115 s = nbytes and f.read(nbytes)
1104 1116 if not s: break
1105 1117 if limit: limit -= len(s)
1106 1118 yield s
1107 1119
1108 1120 def makedate():
1109 1121 lt = time.localtime()
1110 1122 if lt[8] == 1 and time.daylight:
1111 1123 tz = time.altzone
1112 1124 else:
1113 1125 tz = time.timezone
1114 1126 return time.mktime(lt), tz
1115 1127
1116 1128 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1117 1129 """represent a (unixtime, offset) tuple as a localized time.
1118 1130 unixtime is seconds since the epoch, and offset is the time zone's
1119 1131 number of seconds away from UTC. if timezone is false, do not
1120 1132 append time zone to string."""
1121 1133 t, tz = date or makedate()
1122 1134 s = time.strftime(format, time.gmtime(float(t) - tz))
1123 1135 if timezone:
1124 1136 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1125 1137 return s
1126 1138
1127 1139 def strdate(string, format, defaults):
1128 1140 """parse a localized time string and return a (unixtime, offset) tuple.
1129 1141 if the string cannot be parsed, ValueError is raised."""
1130 1142 def timezone(string):
1131 1143 tz = string.split()[-1]
1132 1144 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1133 1145 tz = int(tz)
1134 1146 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1135 1147 return offset
1136 1148 if tz == "GMT" or tz == "UTC":
1137 1149 return 0
1138 1150 return None
1139 1151
1140 1152 # NOTE: unixtime = localunixtime + offset
1141 1153 offset, date = timezone(string), string
1142 1154 if offset != None:
1143 1155 date = " ".join(string.split()[:-1])
1144 1156
1145 1157 # add missing elements from defaults
1146 1158 for part in defaults:
1147 1159 found = [True for p in part if ("%"+p) in format]
1148 1160 if not found:
1149 1161 date += "@" + defaults[part]
1150 1162 format += "@%" + part[0]
1151 1163
1152 1164 timetuple = time.strptime(date, format)
1153 1165 localunixtime = int(calendar.timegm(timetuple))
1154 1166 if offset is None:
1155 1167 # local timezone
1156 1168 unixtime = int(time.mktime(timetuple))
1157 1169 offset = unixtime - localunixtime
1158 1170 else:
1159 1171 unixtime = localunixtime + offset
1160 1172 return unixtime, offset
1161 1173
1162 1174 def parsedate(string, formats=None, defaults=None):
1163 1175 """parse a localized time string and return a (unixtime, offset) tuple.
1164 1176 The date may be a "unixtime offset" string or in one of the specified
1165 1177 formats."""
1166 1178 if not string:
1167 1179 return 0, 0
1168 1180 if not formats:
1169 1181 formats = defaultdateformats
1170 1182 string = string.strip()
1171 1183 try:
1172 1184 when, offset = map(int, string.split(' '))
1173 1185 except ValueError:
1174 1186 # fill out defaults
1175 1187 if not defaults:
1176 1188 defaults = {}
1177 1189 now = makedate()
1178 1190 for part in "d mb yY HI M S".split():
1179 1191 if part not in defaults:
1180 1192 if part[0] in "HMS":
1181 1193 defaults[part] = "00"
1182 1194 elif part[0] in "dm":
1183 1195 defaults[part] = "1"
1184 1196 else:
1185 1197 defaults[part] = datestr(now, "%" + part[0], False)
1186 1198
1187 1199 for format in formats:
1188 1200 try:
1189 1201 when, offset = strdate(string, format, defaults)
1190 1202 except ValueError:
1191 1203 pass
1192 1204 else:
1193 1205 break
1194 1206 else:
1195 1207 raise Abort(_('invalid date: %r ') % string)
1196 1208 # validate explicit (probably user-specified) date and
1197 1209 # time zone offset. values must fit in signed 32 bits for
1198 1210 # current 32-bit linux runtimes. timezones go from UTC-12
1199 1211 # to UTC+14
1200 1212 if abs(when) > 0x7fffffff:
1201 1213 raise Abort(_('date exceeds 32 bits: %d') % when)
1202 1214 if offset < -50400 or offset > 43200:
1203 1215 raise Abort(_('impossible time zone offset: %d') % offset)
1204 1216 return when, offset
1205 1217
1206 1218 def matchdate(date):
1207 1219 """Return a function that matches a given date match specifier
1208 1220
1209 1221 Formats include:
1210 1222
1211 1223 '{date}' match a given date to the accuracy provided
1212 1224
1213 1225 '<{date}' on or before a given date
1214 1226
1215 1227 '>{date}' on or after a given date
1216 1228
1217 1229 """
1218 1230
1219 1231 def lower(date):
1220 1232 return parsedate(date, extendeddateformats)[0]
1221 1233
1222 1234 def upper(date):
1223 1235 d = dict(mb="12", HI="23", M="59", S="59")
1224 1236 for days in "31 30 29".split():
1225 1237 try:
1226 1238 d["d"] = days
1227 1239 return parsedate(date, extendeddateformats, d)[0]
1228 1240 except:
1229 1241 pass
1230 1242 d["d"] = "28"
1231 1243 return parsedate(date, extendeddateformats, d)[0]
1232 1244
1233 1245 if date[0] == "<":
1234 1246 when = upper(date[1:])
1235 1247 return lambda x: x <= when
1236 1248 elif date[0] == ">":
1237 1249 when = lower(date[1:])
1238 1250 return lambda x: x >= when
1239 1251 elif date[0] == "-":
1240 1252 try:
1241 1253 days = int(date[1:])
1242 1254 except ValueError:
1243 1255 raise Abort(_("invalid day spec: %s") % date[1:])
1244 1256 when = makedate()[0] - days * 3600 * 24
1245 1257 return lambda x: x >= when
1246 1258 elif " to " in date:
1247 1259 a, b = date.split(" to ")
1248 1260 start, stop = lower(a), upper(b)
1249 1261 return lambda x: x >= start and x <= stop
1250 1262 else:
1251 1263 start, stop = lower(date), upper(date)
1252 1264 return lambda x: x >= start and x <= stop
1253 1265
1254 1266 def shortuser(user):
1255 1267 """Return a short representation of a user name or email address."""
1256 1268 f = user.find('@')
1257 1269 if f >= 0:
1258 1270 user = user[:f]
1259 1271 f = user.find('<')
1260 1272 if f >= 0:
1261 1273 user = user[f+1:]
1262 1274 f = user.find(' ')
1263 1275 if f >= 0:
1264 1276 user = user[:f]
1265 1277 f = user.find('.')
1266 1278 if f >= 0:
1267 1279 user = user[:f]
1268 1280 return user
1269 1281
1270 1282 def ellipsis(text, maxlength=400):
1271 1283 """Trim string to at most maxlength (default: 400) characters."""
1272 1284 if len(text) <= maxlength:
1273 1285 return text
1274 1286 else:
1275 1287 return "%s..." % (text[:maxlength-3])
1276 1288
1277 1289 def walkrepos(path):
1278 1290 '''yield every hg repository under path, recursively.'''
1279 1291 def errhandler(err):
1280 1292 if err.filename == path:
1281 1293 raise err
1282 1294
1283 1295 for root, dirs, files in os.walk(path, onerror=errhandler):
1284 1296 for d in dirs:
1285 1297 if d == '.hg':
1286 1298 yield root
1287 1299 dirs[:] = []
1288 1300 break
1289 1301
1290 1302 _rcpath = None
1291 1303
1292 1304 def rcpath():
1293 1305 '''return hgrc search path. if env var HGRCPATH is set, use it.
1294 1306 for each item in path, if directory, use files ending in .rc,
1295 1307 else use item.
1296 1308 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1297 1309 if no HGRCPATH, use default os-specific path.'''
1298 1310 global _rcpath
1299 1311 if _rcpath is None:
1300 1312 if 'HGRCPATH' in os.environ:
1301 1313 _rcpath = []
1302 1314 for p in os.environ['HGRCPATH'].split(os.pathsep):
1303 1315 if not p: continue
1304 1316 if os.path.isdir(p):
1305 1317 for f in os.listdir(p):
1306 1318 if f.endswith('.rc'):
1307 1319 _rcpath.append(os.path.join(p, f))
1308 1320 else:
1309 1321 _rcpath.append(p)
1310 1322 else:
1311 1323 _rcpath = os_rcpath()
1312 1324 return _rcpath
1313 1325
1314 1326 def bytecount(nbytes):
1315 1327 '''return byte count formatted as readable string, with units'''
1316 1328
1317 1329 units = (
1318 1330 (100, 1<<30, _('%.0f GB')),
1319 1331 (10, 1<<30, _('%.1f GB')),
1320 1332 (1, 1<<30, _('%.2f GB')),
1321 1333 (100, 1<<20, _('%.0f MB')),
1322 1334 (10, 1<<20, _('%.1f MB')),
1323 1335 (1, 1<<20, _('%.2f MB')),
1324 1336 (100, 1<<10, _('%.0f KB')),
1325 1337 (10, 1<<10, _('%.1f KB')),
1326 1338 (1, 1<<10, _('%.2f KB')),
1327 1339 (1, 1, _('%.0f bytes')),
1328 1340 )
1329 1341
1330 1342 for multiplier, divisor, format in units:
1331 1343 if nbytes >= divisor * multiplier:
1332 1344 return format % (nbytes / float(divisor))
1333 1345 return units[-1][2] % nbytes
1334 1346
1335 1347 def drop_scheme(scheme, path):
1336 1348 sc = scheme + ':'
1337 1349 if path.startswith(sc):
1338 1350 path = path[len(sc):]
1339 1351 if path.startswith('//'):
1340 1352 path = path[2:]
1341 1353 return path
General Comments 0
You need to be logged in to leave comments. Login now