diff options
Diffstat (limited to 'websdk/mercurial/store.py')
-rw-r--r--[l---------] | websdk/mercurial/store.py | 428 |
1 files changed, 427 insertions, 1 deletions
diff --git a/websdk/mercurial/store.py b/websdk/mercurial/store.py index 5f2639a..fac0802 120000..100644 --- a/websdk/mercurial/store.py +++ b/websdk/mercurial/store.py @@ -1 +1,427 @@ -/usr/share/pyshared/mercurial/store.py
\ No newline at end of file +# 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, scmutil, 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): + ''' + >>> encodedir('data/foo.i') + 'data/foo.i' + >>> encodedir('data/foo.i/bla.i') + 'data/foo.i.hg/bla.i' + >>> encodedir('data/foo.i.hg/bla.i') + 'data/foo.i.hg.hg/bla.i' + ''' + if not path.startswith('data/'): + return path + return (path + .replace(".hg/", ".hg.hg/") + .replace(".i/", ".i.hg/") + .replace(".d/", ".d.hg/")) + +def decodedir(path): + ''' + >>> decodedir('data/foo.i') + 'data/foo.i' + >>> decodedir('data/foo.i.hg/bla.i') + 'data/foo.i/bla.i' + >>> decodedir('data/foo.i.hg.hg/bla.i') + 'data/foo.i.hg/bla.i' + ''' + 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(): + ''' + >>> enc, dec = _buildencodefun() + + >>> enc('nothing/special.txt') + 'nothing/special.txt' + >>> dec('nothing/special.txt') + 'nothing/special.txt' + + >>> enc('HELLO') + '_h_e_l_l_o' + >>> dec('_h_e_l_l_o') + 'HELLO' + + >>> enc('hello:world?') + 'hello~3aworld~3f' + >>> dec('hello~3aworld~3f') + 'hello:world?' + + >>> enc('the\x07quick\xADshot') + 'the~07quick~adshot' + >>> dec('the~07quick~adshot') + 'the\\x07quick\\xadshot' + ''' + e = '_' + winreserved = [ord(x) for x in '\\:*?"<>|'] + cmap = dict([(chr(x), chr(x)) for x in xrange(127)]) + for x in (range(32) + range(126, 256) + winreserved): + 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 _buildlowerencodefun(): + ''' + >>> f = _buildlowerencodefun() + >>> f('nothing/special.txt') + 'nothing/special.txt' + >>> f('HELLO') + 'hello' + >>> f('hello:world?') + 'hello~3aworld~3f' + >>> f('the\x07quick\xADshot') + 'the~07quick~adshot' + ''' + winreserved = [ord(x) for x in '\\:*?"<>|'] + cmap = dict([(chr(x), chr(x)) for x in xrange(127)]) + for x in (range(32) + range(126, 256) + winreserved): + 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 = _buildlowerencodefun() + +_winreservednames = '''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): + ''' + Encodes filenames containing names reserved by Windows or which end in + period or space. Does not touch other single reserved characters c. + Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here. + Additionally encodes space or period at the beginning, if dotencode is + True. + path is assumed to be all lowercase. + + >>> _auxencode('.foo/aux.txt/txt.aux/con/prn/nul/foo.', True) + '~2efoo/au~78.txt/txt.aux/co~6e/pr~6e/nu~6c/foo~2e' + >>> _auxencode('.com1com2/lpt9.lpt4.lpt1/conprn/foo.', False) + '.com1com2/lp~749.lpt4.lpt1/conprn/foo~2e' + >>> _auxencode('foo. ', True) + 'foo.~20' + >>> _auxencode(' .foo', True) + '~20.foo' + ''' + res = [] + for n in path.split('/'): + if n: + base = n.split('.')[0] + if base and (base in _winreservednames): + # 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) + +_maxstorepathlen = 120 +_dirprefixlen = 8 +_maxshortdirslen = 8 * (_dirprefixlen + 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 _maxstorepathlen, a + non-reversible hybrid hashing of the path is done instead. + This encoding uses up to _dirprefixlen characters of all directory + levels of the lowerencoded path, but not more levels than can fit into + _maxshortdirslen. + 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 _maxstorepathlen 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) > _maxstorepathlen: + 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[:_dirprefixlen] + if d[-1] in '. ': + # Windows can't access dirs ending in period or space + d = d[:-1] + '_' + t = '/'.join(sdirs) + '/' + d + if len(t) > _maxshortdirslen: + break + sdirs.append(d) + dirs = '/'.join(sdirs) + if len(dirs) > 0: + dirs += '/' + res = 'dh/' + dirs + digest + ext + spaceleft = _maxstorepathlen - len(res) + if spaceleft > 0: + filler = basename[:spaceleft] + 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, openertype): + self.path = path + self.createmode = _calcmode(path) + op = openertype(self.path) + op.createmode = self.createmode + self.opener = scmutil.filteropener(op, encodedir) + + def join(self, f): + return self.path + '/' + encodedir(f) + + def _walk(self, relpath, recurse): + '''yields (unencoded, encoded, size)''' + path = self.path + if relpath: + path += '/' + relpath + striplen = len(self.path) + 1 + l = [] + if os.path.isdir(path): + visit = [path] + while visit: + p = visit.pop() + for f, kind, st in osutil.listdir(p, stat=True): + fp = 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() + + def write(self): + pass + +class encodedstore(basicstore): + def __init__(self, path, openertype): + self.path = path + '/store' + self.createmode = _calcmode(self.path) + op = openertype(self.path) + op.createmode = self.createmode + self.opener = scmutil.filteropener(op, encodefilename) + + 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.path + '/' + encodefilename(f) + + def copylist(self): + return (['requires', '00changelog.i'] + + ['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 + self._dirty = False + + def _load(self): + '''fill the entries from the fncache file''' + self.entries = set() + self._dirty = False + 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) + self._dirty = False + + def write(self): + if not self._dirty: + return + fp = self.opener('fncache', mode='wb', atomictemp=True) + for p in self.entries: + fp.write(encodedir(p) + '\n') + fp.close() + self._dirty = False + + def add(self, fn): + if self.entries is None: + self._load() + if fn not in self.entries: + self._dirty = True + 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 _fncacheopener(scmutil.abstractopener): + def __init__(self, op, fnc, encode): + self.opener = op + self.fncache = fnc + self.encode = encode + + def __call__(self, path, mode='r', *args, **kw): + if mode not in ('r', 'rb') and path.startswith('data/'): + self.fncache.add(path) + return self.opener(self.encode(path), mode, *args, **kw) + +class fncachestore(basicstore): + def __init__(self, path, openertype, encode): + self.encode = encode + self.path = path + '/store' + self.createmode = _calcmode(self.path) + op = openertype(self.path) + op.createmode = self.createmode + fnc = fncache(op) + self.fncache = fnc + self.opener = _fncacheopener(op, fnc, encode) + + def join(self, f): + return self.path + '/' + self.encode(f) + + def datafiles(self): + rewrite = False + existing = [] + spath = self.path + for f in self.fncache: + ef = self.encode(f) + try: + st = os.stat(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'] + + ['store/' + f for f in d.split()]) + + def write(self): + self.fncache.write() + +def store(requirements, path, openertype): + 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, openertype, encode) + return encodedstore(path, openertype) + return basicstore(path, openertype) |