##// END OF EJS Templates
util: fix ellipsis() not to break multi-byte sequence (issue2564)...
Yuya Nishihara -
r13225:e3bf1670 1.7.3 stable
parent child Browse files
Show More
@@ -1,1527 +1,1538 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 or any later version.
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, encoding
18 18 import errno, re, shutil, sys, tempfile, traceback
19 19 import os, stat, time, calendar, textwrap, unicodedata, signal
20 20 import imp, socket
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 if sys.version_info >= (2, 5):
32 32 from hashlib import sha1 as _sha1
33 33 else:
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 __builtin__
40 40
41 41 if sys.version_info[0] < 3:
42 42 def fakebuffer(sliceable, offset=0):
43 43 return sliceable[offset:]
44 44 else:
45 45 def fakebuffer(sliceable, offset=0):
46 46 return memoryview(sliceable)[offset:]
47 47 try:
48 48 buffer
49 49 except NameError:
50 50 __builtin__.buffer = fakebuffer
51 51
52 52 import subprocess
53 53 closefds = os.name == 'posix'
54 54
55 55 def popen2(cmd, env=None, newlines=False):
56 56 # Setting bufsize to -1 lets the system decide the buffer size.
57 57 # The default for bufsize is 0, meaning unbuffered. This leads to
58 58 # poor performance on Mac OS X: http://bugs.python.org/issue4194
59 59 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
60 60 close_fds=closefds,
61 61 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
62 62 universal_newlines=newlines,
63 63 env=env)
64 64 return p.stdin, p.stdout
65 65
66 66 def popen3(cmd, env=None, newlines=False):
67 67 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
68 68 close_fds=closefds,
69 69 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
70 70 stderr=subprocess.PIPE,
71 71 universal_newlines=newlines,
72 72 env=env)
73 73 return p.stdin, p.stdout, p.stderr
74 74
75 75 def version():
76 76 """Return version information if available."""
77 77 try:
78 78 import __version__
79 79 return __version__.version
80 80 except ImportError:
81 81 return 'unknown'
82 82
83 83 # used by parsedate
84 84 defaultdateformats = (
85 85 '%Y-%m-%d %H:%M:%S',
86 86 '%Y-%m-%d %I:%M:%S%p',
87 87 '%Y-%m-%d %H:%M',
88 88 '%Y-%m-%d %I:%M%p',
89 89 '%Y-%m-%d',
90 90 '%m-%d',
91 91 '%m/%d',
92 92 '%m/%d/%y',
93 93 '%m/%d/%Y',
94 94 '%a %b %d %H:%M:%S %Y',
95 95 '%a %b %d %I:%M:%S%p %Y',
96 96 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
97 97 '%b %d %H:%M:%S %Y',
98 98 '%b %d %I:%M:%S%p %Y',
99 99 '%b %d %H:%M:%S',
100 100 '%b %d %I:%M:%S%p',
101 101 '%b %d %H:%M',
102 102 '%b %d %I:%M%p',
103 103 '%b %d %Y',
104 104 '%b %d',
105 105 '%H:%M:%S',
106 106 '%I:%M:%S%p',
107 107 '%H:%M',
108 108 '%I:%M%p',
109 109 )
110 110
111 111 extendeddateformats = defaultdateformats + (
112 112 "%Y",
113 113 "%Y-%m",
114 114 "%b",
115 115 "%b %Y",
116 116 )
117 117
118 118 def cachefunc(func):
119 119 '''cache the result of function calls'''
120 120 # XXX doesn't handle keywords args
121 121 cache = {}
122 122 if func.func_code.co_argcount == 1:
123 123 # we gain a small amount of time because
124 124 # we don't need to pack/unpack the list
125 125 def f(arg):
126 126 if arg not in cache:
127 127 cache[arg] = func(arg)
128 128 return cache[arg]
129 129 else:
130 130 def f(*args):
131 131 if args not in cache:
132 132 cache[args] = func(*args)
133 133 return cache[args]
134 134
135 135 return f
136 136
137 137 def lrucachefunc(func):
138 138 '''cache most recent results of function calls'''
139 139 cache = {}
140 140 order = []
141 141 if func.func_code.co_argcount == 1:
142 142 def f(arg):
143 143 if arg not in cache:
144 144 if len(cache) > 20:
145 145 del cache[order.pop(0)]
146 146 cache[arg] = func(arg)
147 147 else:
148 148 order.remove(arg)
149 149 order.append(arg)
150 150 return cache[arg]
151 151 else:
152 152 def f(*args):
153 153 if args not in cache:
154 154 if len(cache) > 20:
155 155 del cache[order.pop(0)]
156 156 cache[args] = func(*args)
157 157 else:
158 158 order.remove(args)
159 159 order.append(args)
160 160 return cache[args]
161 161
162 162 return f
163 163
164 164 class propertycache(object):
165 165 def __init__(self, func):
166 166 self.func = func
167 167 self.name = func.__name__
168 168 def __get__(self, obj, type=None):
169 169 result = self.func(obj)
170 170 setattr(obj, self.name, result)
171 171 return result
172 172
173 173 def pipefilter(s, cmd):
174 174 '''filter string S through command CMD, returning its output'''
175 175 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
176 176 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
177 177 pout, perr = p.communicate(s)
178 178 return pout
179 179
180 180 def tempfilter(s, cmd):
181 181 '''filter string S through a pair of temporary files with CMD.
182 182 CMD is used as a template to create the real command to be run,
183 183 with the strings INFILE and OUTFILE replaced by the real names of
184 184 the temporary files generated.'''
185 185 inname, outname = None, None
186 186 try:
187 187 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
188 188 fp = os.fdopen(infd, 'wb')
189 189 fp.write(s)
190 190 fp.close()
191 191 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
192 192 os.close(outfd)
193 193 cmd = cmd.replace('INFILE', inname)
194 194 cmd = cmd.replace('OUTFILE', outname)
195 195 code = os.system(cmd)
196 196 if sys.platform == 'OpenVMS' and code & 1:
197 197 code = 0
198 198 if code:
199 199 raise Abort(_("command '%s' failed: %s") %
200 200 (cmd, explain_exit(code)))
201 201 return open(outname, 'rb').read()
202 202 finally:
203 203 try:
204 204 if inname:
205 205 os.unlink(inname)
206 206 except:
207 207 pass
208 208 try:
209 209 if outname:
210 210 os.unlink(outname)
211 211 except:
212 212 pass
213 213
214 214 filtertable = {
215 215 'tempfile:': tempfilter,
216 216 'pipe:': pipefilter,
217 217 }
218 218
219 219 def filter(s, cmd):
220 220 "filter a string through a command that transforms its input to its output"
221 221 for name, fn in filtertable.iteritems():
222 222 if cmd.startswith(name):
223 223 return fn(s, cmd[len(name):].lstrip())
224 224 return pipefilter(s, cmd)
225 225
226 226 def binary(s):
227 227 """return true if a string is binary data"""
228 228 return bool(s and '\0' in s)
229 229
230 230 def increasingchunks(source, min=1024, max=65536):
231 231 '''return no less than min bytes per chunk while data remains,
232 232 doubling min after each chunk until it reaches max'''
233 233 def log2(x):
234 234 if not x:
235 235 return 0
236 236 i = 0
237 237 while x:
238 238 x >>= 1
239 239 i += 1
240 240 return i - 1
241 241
242 242 buf = []
243 243 blen = 0
244 244 for chunk in source:
245 245 buf.append(chunk)
246 246 blen += len(chunk)
247 247 if blen >= min:
248 248 if min < max:
249 249 min = min << 1
250 250 nmin = 1 << log2(blen)
251 251 if nmin > min:
252 252 min = nmin
253 253 if min > max:
254 254 min = max
255 255 yield ''.join(buf)
256 256 blen = 0
257 257 buf = []
258 258 if buf:
259 259 yield ''.join(buf)
260 260
261 261 Abort = error.Abort
262 262
263 263 def always(fn):
264 264 return True
265 265
266 266 def never(fn):
267 267 return False
268 268
269 269 def pathto(root, n1, n2):
270 270 '''return the relative path from one place to another.
271 271 root should use os.sep to separate directories
272 272 n1 should use os.sep to separate directories
273 273 n2 should use "/" to separate directories
274 274 returns an os.sep-separated path.
275 275
276 276 If n1 is a relative path, it's assumed it's
277 277 relative to root.
278 278 n2 should always be relative to root.
279 279 '''
280 280 if not n1:
281 281 return localpath(n2)
282 282 if os.path.isabs(n1):
283 283 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
284 284 return os.path.join(root, localpath(n2))
285 285 n2 = '/'.join((pconvert(root), n2))
286 286 a, b = splitpath(n1), n2.split('/')
287 287 a.reverse()
288 288 b.reverse()
289 289 while a and b and a[-1] == b[-1]:
290 290 a.pop()
291 291 b.pop()
292 292 b.reverse()
293 293 return os.sep.join((['..'] * len(a)) + b) or '.'
294 294
295 295 def canonpath(root, cwd, myname, auditor=None):
296 296 """return the canonical path of myname, given cwd and root"""
297 297 if endswithsep(root):
298 298 rootsep = root
299 299 else:
300 300 rootsep = root + os.sep
301 301 name = myname
302 302 if not os.path.isabs(name):
303 303 name = os.path.join(root, cwd, name)
304 304 name = os.path.normpath(name)
305 305 if auditor is None:
306 306 auditor = path_auditor(root)
307 307 if name != rootsep and name.startswith(rootsep):
308 308 name = name[len(rootsep):]
309 309 auditor(name)
310 310 return pconvert(name)
311 311 elif name == root:
312 312 return ''
313 313 else:
314 314 # Determine whether `name' is in the hierarchy at or beneath `root',
315 315 # by iterating name=dirname(name) until that causes no change (can't
316 316 # check name == '/', because that doesn't work on windows). For each
317 317 # `name', compare dev/inode numbers. If they match, the list `rel'
318 318 # holds the reversed list of components making up the relative file
319 319 # name we want.
320 320 root_st = os.stat(root)
321 321 rel = []
322 322 while True:
323 323 try:
324 324 name_st = os.stat(name)
325 325 except OSError:
326 326 break
327 327 if samestat(name_st, root_st):
328 328 if not rel:
329 329 # name was actually the same as root (maybe a symlink)
330 330 return ''
331 331 rel.reverse()
332 332 name = os.path.join(*rel)
333 333 auditor(name)
334 334 return pconvert(name)
335 335 dirname, basename = os.path.split(name)
336 336 rel.append(basename)
337 337 if dirname == name:
338 338 break
339 339 name = dirname
340 340
341 341 raise Abort('%s not under root' % myname)
342 342
343 343 _hgexecutable = None
344 344
345 345 def main_is_frozen():
346 346 """return True if we are a frozen executable.
347 347
348 348 The code supports py2exe (most common, Windows only) and tools/freeze
349 349 (portable, not much used).
350 350 """
351 351 return (hasattr(sys, "frozen") or # new py2exe
352 352 hasattr(sys, "importers") or # old py2exe
353 353 imp.is_frozen("__main__")) # tools/freeze
354 354
355 355 def hgexecutable():
356 356 """return location of the 'hg' executable.
357 357
358 358 Defaults to $HG or 'hg' in the search path.
359 359 """
360 360 if _hgexecutable is None:
361 361 hg = os.environ.get('HG')
362 362 if hg:
363 363 set_hgexecutable(hg)
364 364 elif main_is_frozen():
365 365 set_hgexecutable(sys.executable)
366 366 else:
367 367 exe = find_exe('hg') or os.path.basename(sys.argv[0])
368 368 set_hgexecutable(exe)
369 369 return _hgexecutable
370 370
371 371 def set_hgexecutable(path):
372 372 """set location of the 'hg' executable"""
373 373 global _hgexecutable
374 374 _hgexecutable = path
375 375
376 376 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None, out=None):
377 377 '''enhanced shell command execution.
378 378 run with environment maybe modified, maybe in different dir.
379 379
380 380 if command fails and onerr is None, return status. if ui object,
381 381 print error message and return status, else raise onerr object as
382 382 exception.
383 383
384 384 if out is specified, it is assumed to be a file-like object that has a
385 385 write() method. stdout and stderr will be redirected to out.'''
386 386 def py2shell(val):
387 387 'convert python object into string that is useful to shell'
388 388 if val is None or val is False:
389 389 return '0'
390 390 if val is True:
391 391 return '1'
392 392 return str(val)
393 393 origcmd = cmd
394 394 cmd = quotecommand(cmd)
395 395 env = dict(os.environ)
396 396 env.update((k, py2shell(v)) for k, v in environ.iteritems())
397 397 env['HG'] = hgexecutable()
398 398 if out is None:
399 399 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
400 400 env=env, cwd=cwd)
401 401 else:
402 402 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
403 403 env=env, cwd=cwd, stdout=subprocess.PIPE,
404 404 stderr=subprocess.STDOUT)
405 405 for line in proc.stdout:
406 406 out.write(line)
407 407 proc.wait()
408 408 rc = proc.returncode
409 409 if sys.platform == 'OpenVMS' and rc & 1:
410 410 rc = 0
411 411 if rc and onerr:
412 412 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
413 413 explain_exit(rc)[0])
414 414 if errprefix:
415 415 errmsg = '%s: %s' % (errprefix, errmsg)
416 416 try:
417 417 onerr.warn(errmsg + '\n')
418 418 except AttributeError:
419 419 raise onerr(errmsg)
420 420 return rc
421 421
422 422 def checksignature(func):
423 423 '''wrap a function with code to check for calling errors'''
424 424 def check(*args, **kwargs):
425 425 try:
426 426 return func(*args, **kwargs)
427 427 except TypeError:
428 428 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
429 429 raise error.SignatureError
430 430 raise
431 431
432 432 return check
433 433
434 434 def unlink(f):
435 435 """unlink and remove the directory if it is empty"""
436 436 os.unlink(f)
437 437 # try removing directories that might now be empty
438 438 try:
439 439 os.removedirs(os.path.dirname(f))
440 440 except OSError:
441 441 pass
442 442
443 443 def copyfile(src, dest):
444 444 "copy a file, preserving mode and atime/mtime"
445 445 if os.path.islink(src):
446 446 try:
447 447 os.unlink(dest)
448 448 except:
449 449 pass
450 450 os.symlink(os.readlink(src), dest)
451 451 else:
452 452 try:
453 453 shutil.copyfile(src, dest)
454 454 shutil.copystat(src, dest)
455 455 except shutil.Error, inst:
456 456 raise Abort(str(inst))
457 457
458 458 def copyfiles(src, dst, hardlink=None):
459 459 """Copy a directory tree using hardlinks if possible"""
460 460
461 461 if hardlink is None:
462 462 hardlink = (os.stat(src).st_dev ==
463 463 os.stat(os.path.dirname(dst)).st_dev)
464 464
465 465 num = 0
466 466 if os.path.isdir(src):
467 467 os.mkdir(dst)
468 468 for name, kind in osutil.listdir(src):
469 469 srcname = os.path.join(src, name)
470 470 dstname = os.path.join(dst, name)
471 471 hardlink, n = copyfiles(srcname, dstname, hardlink)
472 472 num += n
473 473 else:
474 474 if hardlink:
475 475 try:
476 476 os_link(src, dst)
477 477 except (IOError, OSError):
478 478 hardlink = False
479 479 shutil.copy(src, dst)
480 480 else:
481 481 shutil.copy(src, dst)
482 482 num += 1
483 483
484 484 return hardlink, num
485 485
486 486 class path_auditor(object):
487 487 '''ensure that a filesystem path contains no banned components.
488 488 the following properties of a path are checked:
489 489
490 490 - under top-level .hg
491 491 - starts at the root of a windows drive
492 492 - contains ".."
493 493 - traverses a symlink (e.g. a/symlink_here/b)
494 494 - inside a nested repository (a callback can be used to approve
495 495 some nested repositories, e.g., subrepositories)
496 496 '''
497 497
498 498 def __init__(self, root, callback=None):
499 499 self.audited = set()
500 500 self.auditeddir = set()
501 501 self.root = root
502 502 self.callback = callback
503 503
504 504 def __call__(self, path):
505 505 if path in self.audited:
506 506 return
507 507 normpath = os.path.normcase(path)
508 508 parts = splitpath(normpath)
509 509 if (os.path.splitdrive(path)[0]
510 510 or parts[0].lower() in ('.hg', '.hg.', '')
511 511 or os.pardir in parts):
512 512 raise Abort(_("path contains illegal component: %s") % path)
513 513 if '.hg' in path.lower():
514 514 lparts = [p.lower() for p in parts]
515 515 for p in '.hg', '.hg.':
516 516 if p in lparts[1:]:
517 517 pos = lparts.index(p)
518 518 base = os.path.join(*parts[:pos])
519 519 raise Abort(_('path %r is inside repo %r') % (path, base))
520 520 def check(prefix):
521 521 curpath = os.path.join(self.root, prefix)
522 522 try:
523 523 st = os.lstat(curpath)
524 524 except OSError, err:
525 525 # EINVAL can be raised as invalid path syntax under win32.
526 526 # They must be ignored for patterns can be checked too.
527 527 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
528 528 raise
529 529 else:
530 530 if stat.S_ISLNK(st.st_mode):
531 531 raise Abort(_('path %r traverses symbolic link %r') %
532 532 (path, prefix))
533 533 elif (stat.S_ISDIR(st.st_mode) and
534 534 os.path.isdir(os.path.join(curpath, '.hg'))):
535 535 if not self.callback or not self.callback(curpath):
536 536 raise Abort(_('path %r is inside repo %r') %
537 537 (path, prefix))
538 538 parts.pop()
539 539 prefixes = []
540 540 while parts:
541 541 prefix = os.sep.join(parts)
542 542 if prefix in self.auditeddir:
543 543 break
544 544 check(prefix)
545 545 prefixes.append(prefix)
546 546 parts.pop()
547 547
548 548 self.audited.add(path)
549 549 # only add prefixes to the cache after checking everything: we don't
550 550 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
551 551 self.auditeddir.update(prefixes)
552 552
553 553 def nlinks(pathname):
554 554 """Return number of hardlinks for the given file."""
555 555 return os.lstat(pathname).st_nlink
556 556
557 557 if hasattr(os, 'link'):
558 558 os_link = os.link
559 559 else:
560 560 def os_link(src, dst):
561 561 raise OSError(0, _("Hardlinks not supported"))
562 562
563 563 def lookup_reg(key, name=None, scope=None):
564 564 return None
565 565
566 566 def hidewindow():
567 567 """Hide current shell window.
568 568
569 569 Used to hide the window opened when starting asynchronous
570 570 child process under Windows, unneeded on other systems.
571 571 """
572 572 pass
573 573
574 574 if os.name == 'nt':
575 575 from windows import *
576 576 else:
577 577 from posix import *
578 578
579 579 def makelock(info, pathname):
580 580 try:
581 581 return os.symlink(info, pathname)
582 582 except OSError, why:
583 583 if why.errno == errno.EEXIST:
584 584 raise
585 585 except AttributeError: # no symlink in os
586 586 pass
587 587
588 588 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
589 589 os.write(ld, info)
590 590 os.close(ld)
591 591
592 592 def readlock(pathname):
593 593 try:
594 594 return os.readlink(pathname)
595 595 except OSError, why:
596 596 if why.errno not in (errno.EINVAL, errno.ENOSYS):
597 597 raise
598 598 except AttributeError: # no symlink in os
599 599 pass
600 600 return posixfile(pathname).read()
601 601
602 602 def fstat(fp):
603 603 '''stat file object that may not have fileno method.'''
604 604 try:
605 605 return os.fstat(fp.fileno())
606 606 except AttributeError:
607 607 return os.stat(fp.name)
608 608
609 609 # File system features
610 610
611 611 def checkcase(path):
612 612 """
613 613 Check whether the given path is on a case-sensitive filesystem
614 614
615 615 Requires a path (like /foo/.hg) ending with a foldable final
616 616 directory component.
617 617 """
618 618 s1 = os.stat(path)
619 619 d, b = os.path.split(path)
620 620 p2 = os.path.join(d, b.upper())
621 621 if path == p2:
622 622 p2 = os.path.join(d, b.lower())
623 623 try:
624 624 s2 = os.stat(p2)
625 625 if s2 == s1:
626 626 return False
627 627 return True
628 628 except:
629 629 return True
630 630
631 631 _fspathcache = {}
632 632 def fspath(name, root):
633 633 '''Get name in the case stored in the filesystem
634 634
635 635 The name is either relative to root, or it is an absolute path starting
636 636 with root. Note that this function is unnecessary, and should not be
637 637 called, for case-sensitive filesystems (simply because it's expensive).
638 638 '''
639 639 # If name is absolute, make it relative
640 640 if name.lower().startswith(root.lower()):
641 641 l = len(root)
642 642 if name[l] == os.sep or name[l] == os.altsep:
643 643 l = l + 1
644 644 name = name[l:]
645 645
646 646 if not os.path.lexists(os.path.join(root, name)):
647 647 return None
648 648
649 649 seps = os.sep
650 650 if os.altsep:
651 651 seps = seps + os.altsep
652 652 # Protect backslashes. This gets silly very quickly.
653 653 seps.replace('\\','\\\\')
654 654 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
655 655 dir = os.path.normcase(os.path.normpath(root))
656 656 result = []
657 657 for part, sep in pattern.findall(name):
658 658 if sep:
659 659 result.append(sep)
660 660 continue
661 661
662 662 if dir not in _fspathcache:
663 663 _fspathcache[dir] = os.listdir(dir)
664 664 contents = _fspathcache[dir]
665 665
666 666 lpart = part.lower()
667 667 lenp = len(part)
668 668 for n in contents:
669 669 if lenp == len(n) and n.lower() == lpart:
670 670 result.append(n)
671 671 break
672 672 else:
673 673 # Cannot happen, as the file exists!
674 674 result.append(part)
675 675 dir = os.path.join(dir, lpart)
676 676
677 677 return ''.join(result)
678 678
679 679 def checkexec(path):
680 680 """
681 681 Check whether the given path is on a filesystem with UNIX-like exec flags
682 682
683 683 Requires a directory (like /foo/.hg)
684 684 """
685 685
686 686 # VFAT on some Linux versions can flip mode but it doesn't persist
687 687 # a FS remount. Frequently we can detect it if files are created
688 688 # with exec bit on.
689 689
690 690 try:
691 691 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
692 692 fh, fn = tempfile.mkstemp(dir=path, prefix='hg-checkexec-')
693 693 try:
694 694 os.close(fh)
695 695 m = os.stat(fn).st_mode & 0777
696 696 new_file_has_exec = m & EXECFLAGS
697 697 os.chmod(fn, m ^ EXECFLAGS)
698 698 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
699 699 finally:
700 700 os.unlink(fn)
701 701 except (IOError, OSError):
702 702 # we don't care, the user probably won't be able to commit anyway
703 703 return False
704 704 return not (new_file_has_exec or exec_flags_cannot_flip)
705 705
706 706 def checklink(path):
707 707 """check whether the given path is on a symlink-capable filesystem"""
708 708 # mktemp is not racy because symlink creation will fail if the
709 709 # file already exists
710 710 name = tempfile.mktemp(dir=path, prefix='hg-checklink-')
711 711 try:
712 712 os.symlink(".", name)
713 713 os.unlink(name)
714 714 return True
715 715 except (OSError, AttributeError):
716 716 return False
717 717
718 718 def checknlink(testfile):
719 719 '''check whether hardlink count reporting works properly'''
720 720
721 721 # testfile may be open, so we need a separate file for checking to
722 722 # work around issue2543 (or testfile may get lost on Samba shares)
723 723 f1 = testfile + ".hgtmp1"
724 724 if os.path.lexists(f1):
725 725 return False
726 726 try:
727 727 posixfile(f1, 'w').close()
728 728 except IOError:
729 729 return False
730 730
731 731 f2 = testfile + ".hgtmp2"
732 732 fd = None
733 733 try:
734 734 try:
735 735 os_link(f1, f2)
736 736 except OSError:
737 737 return False
738 738
739 739 # nlinks() may behave differently for files on Windows shares if
740 740 # the file is open.
741 741 fd = open(f2)
742 742 return nlinks(f2) > 1
743 743 finally:
744 744 if fd is not None:
745 745 fd.close()
746 746 for f in (f1, f2):
747 747 try:
748 748 os.unlink(f)
749 749 except OSError:
750 750 pass
751 751
752 752 return False
753 753
754 754 def endswithsep(path):
755 755 '''Check path ends with os.sep or os.altsep.'''
756 756 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
757 757
758 758 def splitpath(path):
759 759 '''Split path by os.sep.
760 760 Note that this function does not use os.altsep because this is
761 761 an alternative of simple "xxx.split(os.sep)".
762 762 It is recommended to use os.path.normpath() before using this
763 763 function if need.'''
764 764 return path.split(os.sep)
765 765
766 766 def gui():
767 767 '''Are we running in a GUI?'''
768 768 return os.name == "nt" or os.name == "mac" or os.environ.get("DISPLAY")
769 769
770 770 def mktempcopy(name, emptyok=False, createmode=None):
771 771 """Create a temporary file with the same contents from name
772 772
773 773 The permission bits are copied from the original file.
774 774
775 775 If the temporary file is going to be truncated immediately, you
776 776 can use emptyok=True as an optimization.
777 777
778 778 Returns the name of the temporary file.
779 779 """
780 780 d, fn = os.path.split(name)
781 781 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
782 782 os.close(fd)
783 783 # Temporary files are created with mode 0600, which is usually not
784 784 # what we want. If the original file already exists, just copy
785 785 # its mode. Otherwise, manually obey umask.
786 786 try:
787 787 st_mode = os.lstat(name).st_mode & 0777
788 788 except OSError, inst:
789 789 if inst.errno != errno.ENOENT:
790 790 raise
791 791 st_mode = createmode
792 792 if st_mode is None:
793 793 st_mode = ~umask
794 794 st_mode &= 0666
795 795 os.chmod(temp, st_mode)
796 796 if emptyok:
797 797 return temp
798 798 try:
799 799 try:
800 800 ifp = posixfile(name, "rb")
801 801 except IOError, inst:
802 802 if inst.errno == errno.ENOENT:
803 803 return temp
804 804 if not getattr(inst, 'filename', None):
805 805 inst.filename = name
806 806 raise
807 807 ofp = posixfile(temp, "wb")
808 808 for chunk in filechunkiter(ifp):
809 809 ofp.write(chunk)
810 810 ifp.close()
811 811 ofp.close()
812 812 except:
813 813 try: os.unlink(temp)
814 814 except: pass
815 815 raise
816 816 return temp
817 817
818 818 class atomictempfile(object):
819 819 """file-like object that atomically updates a file
820 820
821 821 All writes will be redirected to a temporary copy of the original
822 822 file. When rename is called, the copy is renamed to the original
823 823 name, making the changes visible.
824 824 """
825 825 def __init__(self, name, mode='w+b', createmode=None):
826 826 self.__name = name
827 827 self._fp = None
828 828 self.temp = mktempcopy(name, emptyok=('w' in mode),
829 829 createmode=createmode)
830 830 self._fp = posixfile(self.temp, mode)
831 831
832 832 def __getattr__(self, name):
833 833 return getattr(self._fp, name)
834 834
835 835 def rename(self):
836 836 if not self._fp.closed:
837 837 self._fp.close()
838 838 rename(self.temp, localpath(self.__name))
839 839
840 840 def __del__(self):
841 841 if not self._fp:
842 842 return
843 843 if not self._fp.closed:
844 844 try:
845 845 os.unlink(self.temp)
846 846 except: pass
847 847 self._fp.close()
848 848
849 849 def makedirs(name, mode=None):
850 850 """recursive directory creation with parent mode inheritance"""
851 851 parent = os.path.abspath(os.path.dirname(name))
852 852 try:
853 853 os.mkdir(name)
854 854 if mode is not None:
855 855 os.chmod(name, mode)
856 856 return
857 857 except OSError, err:
858 858 if err.errno == errno.EEXIST:
859 859 return
860 860 if not name or parent == name or err.errno != errno.ENOENT:
861 861 raise
862 862 makedirs(parent, mode)
863 863 makedirs(name, mode)
864 864
865 865 class opener(object):
866 866 """Open files relative to a base directory
867 867
868 868 This class is used to hide the details of COW semantics and
869 869 remote file access from higher level code.
870 870 """
871 871 def __init__(self, base, audit=True):
872 872 self.base = base
873 873 if audit:
874 874 self.auditor = path_auditor(base)
875 875 else:
876 876 self.auditor = always
877 877 self.createmode = None
878 878 self._trustnlink = None
879 879
880 880 @propertycache
881 881 def _can_symlink(self):
882 882 return checklink(self.base)
883 883
884 884 def _fixfilemode(self, name):
885 885 if self.createmode is None:
886 886 return
887 887 os.chmod(name, self.createmode & 0666)
888 888
889 889 def __call__(self, path, mode="r", text=False, atomictemp=False):
890 890 self.auditor(path)
891 891 f = os.path.join(self.base, path)
892 892
893 893 if not text and "b" not in mode:
894 894 mode += "b" # for that other OS
895 895
896 896 nlink = -1
897 897 st_mode = None
898 898 dirname, basename = os.path.split(f)
899 899 # If basename is empty, then the path is malformed because it points
900 900 # to a directory. Let the posixfile() call below raise IOError.
901 901 if basename and mode not in ('r', 'rb'):
902 902 if atomictemp:
903 903 if not os.path.isdir(dirname):
904 904 makedirs(dirname, self.createmode)
905 905 return atomictempfile(f, mode, self.createmode)
906 906 try:
907 907 if 'w' in mode:
908 908 st_mode = os.lstat(f).st_mode & 0777
909 909 os.unlink(f)
910 910 nlink = 0
911 911 else:
912 912 # nlinks() may behave differently for files on Windows
913 913 # shares if the file is open.
914 914 fd = open(f)
915 915 nlink = nlinks(f)
916 916 fd.close()
917 917 except (OSError, IOError):
918 918 nlink = 0
919 919 if not os.path.isdir(dirname):
920 920 makedirs(dirname, self.createmode)
921 921 if nlink > 0:
922 922 if self._trustnlink is None:
923 923 self._trustnlink = nlink > 1 or checknlink(f)
924 924 if nlink > 1 or not self._trustnlink:
925 925 rename(mktempcopy(f), f)
926 926 fp = posixfile(f, mode)
927 927 if nlink == 0:
928 928 if st_mode is None:
929 929 self._fixfilemode(f)
930 930 else:
931 931 os.chmod(f, st_mode)
932 932 return fp
933 933
934 934 def symlink(self, src, dst):
935 935 self.auditor(dst)
936 936 linkname = os.path.join(self.base, dst)
937 937 try:
938 938 os.unlink(linkname)
939 939 except OSError:
940 940 pass
941 941
942 942 dirname = os.path.dirname(linkname)
943 943 if not os.path.exists(dirname):
944 944 makedirs(dirname, self.createmode)
945 945
946 946 if self._can_symlink:
947 947 try:
948 948 os.symlink(src, linkname)
949 949 except OSError, err:
950 950 raise OSError(err.errno, _('could not symlink to %r: %s') %
951 951 (src, err.strerror), linkname)
952 952 else:
953 953 f = self(dst, "w")
954 954 f.write(src)
955 955 f.close()
956 956 self._fixfilemode(dst)
957 957
958 958 class chunkbuffer(object):
959 959 """Allow arbitrary sized chunks of data to be efficiently read from an
960 960 iterator over chunks of arbitrary size."""
961 961
962 962 def __init__(self, in_iter):
963 963 """in_iter is the iterator that's iterating over the input chunks.
964 964 targetsize is how big a buffer to try to maintain."""
965 965 def splitbig(chunks):
966 966 for chunk in chunks:
967 967 if len(chunk) > 2**20:
968 968 pos = 0
969 969 while pos < len(chunk):
970 970 end = pos + 2 ** 18
971 971 yield chunk[pos:end]
972 972 pos = end
973 973 else:
974 974 yield chunk
975 975 self.iter = splitbig(in_iter)
976 976 self._queue = []
977 977
978 978 def read(self, l):
979 979 """Read L bytes of data from the iterator of chunks of data.
980 980 Returns less than L bytes if the iterator runs dry."""
981 981 left = l
982 982 buf = ''
983 983 queue = self._queue
984 984 while left > 0:
985 985 # refill the queue
986 986 if not queue:
987 987 target = 2**18
988 988 for chunk in self.iter:
989 989 queue.append(chunk)
990 990 target -= len(chunk)
991 991 if target <= 0:
992 992 break
993 993 if not queue:
994 994 break
995 995
996 996 chunk = queue.pop(0)
997 997 left -= len(chunk)
998 998 if left < 0:
999 999 queue.insert(0, chunk[left:])
1000 1000 buf += chunk[:left]
1001 1001 else:
1002 1002 buf += chunk
1003 1003
1004 1004 return buf
1005 1005
1006 1006 def filechunkiter(f, size=65536, limit=None):
1007 1007 """Create a generator that produces the data in the file size
1008 1008 (default 65536) bytes at a time, up to optional limit (default is
1009 1009 to read all data). Chunks may be less than size bytes if the
1010 1010 chunk is the last chunk in the file, or the file is a socket or
1011 1011 some other type of file that sometimes reads less data than is
1012 1012 requested."""
1013 1013 assert size >= 0
1014 1014 assert limit is None or limit >= 0
1015 1015 while True:
1016 1016 if limit is None:
1017 1017 nbytes = size
1018 1018 else:
1019 1019 nbytes = min(limit, size)
1020 1020 s = nbytes and f.read(nbytes)
1021 1021 if not s:
1022 1022 break
1023 1023 if limit:
1024 1024 limit -= len(s)
1025 1025 yield s
1026 1026
1027 1027 def makedate():
1028 1028 lt = time.localtime()
1029 1029 if lt[8] == 1 and time.daylight:
1030 1030 tz = time.altzone
1031 1031 else:
1032 1032 tz = time.timezone
1033 1033 t = time.mktime(lt)
1034 1034 if t < 0:
1035 1035 hint = _("check your clock")
1036 1036 raise Abort(_("negative timestamp: %d") % t, hint=hint)
1037 1037 return t, tz
1038 1038
1039 1039 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1040 1040 """represent a (unixtime, offset) tuple as a localized time.
1041 1041 unixtime is seconds since the epoch, and offset is the time zone's
1042 1042 number of seconds away from UTC. if timezone is false, do not
1043 1043 append time zone to string."""
1044 1044 t, tz = date or makedate()
1045 1045 if t < 0:
1046 1046 t = 0 # time.gmtime(lt) fails on Windows for lt < -43200
1047 1047 tz = 0
1048 1048 if "%1" in format or "%2" in format:
1049 1049 sign = (tz > 0) and "-" or "+"
1050 1050 minutes = abs(tz) // 60
1051 1051 format = format.replace("%1", "%c%02d" % (sign, minutes // 60))
1052 1052 format = format.replace("%2", "%02d" % (minutes % 60))
1053 1053 s = time.strftime(format, time.gmtime(float(t) - tz))
1054 1054 return s
1055 1055
1056 1056 def shortdate(date=None):
1057 1057 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1058 1058 return datestr(date, format='%Y-%m-%d')
1059 1059
1060 1060 def strdate(string, format, defaults=[]):
1061 1061 """parse a localized time string and return a (unixtime, offset) tuple.
1062 1062 if the string cannot be parsed, ValueError is raised."""
1063 1063 def timezone(string):
1064 1064 tz = string.split()[-1]
1065 1065 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1066 1066 sign = (tz[0] == "+") and 1 or -1
1067 1067 hours = int(tz[1:3])
1068 1068 minutes = int(tz[3:5])
1069 1069 return -sign * (hours * 60 + minutes) * 60
1070 1070 if tz == "GMT" or tz == "UTC":
1071 1071 return 0
1072 1072 return None
1073 1073
1074 1074 # NOTE: unixtime = localunixtime + offset
1075 1075 offset, date = timezone(string), string
1076 1076 if offset != None:
1077 1077 date = " ".join(string.split()[:-1])
1078 1078
1079 1079 # add missing elements from defaults
1080 1080 usenow = False # default to using biased defaults
1081 1081 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1082 1082 found = [True for p in part if ("%"+p) in format]
1083 1083 if not found:
1084 1084 date += "@" + defaults[part][usenow]
1085 1085 format += "@%" + part[0]
1086 1086 else:
1087 1087 # We've found a specific time element, less specific time
1088 1088 # elements are relative to today
1089 1089 usenow = True
1090 1090
1091 1091 timetuple = time.strptime(date, format)
1092 1092 localunixtime = int(calendar.timegm(timetuple))
1093 1093 if offset is None:
1094 1094 # local timezone
1095 1095 unixtime = int(time.mktime(timetuple))
1096 1096 offset = unixtime - localunixtime
1097 1097 else:
1098 1098 unixtime = localunixtime + offset
1099 1099 return unixtime, offset
1100 1100
1101 1101 def parsedate(date, formats=None, bias={}):
1102 1102 """parse a localized date/time and return a (unixtime, offset) tuple.
1103 1103
1104 1104 The date may be a "unixtime offset" string or in one of the specified
1105 1105 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1106 1106 """
1107 1107 if not date:
1108 1108 return 0, 0
1109 1109 if isinstance(date, tuple) and len(date) == 2:
1110 1110 return date
1111 1111 if not formats:
1112 1112 formats = defaultdateformats
1113 1113 date = date.strip()
1114 1114 try:
1115 1115 when, offset = map(int, date.split(' '))
1116 1116 except ValueError:
1117 1117 # fill out defaults
1118 1118 now = makedate()
1119 1119 defaults = {}
1120 1120 nowmap = {}
1121 1121 for part in "d mb yY HI M S".split():
1122 1122 # this piece is for rounding the specific end of unknowns
1123 1123 b = bias.get(part)
1124 1124 if b is None:
1125 1125 if part[0] in "HMS":
1126 1126 b = "00"
1127 1127 else:
1128 1128 b = "0"
1129 1129
1130 1130 # this piece is for matching the generic end to today's date
1131 1131 n = datestr(now, "%" + part[0])
1132 1132
1133 1133 defaults[part] = (b, n)
1134 1134
1135 1135 for format in formats:
1136 1136 try:
1137 1137 when, offset = strdate(date, format, defaults)
1138 1138 except (ValueError, OverflowError):
1139 1139 pass
1140 1140 else:
1141 1141 break
1142 1142 else:
1143 1143 raise Abort(_('invalid date: %r') % date)
1144 1144 # validate explicit (probably user-specified) date and
1145 1145 # time zone offset. values must fit in signed 32 bits for
1146 1146 # current 32-bit linux runtimes. timezones go from UTC-12
1147 1147 # to UTC+14
1148 1148 if abs(when) > 0x7fffffff:
1149 1149 raise Abort(_('date exceeds 32 bits: %d') % when)
1150 1150 if when < 0:
1151 1151 raise Abort(_('negative date value: %d') % when)
1152 1152 if offset < -50400 or offset > 43200:
1153 1153 raise Abort(_('impossible time zone offset: %d') % offset)
1154 1154 return when, offset
1155 1155
1156 1156 def matchdate(date):
1157 1157 """Return a function that matches a given date match specifier
1158 1158
1159 1159 Formats include:
1160 1160
1161 1161 '{date}' match a given date to the accuracy provided
1162 1162
1163 1163 '<{date}' on or before a given date
1164 1164
1165 1165 '>{date}' on or after a given date
1166 1166
1167 1167 >>> p1 = parsedate("10:29:59")
1168 1168 >>> p2 = parsedate("10:30:00")
1169 1169 >>> p3 = parsedate("10:30:59")
1170 1170 >>> p4 = parsedate("10:31:00")
1171 1171 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1172 1172 >>> f = matchdate("10:30")
1173 1173 >>> f(p1[0])
1174 1174 False
1175 1175 >>> f(p2[0])
1176 1176 True
1177 1177 >>> f(p3[0])
1178 1178 True
1179 1179 >>> f(p4[0])
1180 1180 False
1181 1181 >>> f(p5[0])
1182 1182 False
1183 1183 """
1184 1184
1185 1185 def lower(date):
1186 1186 d = dict(mb="1", d="1")
1187 1187 return parsedate(date, extendeddateformats, d)[0]
1188 1188
1189 1189 def upper(date):
1190 1190 d = dict(mb="12", HI="23", M="59", S="59")
1191 1191 for days in "31 30 29".split():
1192 1192 try:
1193 1193 d["d"] = days
1194 1194 return parsedate(date, extendeddateformats, d)[0]
1195 1195 except:
1196 1196 pass
1197 1197 d["d"] = "28"
1198 1198 return parsedate(date, extendeddateformats, d)[0]
1199 1199
1200 1200 date = date.strip()
1201 1201 if date[0] == "<":
1202 1202 when = upper(date[1:])
1203 1203 return lambda x: x <= when
1204 1204 elif date[0] == ">":
1205 1205 when = lower(date[1:])
1206 1206 return lambda x: x >= when
1207 1207 elif date[0] == "-":
1208 1208 try:
1209 1209 days = int(date[1:])
1210 1210 except ValueError:
1211 1211 raise Abort(_("invalid day spec: %s") % date[1:])
1212 1212 when = makedate()[0] - days * 3600 * 24
1213 1213 return lambda x: x >= when
1214 1214 elif " to " in date:
1215 1215 a, b = date.split(" to ")
1216 1216 start, stop = lower(a), upper(b)
1217 1217 return lambda x: x >= start and x <= stop
1218 1218 else:
1219 1219 start, stop = lower(date), upper(date)
1220 1220 return lambda x: x >= start and x <= stop
1221 1221
1222 1222 def shortuser(user):
1223 1223 """Return a short representation of a user name or email address."""
1224 1224 f = user.find('@')
1225 1225 if f >= 0:
1226 1226 user = user[:f]
1227 1227 f = user.find('<')
1228 1228 if f >= 0:
1229 1229 user = user[f + 1:]
1230 1230 f = user.find(' ')
1231 1231 if f >= 0:
1232 1232 user = user[:f]
1233 1233 f = user.find('.')
1234 1234 if f >= 0:
1235 1235 user = user[:f]
1236 1236 return user
1237 1237
1238 1238 def email(author):
1239 1239 '''get email of author.'''
1240 1240 r = author.find('>')
1241 1241 if r == -1:
1242 1242 r = None
1243 1243 return author[author.find('<') + 1:r]
1244 1244
1245 def _ellipsis(text, maxlength):
1246 if len(text) <= maxlength:
1247 return text, False
1248 else:
1249 return "%s..." % (text[:maxlength - 3]), True
1250
1245 1251 def ellipsis(text, maxlength=400):
1246 1252 """Trim string to at most maxlength (default: 400) characters."""
1247 if len(text) <= maxlength:
1248 return text
1249 else:
1250 return "%s..." % (text[:maxlength - 3])
1253 try:
1254 # use unicode not to split at intermediate multi-byte sequence
1255 utext, truncated = _ellipsis(text.decode(encoding.encoding),
1256 maxlength)
1257 if not truncated:
1258 return text
1259 return utext.encode(encoding.encoding)
1260 except (UnicodeDecodeError, UnicodeEncodeError):
1261 return _ellipsis(text, maxlength)[0]
1251 1262
1252 1263 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
1253 1264 '''yield every hg repository under path, recursively.'''
1254 1265 def errhandler(err):
1255 1266 if err.filename == path:
1256 1267 raise err
1257 1268 if followsym and hasattr(os.path, 'samestat'):
1258 1269 def _add_dir_if_not_there(dirlst, dirname):
1259 1270 match = False
1260 1271 samestat = os.path.samestat
1261 1272 dirstat = os.stat(dirname)
1262 1273 for lstdirstat in dirlst:
1263 1274 if samestat(dirstat, lstdirstat):
1264 1275 match = True
1265 1276 break
1266 1277 if not match:
1267 1278 dirlst.append(dirstat)
1268 1279 return not match
1269 1280 else:
1270 1281 followsym = False
1271 1282
1272 1283 if (seen_dirs is None) and followsym:
1273 1284 seen_dirs = []
1274 1285 _add_dir_if_not_there(seen_dirs, path)
1275 1286 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
1276 1287 dirs.sort()
1277 1288 if '.hg' in dirs:
1278 1289 yield root # found a repository
1279 1290 qroot = os.path.join(root, '.hg', 'patches')
1280 1291 if os.path.isdir(os.path.join(qroot, '.hg')):
1281 1292 yield qroot # we have a patch queue repo here
1282 1293 if recurse:
1283 1294 # avoid recursing inside the .hg directory
1284 1295 dirs.remove('.hg')
1285 1296 else:
1286 1297 dirs[:] = [] # don't descend further
1287 1298 elif followsym:
1288 1299 newdirs = []
1289 1300 for d in dirs:
1290 1301 fname = os.path.join(root, d)
1291 1302 if _add_dir_if_not_there(seen_dirs, fname):
1292 1303 if os.path.islink(fname):
1293 1304 for hgname in walkrepos(fname, True, seen_dirs):
1294 1305 yield hgname
1295 1306 else:
1296 1307 newdirs.append(d)
1297 1308 dirs[:] = newdirs
1298 1309
1299 1310 _rcpath = None
1300 1311
1301 1312 def os_rcpath():
1302 1313 '''return default os-specific hgrc search path'''
1303 1314 path = system_rcpath()
1304 1315 path.extend(user_rcpath())
1305 1316 path = [os.path.normpath(f) for f in path]
1306 1317 return path
1307 1318
1308 1319 def rcpath():
1309 1320 '''return hgrc search path. if env var HGRCPATH is set, use it.
1310 1321 for each item in path, if directory, use files ending in .rc,
1311 1322 else use item.
1312 1323 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1313 1324 if no HGRCPATH, use default os-specific path.'''
1314 1325 global _rcpath
1315 1326 if _rcpath is None:
1316 1327 if 'HGRCPATH' in os.environ:
1317 1328 _rcpath = []
1318 1329 for p in os.environ['HGRCPATH'].split(os.pathsep):
1319 1330 if not p:
1320 1331 continue
1321 1332 p = expandpath(p)
1322 1333 if os.path.isdir(p):
1323 1334 for f, kind in osutil.listdir(p):
1324 1335 if f.endswith('.rc'):
1325 1336 _rcpath.append(os.path.join(p, f))
1326 1337 else:
1327 1338 _rcpath.append(p)
1328 1339 else:
1329 1340 _rcpath = os_rcpath()
1330 1341 return _rcpath
1331 1342
1332 1343 def bytecount(nbytes):
1333 1344 '''return byte count formatted as readable string, with units'''
1334 1345
1335 1346 units = (
1336 1347 (100, 1 << 30, _('%.0f GB')),
1337 1348 (10, 1 << 30, _('%.1f GB')),
1338 1349 (1, 1 << 30, _('%.2f GB')),
1339 1350 (100, 1 << 20, _('%.0f MB')),
1340 1351 (10, 1 << 20, _('%.1f MB')),
1341 1352 (1, 1 << 20, _('%.2f MB')),
1342 1353 (100, 1 << 10, _('%.0f KB')),
1343 1354 (10, 1 << 10, _('%.1f KB')),
1344 1355 (1, 1 << 10, _('%.2f KB')),
1345 1356 (1, 1, _('%.0f bytes')),
1346 1357 )
1347 1358
1348 1359 for multiplier, divisor, format in units:
1349 1360 if nbytes >= divisor * multiplier:
1350 1361 return format % (nbytes / float(divisor))
1351 1362 return units[-1][2] % nbytes
1352 1363
1353 1364 def drop_scheme(scheme, path):
1354 1365 sc = scheme + ':'
1355 1366 if path.startswith(sc):
1356 1367 path = path[len(sc):]
1357 1368 if path.startswith('//'):
1358 1369 if scheme == 'file':
1359 1370 i = path.find('/', 2)
1360 1371 if i == -1:
1361 1372 return ''
1362 1373 # On Windows, absolute paths are rooted at the current drive
1363 1374 # root. On POSIX they are rooted at the file system root.
1364 1375 if os.name == 'nt':
1365 1376 droot = os.path.splitdrive(os.getcwd())[0] + '/'
1366 1377 path = os.path.join(droot, path[i + 1:])
1367 1378 else:
1368 1379 path = path[i:]
1369 1380 else:
1370 1381 path = path[2:]
1371 1382 return path
1372 1383
1373 1384 def uirepr(s):
1374 1385 # Avoid double backslash in Windows path repr()
1375 1386 return repr(s).replace('\\\\', '\\')
1376 1387
1377 1388 #### naming convention of below implementation follows 'textwrap' module
1378 1389
1379 1390 class MBTextWrapper(textwrap.TextWrapper):
1380 1391 def __init__(self, **kwargs):
1381 1392 textwrap.TextWrapper.__init__(self, **kwargs)
1382 1393
1383 1394 def _cutdown(self, str, space_left):
1384 1395 l = 0
1385 1396 ucstr = unicode(str, encoding.encoding)
1386 1397 w = unicodedata.east_asian_width
1387 1398 for i in xrange(len(ucstr)):
1388 1399 l += w(ucstr[i]) in 'WFA' and 2 or 1
1389 1400 if space_left < l:
1390 1401 return (ucstr[:i].encode(encoding.encoding),
1391 1402 ucstr[i:].encode(encoding.encoding))
1392 1403 return str, ''
1393 1404
1394 1405 # ----------------------------------------
1395 1406 # overriding of base class
1396 1407
1397 1408 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1398 1409 space_left = max(width - cur_len, 1)
1399 1410
1400 1411 if self.break_long_words:
1401 1412 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1402 1413 cur_line.append(cut)
1403 1414 reversed_chunks[-1] = res
1404 1415 elif not cur_line:
1405 1416 cur_line.append(reversed_chunks.pop())
1406 1417
1407 1418 #### naming convention of above implementation follows 'textwrap' module
1408 1419
1409 1420 def wrap(line, width, initindent='', hangindent=''):
1410 1421 maxindent = max(len(hangindent), len(initindent))
1411 1422 if width <= maxindent:
1412 1423 # adjust for weird terminal size
1413 1424 width = max(78, maxindent + 1)
1414 1425 wrapper = MBTextWrapper(width=width,
1415 1426 initial_indent=initindent,
1416 1427 subsequent_indent=hangindent)
1417 1428 return wrapper.fill(line)
1418 1429
1419 1430 def iterlines(iterator):
1420 1431 for chunk in iterator:
1421 1432 for line in chunk.splitlines():
1422 1433 yield line
1423 1434
1424 1435 def expandpath(path):
1425 1436 return os.path.expanduser(os.path.expandvars(path))
1426 1437
1427 1438 def hgcmd():
1428 1439 """Return the command used to execute current hg
1429 1440
1430 1441 This is different from hgexecutable() because on Windows we want
1431 1442 to avoid things opening new shell windows like batch files, so we
1432 1443 get either the python call or current executable.
1433 1444 """
1434 1445 if main_is_frozen():
1435 1446 return [sys.executable]
1436 1447 return gethgcmd()
1437 1448
1438 1449 def rundetached(args, condfn):
1439 1450 """Execute the argument list in a detached process.
1440 1451
1441 1452 condfn is a callable which is called repeatedly and should return
1442 1453 True once the child process is known to have started successfully.
1443 1454 At this point, the child process PID is returned. If the child
1444 1455 process fails to start or finishes before condfn() evaluates to
1445 1456 True, return -1.
1446 1457 """
1447 1458 # Windows case is easier because the child process is either
1448 1459 # successfully starting and validating the condition or exiting
1449 1460 # on failure. We just poll on its PID. On Unix, if the child
1450 1461 # process fails to start, it will be left in a zombie state until
1451 1462 # the parent wait on it, which we cannot do since we expect a long
1452 1463 # running process on success. Instead we listen for SIGCHLD telling
1453 1464 # us our child process terminated.
1454 1465 terminated = set()
1455 1466 def handler(signum, frame):
1456 1467 terminated.add(os.wait())
1457 1468 prevhandler = None
1458 1469 if hasattr(signal, 'SIGCHLD'):
1459 1470 prevhandler = signal.signal(signal.SIGCHLD, handler)
1460 1471 try:
1461 1472 pid = spawndetached(args)
1462 1473 while not condfn():
1463 1474 if ((pid in terminated or not testpid(pid))
1464 1475 and not condfn()):
1465 1476 return -1
1466 1477 time.sleep(0.1)
1467 1478 return pid
1468 1479 finally:
1469 1480 if prevhandler is not None:
1470 1481 signal.signal(signal.SIGCHLD, prevhandler)
1471 1482
1472 1483 try:
1473 1484 any, all = any, all
1474 1485 except NameError:
1475 1486 def any(iterable):
1476 1487 for i in iterable:
1477 1488 if i:
1478 1489 return True
1479 1490 return False
1480 1491
1481 1492 def all(iterable):
1482 1493 for i in iterable:
1483 1494 if not i:
1484 1495 return False
1485 1496 return True
1486 1497
1487 1498 def interpolate(prefix, mapping, s, fn=None):
1488 1499 """Return the result of interpolating items in the mapping into string s.
1489 1500
1490 1501 prefix is a single character string, or a two character string with
1491 1502 a backslash as the first character if the prefix needs to be escaped in
1492 1503 a regular expression.
1493 1504
1494 1505 fn is an optional function that will be applied to the replacement text
1495 1506 just before replacement.
1496 1507 """
1497 1508 fn = fn or (lambda s: s)
1498 1509 r = re.compile(r'%s(%s)' % (prefix, '|'.join(mapping.keys())))
1499 1510 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
1500 1511
1501 1512 def getport(port):
1502 1513 """Return the port for a given network service.
1503 1514
1504 1515 If port is an integer, it's returned as is. If it's a string, it's
1505 1516 looked up using socket.getservbyname(). If there's no matching
1506 1517 service, util.Abort is raised.
1507 1518 """
1508 1519 try:
1509 1520 return int(port)
1510 1521 except ValueError:
1511 1522 pass
1512 1523
1513 1524 try:
1514 1525 return socket.getservbyname(port)
1515 1526 except socket.error:
1516 1527 raise Abort(_("no port number associated with service '%s'") % port)
1517 1528
1518 1529 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
1519 1530 '0': False, 'no': False, 'false': False, 'off': False,
1520 1531 'never': False}
1521 1532
1522 1533 def parsebool(s):
1523 1534 """Parse s into a boolean.
1524 1535
1525 1536 If s is not a valid boolean, returns None.
1526 1537 """
1527 1538 return _booleans.get(s.lower(), None)
@@ -1,304 +1,350 b''
1 1
2 2 $ cat <<EOF >> $HGRCPATH
3 3 > [extensions]
4 4 > notify=
5 5 >
6 6 > [hooks]
7 7 > incoming.notify = python:hgext.notify.hook
8 8 >
9 9 > [notify]
10 10 > sources = pull
11 11 > diffstat = False
12 12 >
13 13 > [usersubs]
14 14 > foo@bar = *
15 15 >
16 16 > [reposubs]
17 17 > * = baz
18 18 > EOF
19 19 $ hg help notify
20 20 notify extension - hooks for sending email notifications at commit/push time
21 21
22 22 Subscriptions can be managed through a hgrc file. Default mode is to print
23 23 messages to stdout, for testing and configuring.
24 24
25 25 To use, configure the notify extension and enable it in hgrc like this:
26 26
27 27 [extensions]
28 28 notify =
29 29
30 30 [hooks]
31 31 # one email for each incoming changeset
32 32 incoming.notify = python:hgext.notify.hook
33 33 # batch emails when many changesets incoming at one time
34 34 changegroup.notify = python:hgext.notify.hook
35 35
36 36 [notify]
37 37 # config items go here
38 38
39 39 Required configuration items:
40 40
41 41 config = /path/to/file # file containing subscriptions
42 42
43 43 Optional configuration items:
44 44
45 45 test = True # print messages to stdout for testing
46 46 strip = 3 # number of slashes to strip for url paths
47 47 domain = example.com # domain to use if committer missing domain
48 48 style = ... # style file to use when formatting email
49 49 template = ... # template to use when formatting email
50 50 incoming = ... # template to use when run as incoming hook
51 51 changegroup = ... # template when run as changegroup hook
52 52 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
53 53 maxsubject = 67 # truncate subject line longer than this
54 54 diffstat = True # add a diffstat before the diff content
55 55 sources = serve # notify if source of incoming changes in this list
56 56 # (serve == ssh or http, push, pull, bundle)
57 57 merge = False # send notification for merges (default True)
58 58 [email]
59 59 from = user@host.com # email address to send as if none given
60 60 [web]
61 61 baseurl = http://hgserver/... # root of hg web site for browsing commits
62 62
63 63 The notify config file has same format as a regular hgrc file. It has two
64 64 sections so you can express subscriptions in whatever way is handier for you.
65 65
66 66 [usersubs]
67 67 # key is subscriber email, value is ","-separated list of glob patterns
68 68 user@host = pattern
69 69
70 70 [reposubs]
71 71 # key is glob pattern, value is ","-separated list of subscriber emails
72 72 pattern = user@host
73 73
74 74 Glob patterns are matched against path to repository root.
75 75
76 76 If you like, you can put notify config file in repository that users can push
77 77 changes to, they can manage their own subscriptions.
78 78
79 79 no commands defined
80 80 $ hg init a
81 81 $ echo a > a/a
82 82
83 83 commit
84 84
85 85 $ hg --cwd a commit -Ama -d '0 0'
86 86 adding a
87 87
88 88
89 89 clone
90 90
91 91 $ hg --traceback clone a b
92 92 updating to branch default
93 93 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 94 $ echo a >> a/a
95 95
96 96 commit
97 97
98 98 $ hg --traceback --cwd a commit -Amb -d '1 0'
99 99
100 100 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
101 101
102 102 $ cat <<EOF >> $HGRCPATH
103 103 > [notify]
104 104 > maxsubject = 200
105 105 > EOF
106 106
107 107 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
108 108 of the very long subject line
109 109 pull (minimal config)
110 110
111 111 $ hg --traceback --cwd b pull ../a | \
112 112 > python -c 'import sys,re; print re.sub("\n[\t ]", " ", sys.stdin.read()),'
113 113 pulling from ../a
114 114 searching for changes
115 115 adding changesets
116 116 adding manifests
117 117 adding file changes
118 118 added 1 changesets with 1 changes to 1 files
119 119 Content-Type: text/plain; charset="us-ascii"
120 120 MIME-Version: 1.0
121 121 Content-Transfer-Encoding: 7bit
122 122 Date: * (glob)
123 123 Subject: changeset in $TESTTMP/b: b
124 124 From: test
125 125 X-Hg-Notification: changeset 0647d048b600
126 126 Message-Id: <*> (glob)
127 127 To: baz, foo@bar
128 128
129 129 changeset 0647d048b600 in $TESTTMP/b
130 130 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
131 131 description: b
132 132
133 133 diffs (6 lines):
134 134
135 135 diff -r cb9a9f314b8b -r 0647d048b600 a
136 136 --- a/a Thu Jan 01 00:00:00 1970 +0000
137 137 +++ b/a Thu Jan 01 00:00:01 1970 +0000
138 138 @@ -1,1 +1,2 @@ a
139 139 +a
140 140 (run 'hg update' to get a working copy)
141 141 $ cat <<EOF >> $HGRCPATH
142 142 > [notify]
143 143 > config = `pwd`/.notify.conf
144 144 > domain = test.com
145 145 > strip = 42
146 146 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
147 147 >
148 148 > [web]
149 149 > baseurl = http://test/
150 150 > EOF
151 151
152 152 fail for config file is missing
153 153
154 154 $ hg --cwd b rollback
155 155 rolling back to revision 0 (undo pull)
156 156 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
157 157 pull failed
158 158 $ touch ".notify.conf"
159 159
160 160 pull
161 161
162 162 $ hg --cwd b rollback
163 163 rolling back to revision 0 (undo pull)
164 164 $ hg --traceback --cwd b pull ../a | \
165 165 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
166 166 pulling from ../a
167 167 searching for changes
168 168 adding changesets
169 169 adding manifests
170 170 adding file changes
171 171 added 1 changesets with 1 changes to 1 files
172 172 Content-Type: text/plain; charset="us-ascii"
173 173 MIME-Version: 1.0
174 174 Content-Transfer-Encoding: 7bit
175 175 X-Test: foo
176 176 Date: * (glob)
177 177 Subject: b
178 178 From: test@test.com
179 179 X-Hg-Notification: changeset 0647d048b600
180 180 Message-Id: <*> (glob)
181 181 To: baz@test.com, foo@bar
182 182
183 183 changeset 0647d048b600 in b
184 184 description: b
185 185 diffs (6 lines):
186 186
187 187 diff -r cb9a9f314b8b -r 0647d048b600 a
188 188 --- a/a Thu Jan 01 00:00:00 1970 +0000
189 189 +++ b/a Thu Jan 01 00:00:01 1970 +0000
190 190 @@ -1,1 +1,2 @@
191 191 a
192 192 +a
193 193 (run 'hg update' to get a working copy)
194 194
195 195 $ cat << EOF >> $HGRCPATH
196 196 > [hooks]
197 197 > incoming.notify = python:hgext.notify.hook
198 198 >
199 199 > [notify]
200 200 > sources = pull
201 201 > diffstat = True
202 202 > EOF
203 203
204 204 pull
205 205
206 206 $ hg --cwd b rollback
207 207 rolling back to revision 0 (undo pull)
208 208 $ hg --traceback --cwd b pull ../a | \
209 209 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
210 210 pulling from ../a
211 211 searching for changes
212 212 adding changesets
213 213 adding manifests
214 214 adding file changes
215 215 added 1 changesets with 1 changes to 1 files
216 216 Content-Type: text/plain; charset="us-ascii"
217 217 MIME-Version: 1.0
218 218 Content-Transfer-Encoding: 7bit
219 219 X-Test: foo
220 220 Date: * (glob)
221 221 Subject: b
222 222 From: test@test.com
223 223 X-Hg-Notification: changeset 0647d048b600
224 224 Message-Id: <*> (glob)
225 225 To: baz@test.com, foo@bar
226 226
227 227 changeset 0647d048b600 in b
228 228 description: b
229 229 diffstat:
230 230
231 231 a | 1 +
232 232 1 files changed, 1 insertions(+), 0 deletions(-)
233 233
234 234 diffs (6 lines):
235 235
236 236 diff -r cb9a9f314b8b -r 0647d048b600 a
237 237 --- a/a Thu Jan 01 00:00:00 1970 +0000
238 238 +++ b/a Thu Jan 01 00:00:01 1970 +0000
239 239 @@ -1,1 +1,2 @@
240 240 a
241 241 +a
242 242 (run 'hg update' to get a working copy)
243 243
244 244 test merge
245 245
246 246 $ cd a
247 247 $ hg up -C 0
248 248 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
249 249 $ echo a >> a
250 250 $ hg ci -Am adda2 -d '2 0'
251 251 created new head
252 252 $ hg merge
253 253 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 (branch merge, don't forget to commit)
255 255 $ hg ci -m merge -d '3 0'
256 256 $ cd ..
257 257 $ hg --traceback --cwd b pull ../a | \
258 258 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
259 259 pulling from ../a
260 260 searching for changes
261 261 adding changesets
262 262 adding manifests
263 263 adding file changes
264 264 added 2 changesets with 0 changes to 0 files
265 265 Content-Type: text/plain; charset="us-ascii"
266 266 MIME-Version: 1.0
267 267 Content-Transfer-Encoding: 7bit
268 268 X-Test: foo
269 269 Date: * (glob)
270 270 Subject: adda2
271 271 From: test@test.com
272 272 X-Hg-Notification: changeset 0a184ce6067f
273 273 Message-Id: <*> (glob)
274 274 To: baz@test.com, foo@bar
275 275
276 276 changeset 0a184ce6067f in b
277 277 description: adda2
278 278 diffstat:
279 279
280 280 a | 1 +
281 281 1 files changed, 1 insertions(+), 0 deletions(-)
282 282
283 283 diffs (6 lines):
284 284
285 285 diff -r cb9a9f314b8b -r 0a184ce6067f a
286 286 --- a/a Thu Jan 01 00:00:00 1970 +0000
287 287 +++ b/a Thu Jan 01 00:00:02 1970 +0000
288 288 @@ -1,1 +1,2 @@
289 289 a
290 290 +a
291 291 Content-Type: text/plain; charset="us-ascii"
292 292 MIME-Version: 1.0
293 293 Content-Transfer-Encoding: 7bit
294 294 X-Test: foo
295 295 Date: * (glob)
296 296 Subject: merge
297 297 From: test@test.com
298 298 X-Hg-Notification: changeset 22c88b85aa27
299 299 Message-Id: <*> (glob)
300 300 To: baz@test.com, foo@bar
301 301
302 302 changeset 22c88b85aa27 in b
303 303 description: merge
304 304 (run 'hg update' to get a working copy)
305
306 truncate multi-byte subject
307
308 $ cat <<EOF >> $HGRCPATH
309 > [notify]
310 > maxsubject = 4
311 > EOF
312 $ echo a >> a/a
313 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
314 > -m `python -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
315 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
316 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
317 pulling from ../a
318 searching for changes
319 adding changesets
320 adding manifests
321 adding file changes
322 added 1 changesets with 1 changes to 1 files
323 Content-Type: text/plain; charset="us-ascii"
324 MIME-Version: 1.0
325 Content-Transfer-Encoding: 8bit
326 X-Test: foo
327 Date: * (glob)
328 Subject: \xc3\xa0... (esc)
329 From: test@test.com
330 X-Hg-Notification: changeset 4a47f01c1356
331 Message-Id: <*> (glob)
332 To: baz@test.com, foo@bar
333
334 changeset 4a47f01c1356 in b
335 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
336 diffstat:
337
338 a | 1 +
339 1 files changed, 1 insertions(+), 0 deletions(-)
340
341 diffs (7 lines):
342
343 diff -r 22c88b85aa27 -r 4a47f01c1356 a
344 --- a/a Thu Jan 01 00:00:03 1970 +0000
345 +++ b/a Thu Jan 01 00:00:00 1970 +0000
346 @@ -1,2 +1,3 @@
347 a
348 a
349 +a
350 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now