##// END OF EJS Templates
Backed out changeset fce065538bcf: it caused a 5x performance regression on OS X
Bryan O'Sullivan -
r9083:ec171737 default
parent child Browse files
Show More
@@ -1,1455 +1,1457 b''
1 1 # util.py - Mercurial utility functions and platform specfic implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2, incorporated herein by reference.
9 9
10 10 """Mercurial utility functions and platform specfic implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from i18n import _
17 17 import error, osutil
18 18 import cStringIO, errno, re, shutil, sys, tempfile, traceback
19 19 import os, stat, threading, time, calendar, glob, random
20 20 import imp
21 21
22 22 # Python compatibility
23 23
24 24 def sha1(s):
25 25 return _fastsha1(s)
26 26
27 27 def _fastsha1(s):
28 28 # This function will import sha1 from hashlib or sha (whichever is
29 29 # available) and overwrite itself with it on the first call.
30 30 # Subsequent calls will go directly to the imported function.
31 31 try:
32 32 from hashlib import sha1 as _sha1
33 33 except ImportError:
34 34 from sha import sha as _sha1
35 35 global _fastsha1, sha1
36 36 _fastsha1 = sha1 = _sha1
37 37 return _sha1(s)
38 38
39 39 import subprocess
40 40 closefds = os.name == 'posix'
41 def popen2(cmd):
42 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
41 def popen2(cmd, bufsize=-1):
42 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
43 close_fds=closefds,
43 44 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
44 45 return p.stdin, p.stdout
45 def popen3(cmd):
46 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
46 def popen3(cmd, bufsize=-1):
47 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
48 close_fds=closefds,
47 49 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
48 50 stderr=subprocess.PIPE)
49 51 return p.stdin, p.stdout, p.stderr
50 52
51 53 def version():
52 54 """Return version information if available."""
53 55 try:
54 56 import __version__
55 57 return __version__.version
56 58 except ImportError:
57 59 return 'unknown'
58 60
59 61 # used by parsedate
60 62 defaultdateformats = (
61 63 '%Y-%m-%d %H:%M:%S',
62 64 '%Y-%m-%d %I:%M:%S%p',
63 65 '%Y-%m-%d %H:%M',
64 66 '%Y-%m-%d %I:%M%p',
65 67 '%Y-%m-%d',
66 68 '%m-%d',
67 69 '%m/%d',
68 70 '%m/%d/%y',
69 71 '%m/%d/%Y',
70 72 '%a %b %d %H:%M:%S %Y',
71 73 '%a %b %d %I:%M:%S%p %Y',
72 74 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
73 75 '%b %d %H:%M:%S %Y',
74 76 '%b %d %I:%M:%S%p %Y',
75 77 '%b %d %H:%M:%S',
76 78 '%b %d %I:%M:%S%p',
77 79 '%b %d %H:%M',
78 80 '%b %d %I:%M%p',
79 81 '%b %d %Y',
80 82 '%b %d',
81 83 '%H:%M:%S',
82 84 '%I:%M:%SP',
83 85 '%H:%M',
84 86 '%I:%M%p',
85 87 )
86 88
87 89 extendeddateformats = defaultdateformats + (
88 90 "%Y",
89 91 "%Y-%m",
90 92 "%b",
91 93 "%b %Y",
92 94 )
93 95
94 96 def cachefunc(func):
95 97 '''cache the result of function calls'''
96 98 # XXX doesn't handle keywords args
97 99 cache = {}
98 100 if func.func_code.co_argcount == 1:
99 101 # we gain a small amount of time because
100 102 # we don't need to pack/unpack the list
101 103 def f(arg):
102 104 if arg not in cache:
103 105 cache[arg] = func(arg)
104 106 return cache[arg]
105 107 else:
106 108 def f(*args):
107 109 if args not in cache:
108 110 cache[args] = func(*args)
109 111 return cache[args]
110 112
111 113 return f
112 114
113 115 class propertycache(object):
114 116 def __init__(self, func):
115 117 self.func = func
116 118 self.name = func.__name__
117 119 def __get__(self, obj, type=None):
118 120 result = self.func(obj)
119 121 setattr(obj, self.name, result)
120 122 return result
121 123
122 124 def pipefilter(s, cmd):
123 125 '''filter string S through command CMD, returning its output'''
124 126 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
125 127 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
126 128 pout, perr = p.communicate(s)
127 129 return pout
128 130
129 131 def tempfilter(s, cmd):
130 132 '''filter string S through a pair of temporary files with CMD.
131 133 CMD is used as a template to create the real command to be run,
132 134 with the strings INFILE and OUTFILE replaced by the real names of
133 135 the temporary files generated.'''
134 136 inname, outname = None, None
135 137 try:
136 138 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
137 139 fp = os.fdopen(infd, 'wb')
138 140 fp.write(s)
139 141 fp.close()
140 142 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
141 143 os.close(outfd)
142 144 cmd = cmd.replace('INFILE', inname)
143 145 cmd = cmd.replace('OUTFILE', outname)
144 146 code = os.system(cmd)
145 147 if sys.platform == 'OpenVMS' and code & 1:
146 148 code = 0
147 149 if code: raise Abort(_("command '%s' failed: %s") %
148 150 (cmd, explain_exit(code)))
149 151 return open(outname, 'rb').read()
150 152 finally:
151 153 try:
152 154 if inname: os.unlink(inname)
153 155 except: pass
154 156 try:
155 157 if outname: os.unlink(outname)
156 158 except: pass
157 159
158 160 filtertable = {
159 161 'tempfile:': tempfilter,
160 162 'pipe:': pipefilter,
161 163 }
162 164
163 165 def filter(s, cmd):
164 166 "filter a string through a command that transforms its input to its output"
165 167 for name, fn in filtertable.iteritems():
166 168 if cmd.startswith(name):
167 169 return fn(s, cmd[len(name):].lstrip())
168 170 return pipefilter(s, cmd)
169 171
170 172 def binary(s):
171 173 """return true if a string is binary data"""
172 174 return bool(s and '\0' in s)
173 175
174 176 def increasingchunks(source, min=1024, max=65536):
175 177 '''return no less than min bytes per chunk while data remains,
176 178 doubling min after each chunk until it reaches max'''
177 179 def log2(x):
178 180 if not x:
179 181 return 0
180 182 i = 0
181 183 while x:
182 184 x >>= 1
183 185 i += 1
184 186 return i - 1
185 187
186 188 buf = []
187 189 blen = 0
188 190 for chunk in source:
189 191 buf.append(chunk)
190 192 blen += len(chunk)
191 193 if blen >= min:
192 194 if min < max:
193 195 min = min << 1
194 196 nmin = 1 << log2(blen)
195 197 if nmin > min:
196 198 min = nmin
197 199 if min > max:
198 200 min = max
199 201 yield ''.join(buf)
200 202 blen = 0
201 203 buf = []
202 204 if buf:
203 205 yield ''.join(buf)
204 206
205 207 Abort = error.Abort
206 208
207 209 def always(fn): return True
208 210 def never(fn): return False
209 211
210 212 def patkind(name, default):
211 213 """Split a string into an optional pattern kind prefix and the
212 214 actual pattern."""
213 215 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
214 216 if name.startswith(prefix + ':'): return name.split(':', 1)
215 217 return default, name
216 218
217 219 def globre(pat, head='^', tail='$'):
218 220 "convert a glob pattern into a regexp"
219 221 i, n = 0, len(pat)
220 222 res = ''
221 223 group = 0
222 224 def peek(): return i < n and pat[i]
223 225 while i < n:
224 226 c = pat[i]
225 227 i = i+1
226 228 if c == '*':
227 229 if peek() == '*':
228 230 i += 1
229 231 res += '.*'
230 232 else:
231 233 res += '[^/]*'
232 234 elif c == '?':
233 235 res += '.'
234 236 elif c == '[':
235 237 j = i
236 238 if j < n and pat[j] in '!]':
237 239 j += 1
238 240 while j < n and pat[j] != ']':
239 241 j += 1
240 242 if j >= n:
241 243 res += '\\['
242 244 else:
243 245 stuff = pat[i:j].replace('\\','\\\\')
244 246 i = j + 1
245 247 if stuff[0] == '!':
246 248 stuff = '^' + stuff[1:]
247 249 elif stuff[0] == '^':
248 250 stuff = '\\' + stuff
249 251 res = '%s[%s]' % (res, stuff)
250 252 elif c == '{':
251 253 group += 1
252 254 res += '(?:'
253 255 elif c == '}' and group:
254 256 res += ')'
255 257 group -= 1
256 258 elif c == ',' and group:
257 259 res += '|'
258 260 elif c == '\\':
259 261 p = peek()
260 262 if p:
261 263 i += 1
262 264 res += re.escape(p)
263 265 else:
264 266 res += re.escape(c)
265 267 else:
266 268 res += re.escape(c)
267 269 return head + res + tail
268 270
269 271 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
270 272
271 273 def pathto(root, n1, n2):
272 274 '''return the relative path from one place to another.
273 275 root should use os.sep to separate directories
274 276 n1 should use os.sep to separate directories
275 277 n2 should use "/" to separate directories
276 278 returns an os.sep-separated path.
277 279
278 280 If n1 is a relative path, it's assumed it's
279 281 relative to root.
280 282 n2 should always be relative to root.
281 283 '''
282 284 if not n1: return localpath(n2)
283 285 if os.path.isabs(n1):
284 286 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
285 287 return os.path.join(root, localpath(n2))
286 288 n2 = '/'.join((pconvert(root), n2))
287 289 a, b = splitpath(n1), n2.split('/')
288 290 a.reverse()
289 291 b.reverse()
290 292 while a and b and a[-1] == b[-1]:
291 293 a.pop()
292 294 b.pop()
293 295 b.reverse()
294 296 return os.sep.join((['..'] * len(a)) + b) or '.'
295 297
296 298 def canonpath(root, cwd, myname):
297 299 """return the canonical path of myname, given cwd and root"""
298 300 if root == os.sep:
299 301 rootsep = os.sep
300 302 elif endswithsep(root):
301 303 rootsep = root
302 304 else:
303 305 rootsep = root + os.sep
304 306 name = myname
305 307 if not os.path.isabs(name):
306 308 name = os.path.join(root, cwd, name)
307 309 name = os.path.normpath(name)
308 310 audit_path = path_auditor(root)
309 311 if name != rootsep and name.startswith(rootsep):
310 312 name = name[len(rootsep):]
311 313 audit_path(name)
312 314 return pconvert(name)
313 315 elif name == root:
314 316 return ''
315 317 else:
316 318 # Determine whether `name' is in the hierarchy at or beneath `root',
317 319 # by iterating name=dirname(name) until that causes no change (can't
318 320 # check name == '/', because that doesn't work on windows). For each
319 321 # `name', compare dev/inode numbers. If they match, the list `rel'
320 322 # holds the reversed list of components making up the relative file
321 323 # name we want.
322 324 root_st = os.stat(root)
323 325 rel = []
324 326 while True:
325 327 try:
326 328 name_st = os.stat(name)
327 329 except OSError:
328 330 break
329 331 if samestat(name_st, root_st):
330 332 if not rel:
331 333 # name was actually the same as root (maybe a symlink)
332 334 return ''
333 335 rel.reverse()
334 336 name = os.path.join(*rel)
335 337 audit_path(name)
336 338 return pconvert(name)
337 339 dirname, basename = os.path.split(name)
338 340 rel.append(basename)
339 341 if dirname == name:
340 342 break
341 343 name = dirname
342 344
343 345 raise Abort('%s not under root' % myname)
344 346
345 347 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None, dflt_pat='glob'):
346 348 """build a function to match a set of file patterns
347 349
348 350 arguments:
349 351 canonroot - the canonical root of the tree you're matching against
350 352 cwd - the current working directory, if relevant
351 353 names - patterns to find
352 354 inc - patterns to include
353 355 exc - patterns to exclude
354 356 dflt_pat - if a pattern in names has no explicit type, assume this one
355 357 src - where these patterns came from (e.g. .hgignore)
356 358
357 359 a pattern is one of:
358 360 'glob:<glob>' - a glob relative to cwd
359 361 're:<regexp>' - a regular expression
360 362 'path:<path>' - a path relative to canonroot
361 363 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
362 364 'relpath:<path>' - a path relative to cwd
363 365 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
364 366 '<something>' - one of the cases above, selected by the dflt_pat argument
365 367
366 368 returns:
367 369 a 3-tuple containing
368 370 - list of roots (places where one should start a recursive walk of the fs);
369 371 this often matches the explicit non-pattern names passed in, but also
370 372 includes the initial part of glob: patterns that has no glob characters
371 373 - a bool match(filename) function
372 374 - a bool indicating if any patterns were passed in
373 375 """
374 376
375 377 # a common case: no patterns at all
376 378 if not names and not inc and not exc:
377 379 return [], always, False
378 380
379 381 def contains_glob(name):
380 382 for c in name:
381 383 if c in _globchars: return True
382 384 return False
383 385
384 386 def regex(kind, name, tail):
385 387 '''convert a pattern into a regular expression'''
386 388 if not name:
387 389 return ''
388 390 if kind == 're':
389 391 return name
390 392 elif kind == 'path':
391 393 return '^' + re.escape(name) + '(?:/|$)'
392 394 elif kind == 'relglob':
393 395 return globre(name, '(?:|.*/)', tail)
394 396 elif kind == 'relpath':
395 397 return re.escape(name) + '(?:/|$)'
396 398 elif kind == 'relre':
397 399 if name.startswith('^'):
398 400 return name
399 401 return '.*' + name
400 402 return globre(name, '', tail)
401 403
402 404 def matchfn(pats, tail):
403 405 """build a matching function from a set of patterns"""
404 406 if not pats:
405 407 return
406 408 try:
407 409 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
408 410 if len(pat) > 20000:
409 411 raise OverflowError()
410 412 return re.compile(pat).match
411 413 except OverflowError:
412 414 # We're using a Python with a tiny regex engine and we
413 415 # made it explode, so we'll divide the pattern list in two
414 416 # until it works
415 417 l = len(pats)
416 418 if l < 2:
417 419 raise
418 420 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
419 421 return lambda s: a(s) or b(s)
420 422 except re.error:
421 423 for k, p in pats:
422 424 try:
423 425 re.compile('(?:%s)' % regex(k, p, tail))
424 426 except re.error:
425 427 if src:
426 428 raise Abort("%s: invalid pattern (%s): %s" %
427 429 (src, k, p))
428 430 else:
429 431 raise Abort("invalid pattern (%s): %s" % (k, p))
430 432 raise Abort("invalid pattern")
431 433
432 434 def globprefix(pat):
433 435 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
434 436 root = []
435 437 for p in pat.split('/'):
436 438 if contains_glob(p): break
437 439 root.append(p)
438 440 return '/'.join(root) or '.'
439 441
440 442 def normalizepats(names, default):
441 443 pats = []
442 444 roots = []
443 445 anypats = False
444 446 for kind, name in [patkind(p, default) for p in names]:
445 447 if kind in ('glob', 'relpath'):
446 448 name = canonpath(canonroot, cwd, name)
447 449 elif kind in ('relglob', 'path'):
448 450 name = normpath(name)
449 451
450 452 pats.append((kind, name))
451 453
452 454 if kind in ('glob', 're', 'relglob', 'relre'):
453 455 anypats = True
454 456
455 457 if kind == 'glob':
456 458 root = globprefix(name)
457 459 roots.append(root)
458 460 elif kind in ('relpath', 'path'):
459 461 roots.append(name or '.')
460 462 elif kind == 'relglob':
461 463 roots.append('.')
462 464 return roots, pats, anypats
463 465
464 466 roots, pats, anypats = normalizepats(names, dflt_pat)
465 467
466 468 patmatch = matchfn(pats, '$') or always
467 469 incmatch = always
468 470 if inc:
469 471 dummy, inckinds, dummy = normalizepats(inc, 'glob')
470 472 incmatch = matchfn(inckinds, '(?:/|$)')
471 473 excmatch = never
472 474 if exc:
473 475 dummy, exckinds, dummy = normalizepats(exc, 'glob')
474 476 excmatch = matchfn(exckinds, '(?:/|$)')
475 477
476 478 if not names and inc and not exc:
477 479 # common case: hgignore patterns
478 480 match = incmatch
479 481 else:
480 482 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
481 483
482 484 return (roots, match, (inc or exc or anypats) and True)
483 485
484 486 _hgexecutable = None
485 487
486 488 def main_is_frozen():
487 489 """return True if we are a frozen executable.
488 490
489 491 The code supports py2exe (most common, Windows only) and tools/freeze
490 492 (portable, not much used).
491 493 """
492 494 return (hasattr(sys, "frozen") or # new py2exe
493 495 hasattr(sys, "importers") or # old py2exe
494 496 imp.is_frozen("__main__")) # tools/freeze
495 497
496 498 def hgexecutable():
497 499 """return location of the 'hg' executable.
498 500
499 501 Defaults to $HG or 'hg' in the search path.
500 502 """
501 503 if _hgexecutable is None:
502 504 hg = os.environ.get('HG')
503 505 if hg:
504 506 set_hgexecutable(hg)
505 507 elif main_is_frozen():
506 508 set_hgexecutable(sys.executable)
507 509 else:
508 510 set_hgexecutable(find_exe('hg') or 'hg')
509 511 return _hgexecutable
510 512
511 513 def set_hgexecutable(path):
512 514 """set location of the 'hg' executable"""
513 515 global _hgexecutable
514 516 _hgexecutable = path
515 517
516 518 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
517 519 '''enhanced shell command execution.
518 520 run with environment maybe modified, maybe in different dir.
519 521
520 522 if command fails and onerr is None, return status. if ui object,
521 523 print error message and return status, else raise onerr object as
522 524 exception.'''
523 525 def py2shell(val):
524 526 'convert python object into string that is useful to shell'
525 527 if val in (None, False):
526 528 return '0'
527 529 if val == True:
528 530 return '1'
529 531 return str(val)
530 532 oldenv = {}
531 533 for k in environ:
532 534 oldenv[k] = os.environ.get(k)
533 535 if cwd is not None:
534 536 oldcwd = os.getcwd()
535 537 origcmd = cmd
536 538 if os.name == 'nt':
537 539 cmd = '"%s"' % cmd
538 540 try:
539 541 for k, v in environ.iteritems():
540 542 os.environ[k] = py2shell(v)
541 543 os.environ['HG'] = hgexecutable()
542 544 if cwd is not None and oldcwd != cwd:
543 545 os.chdir(cwd)
544 546 rc = os.system(cmd)
545 547 if sys.platform == 'OpenVMS' and rc & 1:
546 548 rc = 0
547 549 if rc and onerr:
548 550 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
549 551 explain_exit(rc)[0])
550 552 if errprefix:
551 553 errmsg = '%s: %s' % (errprefix, errmsg)
552 554 try:
553 555 onerr.warn(errmsg + '\n')
554 556 except AttributeError:
555 557 raise onerr(errmsg)
556 558 return rc
557 559 finally:
558 560 for k, v in oldenv.iteritems():
559 561 if v is None:
560 562 del os.environ[k]
561 563 else:
562 564 os.environ[k] = v
563 565 if cwd is not None and oldcwd != cwd:
564 566 os.chdir(oldcwd)
565 567
566 568 def checksignature(func):
567 569 '''wrap a function with code to check for calling errors'''
568 570 def check(*args, **kwargs):
569 571 try:
570 572 return func(*args, **kwargs)
571 573 except TypeError:
572 574 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
573 575 raise error.SignatureError
574 576 raise
575 577
576 578 return check
577 579
578 580 # os.path.lexists is not available on python2.3
579 581 def lexists(filename):
580 582 "test whether a file with this name exists. does not follow symlinks"
581 583 try:
582 584 os.lstat(filename)
583 585 except:
584 586 return False
585 587 return True
586 588
587 589 def rename(src, dst):
588 590 """forcibly rename a file"""
589 591 try:
590 592 os.rename(src, dst)
591 593 except OSError, err: # FIXME: check err (EEXIST ?)
592 594
593 595 # On windows, rename to existing file is not allowed, so we
594 596 # must delete destination first. But if a file is open, unlink
595 597 # schedules it for delete but does not delete it. Rename
596 598 # happens immediately even for open files, so we rename
597 599 # destination to a temporary name, then delete that. Then
598 600 # rename is safe to do.
599 601 # The temporary name is chosen at random to avoid the situation
600 602 # where a file is left lying around from a previous aborted run.
601 603 # The usual race condition this introduces can't be avoided as
602 604 # we need the name to rename into, and not the file itself. Due
603 605 # to the nature of the operation however, any races will at worst
604 606 # lead to the rename failing and the current operation aborting.
605 607
606 608 def tempname(prefix):
607 609 for tries in xrange(10):
608 610 temp = '%s-%08x' % (prefix, random.randint(0, 0xffffffff))
609 611 if not os.path.exists(temp):
610 612 return temp
611 613 raise IOError, (errno.EEXIST, "No usable temporary filename found")
612 614
613 615 temp = tempname(dst)
614 616 os.rename(dst, temp)
615 617 os.unlink(temp)
616 618 os.rename(src, dst)
617 619
618 620 def unlink(f):
619 621 """unlink and remove the directory if it is empty"""
620 622 os.unlink(f)
621 623 # try removing directories that might now be empty
622 624 try:
623 625 os.removedirs(os.path.dirname(f))
624 626 except OSError:
625 627 pass
626 628
627 629 def copyfile(src, dest):
628 630 "copy a file, preserving mode and atime/mtime"
629 631 if os.path.islink(src):
630 632 try:
631 633 os.unlink(dest)
632 634 except:
633 635 pass
634 636 os.symlink(os.readlink(src), dest)
635 637 else:
636 638 try:
637 639 shutil.copyfile(src, dest)
638 640 shutil.copystat(src, dest)
639 641 except shutil.Error, inst:
640 642 raise Abort(str(inst))
641 643
642 644 def copyfiles(src, dst, hardlink=None):
643 645 """Copy a directory tree using hardlinks if possible"""
644 646
645 647 if hardlink is None:
646 648 hardlink = (os.stat(src).st_dev ==
647 649 os.stat(os.path.dirname(dst)).st_dev)
648 650
649 651 if os.path.isdir(src):
650 652 os.mkdir(dst)
651 653 for name, kind in osutil.listdir(src):
652 654 srcname = os.path.join(src, name)
653 655 dstname = os.path.join(dst, name)
654 656 copyfiles(srcname, dstname, hardlink)
655 657 else:
656 658 if hardlink:
657 659 try:
658 660 os_link(src, dst)
659 661 except (IOError, OSError):
660 662 hardlink = False
661 663 shutil.copy(src, dst)
662 664 else:
663 665 shutil.copy(src, dst)
664 666
665 667 class path_auditor(object):
666 668 '''ensure that a filesystem path contains no banned components.
667 669 the following properties of a path are checked:
668 670
669 671 - under top-level .hg
670 672 - starts at the root of a windows drive
671 673 - contains ".."
672 674 - traverses a symlink (e.g. a/symlink_here/b)
673 675 - inside a nested repository'''
674 676
675 677 def __init__(self, root):
676 678 self.audited = set()
677 679 self.auditeddir = set()
678 680 self.root = root
679 681
680 682 def __call__(self, path):
681 683 if path in self.audited:
682 684 return
683 685 normpath = os.path.normcase(path)
684 686 parts = splitpath(normpath)
685 687 if (os.path.splitdrive(path)[0]
686 688 or parts[0].lower() in ('.hg', '.hg.', '')
687 689 or os.pardir in parts):
688 690 raise Abort(_("path contains illegal component: %s") % path)
689 691 if '.hg' in path.lower():
690 692 lparts = [p.lower() for p in parts]
691 693 for p in '.hg', '.hg.':
692 694 if p in lparts[1:]:
693 695 pos = lparts.index(p)
694 696 base = os.path.join(*parts[:pos])
695 697 raise Abort(_('path %r is inside repo %r') % (path, base))
696 698 def check(prefix):
697 699 curpath = os.path.join(self.root, prefix)
698 700 try:
699 701 st = os.lstat(curpath)
700 702 except OSError, err:
701 703 # EINVAL can be raised as invalid path syntax under win32.
702 704 # They must be ignored for patterns can be checked too.
703 705 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
704 706 raise
705 707 else:
706 708 if stat.S_ISLNK(st.st_mode):
707 709 raise Abort(_('path %r traverses symbolic link %r') %
708 710 (path, prefix))
709 711 elif (stat.S_ISDIR(st.st_mode) and
710 712 os.path.isdir(os.path.join(curpath, '.hg'))):
711 713 raise Abort(_('path %r is inside repo %r') %
712 714 (path, prefix))
713 715 parts.pop()
714 716 prefixes = []
715 717 for n in range(len(parts)):
716 718 prefix = os.sep.join(parts)
717 719 if prefix in self.auditeddir:
718 720 break
719 721 check(prefix)
720 722 prefixes.append(prefix)
721 723 parts.pop()
722 724
723 725 self.audited.add(path)
724 726 # only add prefixes to the cache after checking everything: we don't
725 727 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
726 728 self.auditeddir.update(prefixes)
727 729
728 730 def nlinks(pathname):
729 731 """Return number of hardlinks for the given file."""
730 732 return os.lstat(pathname).st_nlink
731 733
732 734 if hasattr(os, 'link'):
733 735 os_link = os.link
734 736 else:
735 737 def os_link(src, dst):
736 738 raise OSError(0, _("Hardlinks not supported"))
737 739
738 740 def lookup_reg(key, name=None, scope=None):
739 741 return None
740 742
741 743 if os.name == 'nt':
742 744 from windows import *
743 745 def expand_glob(pats):
744 746 '''On Windows, expand the implicit globs in a list of patterns'''
745 747 ret = []
746 748 for p in pats:
747 749 kind, name = patkind(p, None)
748 750 if kind is None:
749 751 globbed = glob.glob(name)
750 752 if globbed:
751 753 ret.extend(globbed)
752 754 continue
753 755 # if we couldn't expand the glob, just keep it around
754 756 ret.append(p)
755 757 return ret
756 758 else:
757 759 from posix import *
758 760
759 761 def makelock(info, pathname):
760 762 try:
761 763 return os.symlink(info, pathname)
762 764 except OSError, why:
763 765 if why.errno == errno.EEXIST:
764 766 raise
765 767 except AttributeError: # no symlink in os
766 768 pass
767 769
768 770 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
769 771 os.write(ld, info)
770 772 os.close(ld)
771 773
772 774 def readlock(pathname):
773 775 try:
774 776 return os.readlink(pathname)
775 777 except OSError, why:
776 778 if why.errno not in (errno.EINVAL, errno.ENOSYS):
777 779 raise
778 780 except AttributeError: # no symlink in os
779 781 pass
780 782 return posixfile(pathname).read()
781 783
782 784 def fstat(fp):
783 785 '''stat file object that may not have fileno method.'''
784 786 try:
785 787 return os.fstat(fp.fileno())
786 788 except AttributeError:
787 789 return os.stat(fp.name)
788 790
789 791 # File system features
790 792
791 793 def checkcase(path):
792 794 """
793 795 Check whether the given path is on a case-sensitive filesystem
794 796
795 797 Requires a path (like /foo/.hg) ending with a foldable final
796 798 directory component.
797 799 """
798 800 s1 = os.stat(path)
799 801 d, b = os.path.split(path)
800 802 p2 = os.path.join(d, b.upper())
801 803 if path == p2:
802 804 p2 = os.path.join(d, b.lower())
803 805 try:
804 806 s2 = os.stat(p2)
805 807 if s2 == s1:
806 808 return False
807 809 return True
808 810 except:
809 811 return True
810 812
811 813 _fspathcache = {}
812 814 def fspath(name, root):
813 815 '''Get name in the case stored in the filesystem
814 816
815 817 The name is either relative to root, or it is an absolute path starting
816 818 with root. Note that this function is unnecessary, and should not be
817 819 called, for case-sensitive filesystems (simply because it's expensive).
818 820 '''
819 821 # If name is absolute, make it relative
820 822 if name.lower().startswith(root.lower()):
821 823 l = len(root)
822 824 if name[l] == os.sep or name[l] == os.altsep:
823 825 l = l + 1
824 826 name = name[l:]
825 827
826 828 if not os.path.exists(os.path.join(root, name)):
827 829 return None
828 830
829 831 seps = os.sep
830 832 if os.altsep:
831 833 seps = seps + os.altsep
832 834 # Protect backslashes. This gets silly very quickly.
833 835 seps.replace('\\','\\\\')
834 836 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
835 837 dir = os.path.normcase(os.path.normpath(root))
836 838 result = []
837 839 for part, sep in pattern.findall(name):
838 840 if sep:
839 841 result.append(sep)
840 842 continue
841 843
842 844 if dir not in _fspathcache:
843 845 _fspathcache[dir] = os.listdir(dir)
844 846 contents = _fspathcache[dir]
845 847
846 848 lpart = part.lower()
847 849 for n in contents:
848 850 if n.lower() == lpart:
849 851 result.append(n)
850 852 break
851 853 else:
852 854 # Cannot happen, as the file exists!
853 855 result.append(part)
854 856 dir = os.path.join(dir, lpart)
855 857
856 858 return ''.join(result)
857 859
858 860 def checkexec(path):
859 861 """
860 862 Check whether the given path is on a filesystem with UNIX-like exec flags
861 863
862 864 Requires a directory (like /foo/.hg)
863 865 """
864 866
865 867 # VFAT on some Linux versions can flip mode but it doesn't persist
866 868 # a FS remount. Frequently we can detect it if files are created
867 869 # with exec bit on.
868 870
869 871 try:
870 872 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
871 873 fh, fn = tempfile.mkstemp("", "", path)
872 874 try:
873 875 os.close(fh)
874 876 m = os.stat(fn).st_mode & 0777
875 877 new_file_has_exec = m & EXECFLAGS
876 878 os.chmod(fn, m ^ EXECFLAGS)
877 879 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
878 880 finally:
879 881 os.unlink(fn)
880 882 except (IOError, OSError):
881 883 # we don't care, the user probably won't be able to commit anyway
882 884 return False
883 885 return not (new_file_has_exec or exec_flags_cannot_flip)
884 886
885 887 def checklink(path):
886 888 """check whether the given path is on a symlink-capable filesystem"""
887 889 # mktemp is not racy because symlink creation will fail if the
888 890 # file already exists
889 891 name = tempfile.mktemp(dir=path)
890 892 try:
891 893 os.symlink(".", name)
892 894 os.unlink(name)
893 895 return True
894 896 except (OSError, AttributeError):
895 897 return False
896 898
897 899 def needbinarypatch():
898 900 """return True if patches should be applied in binary mode by default."""
899 901 return os.name == 'nt'
900 902
901 903 def endswithsep(path):
902 904 '''Check path ends with os.sep or os.altsep.'''
903 905 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
904 906
905 907 def splitpath(path):
906 908 '''Split path by os.sep.
907 909 Note that this function does not use os.altsep because this is
908 910 an alternative of simple "xxx.split(os.sep)".
909 911 It is recommended to use os.path.normpath() before using this
910 912 function if need.'''
911 913 return path.split(os.sep)
912 914
913 915 def gui():
914 916 '''Are we running in a GUI?'''
915 917 return os.name == "nt" or os.name == "mac" or os.environ.get("DISPLAY")
916 918
917 919 def mktempcopy(name, emptyok=False, createmode=None):
918 920 """Create a temporary file with the same contents from name
919 921
920 922 The permission bits are copied from the original file.
921 923
922 924 If the temporary file is going to be truncated immediately, you
923 925 can use emptyok=True as an optimization.
924 926
925 927 Returns the name of the temporary file.
926 928 """
927 929 d, fn = os.path.split(name)
928 930 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
929 931 os.close(fd)
930 932 # Temporary files are created with mode 0600, which is usually not
931 933 # what we want. If the original file already exists, just copy
932 934 # its mode. Otherwise, manually obey umask.
933 935 try:
934 936 st_mode = os.lstat(name).st_mode & 0777
935 937 except OSError, inst:
936 938 if inst.errno != errno.ENOENT:
937 939 raise
938 940 st_mode = createmode
939 941 if st_mode is None:
940 942 st_mode = ~umask
941 943 st_mode &= 0666
942 944 os.chmod(temp, st_mode)
943 945 if emptyok:
944 946 return temp
945 947 try:
946 948 try:
947 949 ifp = posixfile(name, "rb")
948 950 except IOError, inst:
949 951 if inst.errno == errno.ENOENT:
950 952 return temp
951 953 if not getattr(inst, 'filename', None):
952 954 inst.filename = name
953 955 raise
954 956 ofp = posixfile(temp, "wb")
955 957 for chunk in filechunkiter(ifp):
956 958 ofp.write(chunk)
957 959 ifp.close()
958 960 ofp.close()
959 961 except:
960 962 try: os.unlink(temp)
961 963 except: pass
962 964 raise
963 965 return temp
964 966
965 967 class atomictempfile:
966 968 """file-like object that atomically updates a file
967 969
968 970 All writes will be redirected to a temporary copy of the original
969 971 file. When rename is called, the copy is renamed to the original
970 972 name, making the changes visible.
971 973 """
972 974 def __init__(self, name, mode, createmode):
973 975 self.__name = name
974 976 self.temp = mktempcopy(name, emptyok=('w' in mode),
975 977 createmode=createmode)
976 978 self._fp = posixfile(self.temp, mode)
977 979
978 980 def __getattr__(self, name):
979 981 return getattr(self._fp, name)
980 982
981 983 def rename(self):
982 984 if not self.closed:
983 985 self._fp.close()
984 986 rename(self.temp, localpath(self.__name))
985 987
986 988 def __del__(self):
987 989 if not self.closed:
988 990 try:
989 991 os.unlink(self.temp)
990 992 except: pass
991 993 self._fp.close()
992 994
993 995 def makedirs(name, mode=None):
994 996 """recursive directory creation with parent mode inheritance"""
995 997 try:
996 998 os.mkdir(name)
997 999 if mode is not None:
998 1000 os.chmod(name, mode)
999 1001 return
1000 1002 except OSError, err:
1001 1003 if err.errno == errno.EEXIST:
1002 1004 return
1003 1005 if err.errno != errno.ENOENT:
1004 1006 raise
1005 1007 parent = os.path.abspath(os.path.dirname(name))
1006 1008 makedirs(parent, mode)
1007 1009 makedirs(name, mode)
1008 1010
1009 1011 class opener(object):
1010 1012 """Open files relative to a base directory
1011 1013
1012 1014 This class is used to hide the details of COW semantics and
1013 1015 remote file access from higher level code.
1014 1016 """
1015 1017 def __init__(self, base, audit=True):
1016 1018 self.base = base
1017 1019 if audit:
1018 1020 self.audit_path = path_auditor(base)
1019 1021 else:
1020 1022 self.audit_path = always
1021 1023 self.createmode = None
1022 1024
1023 1025 def __getattr__(self, name):
1024 1026 if name == '_can_symlink':
1025 1027 self._can_symlink = checklink(self.base)
1026 1028 return self._can_symlink
1027 1029 raise AttributeError(name)
1028 1030
1029 1031 def _fixfilemode(self, name):
1030 1032 if self.createmode is None:
1031 1033 return
1032 1034 os.chmod(name, self.createmode & 0666)
1033 1035
1034 1036 def __call__(self, path, mode="r", text=False, atomictemp=False):
1035 1037 self.audit_path(path)
1036 1038 f = os.path.join(self.base, path)
1037 1039
1038 1040 if not text and "b" not in mode:
1039 1041 mode += "b" # for that other OS
1040 1042
1041 1043 nlink = -1
1042 1044 if mode not in ("r", "rb"):
1043 1045 try:
1044 1046 nlink = nlinks(f)
1045 1047 except OSError:
1046 1048 nlink = 0
1047 1049 d = os.path.dirname(f)
1048 1050 if not os.path.isdir(d):
1049 1051 makedirs(d, self.createmode)
1050 1052 if atomictemp:
1051 1053 return atomictempfile(f, mode, self.createmode)
1052 1054 if nlink > 1:
1053 1055 rename(mktempcopy(f), f)
1054 1056 fp = posixfile(f, mode)
1055 1057 if nlink == 0:
1056 1058 self._fixfilemode(f)
1057 1059 return fp
1058 1060
1059 1061 def symlink(self, src, dst):
1060 1062 self.audit_path(dst)
1061 1063 linkname = os.path.join(self.base, dst)
1062 1064 try:
1063 1065 os.unlink(linkname)
1064 1066 except OSError:
1065 1067 pass
1066 1068
1067 1069 dirname = os.path.dirname(linkname)
1068 1070 if not os.path.exists(dirname):
1069 1071 makedirs(dirname, self.createmode)
1070 1072
1071 1073 if self._can_symlink:
1072 1074 try:
1073 1075 os.symlink(src, linkname)
1074 1076 except OSError, err:
1075 1077 raise OSError(err.errno, _('could not symlink to %r: %s') %
1076 1078 (src, err.strerror), linkname)
1077 1079 else:
1078 1080 f = self(dst, "w")
1079 1081 f.write(src)
1080 1082 f.close()
1081 1083 self._fixfilemode(dst)
1082 1084
1083 1085 class chunkbuffer(object):
1084 1086 """Allow arbitrary sized chunks of data to be efficiently read from an
1085 1087 iterator over chunks of arbitrary size."""
1086 1088
1087 1089 def __init__(self, in_iter):
1088 1090 """in_iter is the iterator that's iterating over the input chunks.
1089 1091 targetsize is how big a buffer to try to maintain."""
1090 1092 self.iter = iter(in_iter)
1091 1093 self.buf = ''
1092 1094 self.targetsize = 2**16
1093 1095
1094 1096 def read(self, l):
1095 1097 """Read L bytes of data from the iterator of chunks of data.
1096 1098 Returns less than L bytes if the iterator runs dry."""
1097 1099 if l > len(self.buf) and self.iter:
1098 1100 # Clamp to a multiple of self.targetsize
1099 1101 targetsize = max(l, self.targetsize)
1100 1102 collector = cStringIO.StringIO()
1101 1103 collector.write(self.buf)
1102 1104 collected = len(self.buf)
1103 1105 for chunk in self.iter:
1104 1106 collector.write(chunk)
1105 1107 collected += len(chunk)
1106 1108 if collected >= targetsize:
1107 1109 break
1108 1110 if collected < targetsize:
1109 1111 self.iter = False
1110 1112 self.buf = collector.getvalue()
1111 1113 if len(self.buf) == l:
1112 1114 s, self.buf = str(self.buf), ''
1113 1115 else:
1114 1116 s, self.buf = self.buf[:l], buffer(self.buf, l)
1115 1117 return s
1116 1118
1117 1119 def filechunkiter(f, size=65536, limit=None):
1118 1120 """Create a generator that produces the data in the file size
1119 1121 (default 65536) bytes at a time, up to optional limit (default is
1120 1122 to read all data). Chunks may be less than size bytes if the
1121 1123 chunk is the last chunk in the file, or the file is a socket or
1122 1124 some other type of file that sometimes reads less data than is
1123 1125 requested."""
1124 1126 assert size >= 0
1125 1127 assert limit is None or limit >= 0
1126 1128 while True:
1127 1129 if limit is None: nbytes = size
1128 1130 else: nbytes = min(limit, size)
1129 1131 s = nbytes and f.read(nbytes)
1130 1132 if not s: break
1131 1133 if limit: limit -= len(s)
1132 1134 yield s
1133 1135
1134 1136 def makedate():
1135 1137 lt = time.localtime()
1136 1138 if lt[8] == 1 and time.daylight:
1137 1139 tz = time.altzone
1138 1140 else:
1139 1141 tz = time.timezone
1140 1142 return time.mktime(lt), tz
1141 1143
1142 1144 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1143 1145 """represent a (unixtime, offset) tuple as a localized time.
1144 1146 unixtime is seconds since the epoch, and offset is the time zone's
1145 1147 number of seconds away from UTC. if timezone is false, do not
1146 1148 append time zone to string."""
1147 1149 t, tz = date or makedate()
1148 1150 if "%1" in format or "%2" in format:
1149 1151 sign = (tz > 0) and "-" or "+"
1150 1152 minutes = abs(tz) / 60
1151 1153 format = format.replace("%1", "%c%02d" % (sign, minutes / 60))
1152 1154 format = format.replace("%2", "%02d" % (minutes % 60))
1153 1155 s = time.strftime(format, time.gmtime(float(t) - tz))
1154 1156 return s
1155 1157
1156 1158 def shortdate(date=None):
1157 1159 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1158 1160 return datestr(date, format='%Y-%m-%d')
1159 1161
1160 1162 def strdate(string, format, defaults=[]):
1161 1163 """parse a localized time string and return a (unixtime, offset) tuple.
1162 1164 if the string cannot be parsed, ValueError is raised."""
1163 1165 def timezone(string):
1164 1166 tz = string.split()[-1]
1165 1167 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1166 1168 sign = (tz[0] == "+") and 1 or -1
1167 1169 hours = int(tz[1:3])
1168 1170 minutes = int(tz[3:5])
1169 1171 return -sign * (hours * 60 + minutes) * 60
1170 1172 if tz == "GMT" or tz == "UTC":
1171 1173 return 0
1172 1174 return None
1173 1175
1174 1176 # NOTE: unixtime = localunixtime + offset
1175 1177 offset, date = timezone(string), string
1176 1178 if offset != None:
1177 1179 date = " ".join(string.split()[:-1])
1178 1180
1179 1181 # add missing elements from defaults
1180 1182 for part in defaults:
1181 1183 found = [True for p in part if ("%"+p) in format]
1182 1184 if not found:
1183 1185 date += "@" + defaults[part]
1184 1186 format += "@%" + part[0]
1185 1187
1186 1188 timetuple = time.strptime(date, format)
1187 1189 localunixtime = int(calendar.timegm(timetuple))
1188 1190 if offset is None:
1189 1191 # local timezone
1190 1192 unixtime = int(time.mktime(timetuple))
1191 1193 offset = unixtime - localunixtime
1192 1194 else:
1193 1195 unixtime = localunixtime + offset
1194 1196 return unixtime, offset
1195 1197
1196 1198 def parsedate(date, formats=None, defaults=None):
1197 1199 """parse a localized date/time string and return a (unixtime, offset) tuple.
1198 1200
1199 1201 The date may be a "unixtime offset" string or in one of the specified
1200 1202 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1201 1203 """
1202 1204 if not date:
1203 1205 return 0, 0
1204 1206 if isinstance(date, tuple) and len(date) == 2:
1205 1207 return date
1206 1208 if not formats:
1207 1209 formats = defaultdateformats
1208 1210 date = date.strip()
1209 1211 try:
1210 1212 when, offset = map(int, date.split(' '))
1211 1213 except ValueError:
1212 1214 # fill out defaults
1213 1215 if not defaults:
1214 1216 defaults = {}
1215 1217 now = makedate()
1216 1218 for part in "d mb yY HI M S".split():
1217 1219 if part not in defaults:
1218 1220 if part[0] in "HMS":
1219 1221 defaults[part] = "00"
1220 1222 else:
1221 1223 defaults[part] = datestr(now, "%" + part[0])
1222 1224
1223 1225 for format in formats:
1224 1226 try:
1225 1227 when, offset = strdate(date, format, defaults)
1226 1228 except (ValueError, OverflowError):
1227 1229 pass
1228 1230 else:
1229 1231 break
1230 1232 else:
1231 1233 raise Abort(_('invalid date: %r ') % date)
1232 1234 # validate explicit (probably user-specified) date and
1233 1235 # time zone offset. values must fit in signed 32 bits for
1234 1236 # current 32-bit linux runtimes. timezones go from UTC-12
1235 1237 # to UTC+14
1236 1238 if abs(when) > 0x7fffffff:
1237 1239 raise Abort(_('date exceeds 32 bits: %d') % when)
1238 1240 if offset < -50400 or offset > 43200:
1239 1241 raise Abort(_('impossible time zone offset: %d') % offset)
1240 1242 return when, offset
1241 1243
1242 1244 def matchdate(date):
1243 1245 """Return a function that matches a given date match specifier
1244 1246
1245 1247 Formats include:
1246 1248
1247 1249 '{date}' match a given date to the accuracy provided
1248 1250
1249 1251 '<{date}' on or before a given date
1250 1252
1251 1253 '>{date}' on or after a given date
1252 1254
1253 1255 """
1254 1256
1255 1257 def lower(date):
1256 1258 d = dict(mb="1", d="1")
1257 1259 return parsedate(date, extendeddateformats, d)[0]
1258 1260
1259 1261 def upper(date):
1260 1262 d = dict(mb="12", HI="23", M="59", S="59")
1261 1263 for days in "31 30 29".split():
1262 1264 try:
1263 1265 d["d"] = days
1264 1266 return parsedate(date, extendeddateformats, d)[0]
1265 1267 except:
1266 1268 pass
1267 1269 d["d"] = "28"
1268 1270 return parsedate(date, extendeddateformats, d)[0]
1269 1271
1270 1272 date = date.strip()
1271 1273 if date[0] == "<":
1272 1274 when = upper(date[1:])
1273 1275 return lambda x: x <= when
1274 1276 elif date[0] == ">":
1275 1277 when = lower(date[1:])
1276 1278 return lambda x: x >= when
1277 1279 elif date[0] == "-":
1278 1280 try:
1279 1281 days = int(date[1:])
1280 1282 except ValueError:
1281 1283 raise Abort(_("invalid day spec: %s") % date[1:])
1282 1284 when = makedate()[0] - days * 3600 * 24
1283 1285 return lambda x: x >= when
1284 1286 elif " to " in date:
1285 1287 a, b = date.split(" to ")
1286 1288 start, stop = lower(a), upper(b)
1287 1289 return lambda x: x >= start and x <= stop
1288 1290 else:
1289 1291 start, stop = lower(date), upper(date)
1290 1292 return lambda x: x >= start and x <= stop
1291 1293
1292 1294 def shortuser(user):
1293 1295 """Return a short representation of a user name or email address."""
1294 1296 f = user.find('@')
1295 1297 if f >= 0:
1296 1298 user = user[:f]
1297 1299 f = user.find('<')
1298 1300 if f >= 0:
1299 1301 user = user[f+1:]
1300 1302 f = user.find(' ')
1301 1303 if f >= 0:
1302 1304 user = user[:f]
1303 1305 f = user.find('.')
1304 1306 if f >= 0:
1305 1307 user = user[:f]
1306 1308 return user
1307 1309
1308 1310 def email(author):
1309 1311 '''get email of author.'''
1310 1312 r = author.find('>')
1311 1313 if r == -1: r = None
1312 1314 return author[author.find('<')+1:r]
1313 1315
1314 1316 def ellipsis(text, maxlength=400):
1315 1317 """Trim string to at most maxlength (default: 400) characters."""
1316 1318 if len(text) <= maxlength:
1317 1319 return text
1318 1320 else:
1319 1321 return "%s..." % (text[:maxlength-3])
1320 1322
1321 1323 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
1322 1324 '''yield every hg repository under path, recursively.'''
1323 1325 def errhandler(err):
1324 1326 if err.filename == path:
1325 1327 raise err
1326 1328 if followsym and hasattr(os.path, 'samestat'):
1327 1329 def _add_dir_if_not_there(dirlst, dirname):
1328 1330 match = False
1329 1331 samestat = os.path.samestat
1330 1332 dirstat = os.stat(dirname)
1331 1333 for lstdirstat in dirlst:
1332 1334 if samestat(dirstat, lstdirstat):
1333 1335 match = True
1334 1336 break
1335 1337 if not match:
1336 1338 dirlst.append(dirstat)
1337 1339 return not match
1338 1340 else:
1339 1341 followsym = False
1340 1342
1341 1343 if (seen_dirs is None) and followsym:
1342 1344 seen_dirs = []
1343 1345 _add_dir_if_not_there(seen_dirs, path)
1344 1346 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
1345 1347 if '.hg' in dirs:
1346 1348 yield root # found a repository
1347 1349 qroot = os.path.join(root, '.hg', 'patches')
1348 1350 if os.path.isdir(os.path.join(qroot, '.hg')):
1349 1351 yield qroot # we have a patch queue repo here
1350 1352 if recurse:
1351 1353 # avoid recursing inside the .hg directory
1352 1354 dirs.remove('.hg')
1353 1355 else:
1354 1356 dirs[:] = [] # don't descend further
1355 1357 elif followsym:
1356 1358 newdirs = []
1357 1359 for d in dirs:
1358 1360 fname = os.path.join(root, d)
1359 1361 if _add_dir_if_not_there(seen_dirs, fname):
1360 1362 if os.path.islink(fname):
1361 1363 for hgname in walkrepos(fname, True, seen_dirs):
1362 1364 yield hgname
1363 1365 else:
1364 1366 newdirs.append(d)
1365 1367 dirs[:] = newdirs
1366 1368
1367 1369 _rcpath = None
1368 1370
1369 1371 def os_rcpath():
1370 1372 '''return default os-specific hgrc search path'''
1371 1373 path = system_rcpath()
1372 1374 path.extend(user_rcpath())
1373 1375 path = [os.path.normpath(f) for f in path]
1374 1376 return path
1375 1377
1376 1378 def rcpath():
1377 1379 '''return hgrc search path. if env var HGRCPATH is set, use it.
1378 1380 for each item in path, if directory, use files ending in .rc,
1379 1381 else use item.
1380 1382 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1381 1383 if no HGRCPATH, use default os-specific path.'''
1382 1384 global _rcpath
1383 1385 if _rcpath is None:
1384 1386 if 'HGRCPATH' in os.environ:
1385 1387 _rcpath = []
1386 1388 for p in os.environ['HGRCPATH'].split(os.pathsep):
1387 1389 if not p: continue
1388 1390 if os.path.isdir(p):
1389 1391 for f, kind in osutil.listdir(p):
1390 1392 if f.endswith('.rc'):
1391 1393 _rcpath.append(os.path.join(p, f))
1392 1394 else:
1393 1395 _rcpath.append(p)
1394 1396 else:
1395 1397 _rcpath = os_rcpath()
1396 1398 return _rcpath
1397 1399
1398 1400 def bytecount(nbytes):
1399 1401 '''return byte count formatted as readable string, with units'''
1400 1402
1401 1403 units = (
1402 1404 (100, 1<<30, _('%.0f GB')),
1403 1405 (10, 1<<30, _('%.1f GB')),
1404 1406 (1, 1<<30, _('%.2f GB')),
1405 1407 (100, 1<<20, _('%.0f MB')),
1406 1408 (10, 1<<20, _('%.1f MB')),
1407 1409 (1, 1<<20, _('%.2f MB')),
1408 1410 (100, 1<<10, _('%.0f KB')),
1409 1411 (10, 1<<10, _('%.1f KB')),
1410 1412 (1, 1<<10, _('%.2f KB')),
1411 1413 (1, 1, _('%.0f bytes')),
1412 1414 )
1413 1415
1414 1416 for multiplier, divisor, format in units:
1415 1417 if nbytes >= divisor * multiplier:
1416 1418 return format % (nbytes / float(divisor))
1417 1419 return units[-1][2] % nbytes
1418 1420
1419 1421 def drop_scheme(scheme, path):
1420 1422 sc = scheme + ':'
1421 1423 if path.startswith(sc):
1422 1424 path = path[len(sc):]
1423 1425 if path.startswith('//'):
1424 1426 path = path[2:]
1425 1427 return path
1426 1428
1427 1429 def uirepr(s):
1428 1430 # Avoid double backslash in Windows path repr()
1429 1431 return repr(s).replace('\\\\', '\\')
1430 1432
1431 1433 def termwidth():
1432 1434 if 'COLUMNS' in os.environ:
1433 1435 try:
1434 1436 return int(os.environ['COLUMNS'])
1435 1437 except ValueError:
1436 1438 pass
1437 1439 try:
1438 1440 import termios, array, fcntl
1439 1441 for dev in (sys.stdout, sys.stdin):
1440 1442 try:
1441 1443 fd = dev.fileno()
1442 1444 if not os.isatty(fd):
1443 1445 continue
1444 1446 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
1445 1447 return array.array('h', arri)[1]
1446 1448 except ValueError:
1447 1449 pass
1448 1450 except ImportError:
1449 1451 pass
1450 1452 return 80
1451 1453
1452 1454 def iterlines(iterator):
1453 1455 for chunk in iterator:
1454 1456 for line in chunk.splitlines():
1455 1457 yield line
General Comments 0
You need to be logged in to leave comments. Login now