eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/store.py
changeset 69 c6bca38c1cbf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/store.py	Sat Jan 08 11:20:57 2011 +0530
@@ -0,0 +1,339 @@
+# store.py - repository store handling for Mercurial
+#
+# Copyright 2008 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from i18n import _
+import osutil, util
+import os, stat
+
+_sha = util.sha1
+
+# This avoids a collision between a file named foo and a dir named
+# foo.i or foo.d
+def encodedir(path):
+    if not path.startswith('data/'):
+        return path
+    return (path
+            .replace(".hg/", ".hg.hg/")
+            .replace(".i/", ".i.hg/")
+            .replace(".d/", ".d.hg/"))
+
+def decodedir(path):
+    if not path.startswith('data/') or ".hg/" not in path:
+        return path
+    return (path
+            .replace(".d.hg/", ".d/")
+            .replace(".i.hg/", ".i/")
+            .replace(".hg.hg/", ".hg/"))
+
+def _buildencodefun():
+    e = '_'
+    win_reserved = [ord(x) for x in '\\:*?"<>|']
+    cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
+    for x in (range(32) + range(126, 256) + win_reserved):
+        cmap[chr(x)] = "~%02x" % x
+    for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
+        cmap[chr(x)] = e + chr(x).lower()
+    dmap = {}
+    for k, v in cmap.iteritems():
+        dmap[v] = k
+    def decode(s):
+        i = 0
+        while i < len(s):
+            for l in xrange(1, 4):
+                try:
+                    yield dmap[s[i:i + l]]
+                    i += l
+                    break
+                except KeyError:
+                    pass
+            else:
+                raise KeyError
+    return (lambda s: "".join([cmap[c] for c in encodedir(s)]),
+            lambda s: decodedir("".join(list(decode(s)))))
+
+encodefilename, decodefilename = _buildencodefun()
+
+def _build_lower_encodefun():
+    win_reserved = [ord(x) for x in '\\:*?"<>|']
+    cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
+    for x in (range(32) + range(126, 256) + win_reserved):
+        cmap[chr(x)] = "~%02x" % x
+    for x in range(ord("A"), ord("Z")+1):
+        cmap[chr(x)] = chr(x).lower()
+    return lambda s: "".join([cmap[c] for c in s])
+
+lowerencode = _build_lower_encodefun()
+
+_windows_reserved_filenames = '''con prn aux nul
+    com1 com2 com3 com4 com5 com6 com7 com8 com9
+    lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
+def _auxencode(path, dotencode):
+    res = []
+    for n in path.split('/'):
+        if n:
+            base = n.split('.')[0]
+            if base and (base in _windows_reserved_filenames):
+                # encode third letter ('aux' -> 'au~78')
+                ec = "~%02x" % ord(n[2])
+                n = n[0:2] + ec + n[3:]
+            if n[-1] in '. ':
+                # encode last period or space ('foo...' -> 'foo..~2e')
+                n = n[:-1] + "~%02x" % ord(n[-1])
+            if dotencode and n[0] in '. ':
+                n = "~%02x" % ord(n[0]) + n[1:]
+        res.append(n)
+    return '/'.join(res)
+
+MAX_PATH_LEN_IN_HGSTORE = 120
+DIR_PREFIX_LEN = 8
+_MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
+def _hybridencode(path, auxencode):
+    '''encodes path with a length limit
+
+    Encodes all paths that begin with 'data/', according to the following.
+
+    Default encoding (reversible):
+
+    Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
+    characters are encoded as '~xx', where xx is the two digit hex code
+    of the character (see encodefilename).
+    Relevant path components consisting of Windows reserved filenames are
+    masked by encoding the third character ('aux' -> 'au~78', see auxencode).
+
+    Hashed encoding (not reversible):
+
+    If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a
+    non-reversible hybrid hashing of the path is done instead.
+    This encoding uses up to DIR_PREFIX_LEN characters of all directory
+    levels of the lowerencoded path, but not more levels than can fit into
+    _MAX_SHORTENED_DIRS_LEN.
+    Then follows the filler followed by the sha digest of the full path.
+    The filler is the beginning of the basename of the lowerencoded path
+    (the basename is everything after the last path separator). The filler
+    is as long as possible, filling in characters from the basename until
+    the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars
+    of the basename have been taken).
+    The extension (e.g. '.i' or '.d') is preserved.
+
+    The string 'data/' at the beginning is replaced with 'dh/', if the hashed
+    encoding was used.
+    '''
+    if not path.startswith('data/'):
+        return path
+    # escape directories ending with .i and .d
+    path = encodedir(path)
+    ndpath = path[len('data/'):]
+    res = 'data/' + auxencode(encodefilename(ndpath))
+    if len(res) > MAX_PATH_LEN_IN_HGSTORE:
+        digest = _sha(path).hexdigest()
+        aep = auxencode(lowerencode(ndpath))
+        _root, ext = os.path.splitext(aep)
+        parts = aep.split('/')
+        basename = parts[-1]
+        sdirs = []
+        for p in parts[:-1]:
+            d = p[:DIR_PREFIX_LEN]
+            if d[-1] in '. ':
+                # Windows can't access dirs ending in period or space
+                d = d[:-1] + '_'
+            t = '/'.join(sdirs) + '/' + d
+            if len(t) > _MAX_SHORTENED_DIRS_LEN:
+                break
+            sdirs.append(d)
+        dirs = '/'.join(sdirs)
+        if len(dirs) > 0:
+            dirs += '/'
+        res = 'dh/' + dirs + digest + ext
+        space_left = MAX_PATH_LEN_IN_HGSTORE - len(res)
+        if space_left > 0:
+            filler = basename[:space_left]
+            res = 'dh/' + dirs + filler + digest + ext
+    return res
+
+def _calcmode(path):
+    try:
+        # files in .hg/ will be created using this mode
+        mode = os.stat(path).st_mode
+            # avoid some useless chmods
+        if (0777 & ~util.umask) == (0777 & mode):
+            mode = None
+    except OSError:
+        mode = None
+    return mode
+
+_data = 'data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
+
+class basicstore(object):
+    '''base class for local repository stores'''
+    def __init__(self, path, opener, pathjoiner):
+        self.pathjoiner = pathjoiner
+        self.path = path
+        self.createmode = _calcmode(path)
+        op = opener(self.path)
+        op.createmode = self.createmode
+        self.opener = lambda f, *args, **kw: op(encodedir(f), *args, **kw)
+
+    def join(self, f):
+        return self.pathjoiner(self.path, encodedir(f))
+
+    def _walk(self, relpath, recurse):
+        '''yields (unencoded, encoded, size)'''
+        path = self.pathjoiner(self.path, relpath)
+        striplen = len(self.path) + len(os.sep)
+        l = []
+        if os.path.isdir(path):
+            visit = [path]
+            while visit:
+                p = visit.pop()
+                for f, kind, st in osutil.listdir(p, stat=True):
+                    fp = self.pathjoiner(p, f)
+                    if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
+                        n = util.pconvert(fp[striplen:])
+                        l.append((decodedir(n), n, st.st_size))
+                    elif kind == stat.S_IFDIR and recurse:
+                        visit.append(fp)
+        return sorted(l)
+
+    def datafiles(self):
+        return self._walk('data', True)
+
+    def walk(self):
+        '''yields (unencoded, encoded, size)'''
+        # yield data files first
+        for x in self.datafiles():
+            yield x
+        # yield manifest before changelog
+        for x in reversed(self._walk('', False)):
+            yield x
+
+    def copylist(self):
+        return ['requires'] + _data.split()
+
+class encodedstore(basicstore):
+    def __init__(self, path, opener, pathjoiner):
+        self.pathjoiner = pathjoiner
+        self.path = self.pathjoiner(path, 'store')
+        self.createmode = _calcmode(self.path)
+        op = opener(self.path)
+        op.createmode = self.createmode
+        self.opener = lambda f, *args, **kw: op(encodefilename(f), *args, **kw)
+
+    def datafiles(self):
+        for a, b, size in self._walk('data', True):
+            try:
+                a = decodefilename(a)
+            except KeyError:
+                a = None
+            yield a, b, size
+
+    def join(self, f):
+        return self.pathjoiner(self.path, encodefilename(f))
+
+    def copylist(self):
+        return (['requires', '00changelog.i'] +
+                [self.pathjoiner('store', f) for f in _data.split()])
+
+class fncache(object):
+    # the filename used to be partially encoded
+    # hence the encodedir/decodedir dance
+    def __init__(self, opener):
+        self.opener = opener
+        self.entries = None
+
+    def _load(self):
+        '''fill the entries from the fncache file'''
+        self.entries = set()
+        try:
+            fp = self.opener('fncache', mode='rb')
+        except IOError:
+            # skip nonexistent file
+            return
+        for n, line in enumerate(fp):
+            if (len(line) < 2) or (line[-1] != '\n'):
+                t = _('invalid entry in fncache, line %s') % (n + 1)
+                raise util.Abort(t)
+            self.entries.add(decodedir(line[:-1]))
+        fp.close()
+
+    def rewrite(self, files):
+        fp = self.opener('fncache', mode='wb')
+        for p in files:
+            fp.write(encodedir(p) + '\n')
+        fp.close()
+        self.entries = set(files)
+
+    def add(self, fn):
+        if self.entries is None:
+            self._load()
+        if fn not in self.entries:
+            self.opener('fncache', 'ab').write(encodedir(fn) + '\n')
+            self.entries.add(fn)
+
+    def __contains__(self, fn):
+        if self.entries is None:
+            self._load()
+        return fn in self.entries
+
+    def __iter__(self):
+        if self.entries is None:
+            self._load()
+        return iter(self.entries)
+
+class fncachestore(basicstore):
+    def __init__(self, path, opener, pathjoiner, encode):
+        self.encode = encode
+        self.pathjoiner = pathjoiner
+        self.path = self.pathjoiner(path, 'store')
+        self.createmode = _calcmode(self.path)
+        op = opener(self.path)
+        op.createmode = self.createmode
+        fnc = fncache(op)
+        self.fncache = fnc
+
+        def fncacheopener(path, mode='r', *args, **kw):
+            if mode not in ('r', 'rb') and path.startswith('data/'):
+                fnc.add(path)
+            return op(self.encode(path), mode, *args, **kw)
+        self.opener = fncacheopener
+
+    def join(self, f):
+        return self.pathjoiner(self.path, self.encode(f))
+
+    def datafiles(self):
+        rewrite = False
+        existing = []
+        pjoin = self.pathjoiner
+        spath = self.path
+        for f in self.fncache:
+            ef = self.encode(f)
+            try:
+                st = os.stat(pjoin(spath, ef))
+                yield f, ef, st.st_size
+                existing.append(f)
+            except OSError:
+                # nonexistent entry
+                rewrite = True
+        if rewrite:
+            # rewrite fncache to remove nonexistent entries
+            # (may be caused by rollback / strip)
+            self.fncache.rewrite(existing)
+
+    def copylist(self):
+        d = ('data dh fncache'
+             ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
+        return (['requires', '00changelog.i'] +
+                [self.pathjoiner('store', f) for f in d.split()])
+
+def store(requirements, path, opener, pathjoiner=None):
+    pathjoiner = pathjoiner or os.path.join
+    if 'store' in requirements:
+        if 'fncache' in requirements:
+            auxencode = lambda f: _auxencode(f, 'dotencode' in requirements)
+            encode = lambda f: _hybridencode(f, auxencode)
+            return fncachestore(path, opener, pathjoiner, encode)
+        return encodedstore(path, opener, pathjoiner)
+    return basicstore(path, opener, pathjoiner)