##// END OF EJS Templates
introduce fncache repository layout...
Adrian Buehlmann -
r7229:7946503e default
parent child Browse files
Show More
@@ -16,7 +16,7 b' import merge as merge_'
16 16
17 17 class localrepository(repo.repository):
18 18 capabilities = util.set(('lookup', 'changegroupsubset'))
19 supported = ('revlogv1', 'store')
19 supported = ('revlogv1', 'store', 'fncache')
20 20
21 21 def __init__(self, parentui, path=None, create=0):
22 22 repo.repository.__init__(self)
@@ -35,6 +35,7 b' class localrepository(repo.repository):'
35 35 if parentui.configbool('format', 'usestore', True):
36 36 os.mkdir(os.path.join(self.path, "store"))
37 37 requirements.append("store")
38 requirements.append("fncache")
38 39 # create an invalid changelog
39 40 self.opener("00changelog.i", "a").write(
40 41 '\0\0\0\2' # represents revlogv2
@@ -5,8 +5,11 b''
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 from i18n import _
8 9 import os, stat, osutil, util
9 10
11 _sha = util.sha1
12
10 13 def _buildencodefun():
11 14 e = '_'
12 15 win_reserved = [ord(x) for x in '\\:*?"<>|']
@@ -35,6 +38,93 b' def _buildencodefun():'
35 38
36 39 encodefilename, decodefilename = _buildencodefun()
37 40
41 def _build_lower_encodefun():
42 win_reserved = [ord(x) for x in '\\:*?"<>|']
43 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
44 for x in (range(32) + range(126, 256) + win_reserved):
45 cmap[chr(x)] = "~%02x" % x
46 for x in range(ord("A"), ord("Z")+1):
47 cmap[chr(x)] = chr(x).lower()
48 return lambda s: "".join([cmap[c] for c in s])
49
50 lowerencode = _build_lower_encodefun()
51
52 _windows_reserved_filenames = '''con prn aux nul
53 com1 com2 com3 com4 com5 com6 com7 com8 com9
54 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
55 def auxencode(path):
56 res = []
57 for n in path.split('/'):
58 if n:
59 base = n.split('.')[0]
60 if base and (base in _windows_reserved_filenames):
61 # encode third letter ('aux' -> 'au~78')
62 ec = "~%02x" % ord(n[2])
63 n = n[0:2] + ec + n[3:]
64 res.append(n)
65 return '/'.join(res)
66
67 MAX_PATH_LEN_IN_HGSTORE = 120
68 DIR_PREFIX_LEN = 8
69 _MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
70 def hybridencode(path):
71 '''encodes path with a length limit
72
73 Encodes all paths that begin with 'data/', according to the following.
74
75 Default encoding (reversible):
76
77 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
78 characters are encoded as '~xx', where xx is the two digit hex code
79 of the character (see encodefilename).
80 Relevant path components consisting of Windows reserved filenames are
81 masked by encoding the third character ('aux' -> 'au~78', see auxencode).
82
83 Hashed encoding (not reversible):
84
85 If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a
86 non-reversible hybrid hashing of the path is done instead.
87 This encoding uses up to DIR_PREFIX_LEN characters of all directory
88 levels of the lowerencoded path, but not more levels than can fit into
89 _MAX_SHORTENED_DIRS_LEN.
90 Then follows the filler followed by the sha digest of the full path.
91 The filler is the beginning of the basename of the lowerencoded path
92 (the basename is everything after the last path separator). The filler
93 is as long as possible, filling in characters from the basename until
94 the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars
95 of the basename have been taken).
96 The extension (e.g. '.i' or '.d') is preserved.
97
98 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
99 encoding was used.
100 '''
101 if not path.startswith('data/'):
102 return path
103 ndpath = path[len('data/'):]
104 res = 'data/' + auxencode(encodefilename(ndpath))
105 if len(res) > MAX_PATH_LEN_IN_HGSTORE:
106 digest = _sha(path).hexdigest()
107 aep = auxencode(lowerencode(ndpath))
108 _root, ext = os.path.splitext(aep)
109 parts = aep.split('/')
110 basename = parts[-1]
111 sdirs = []
112 for p in parts[:-1]:
113 d = p[:DIR_PREFIX_LEN]
114 t = '/'.join(sdirs) + '/' + d
115 if len(t) > _MAX_SHORTENED_DIRS_LEN:
116 break
117 sdirs.append(d)
118 dirs = '/'.join(sdirs)
119 if len(dirs) > 0:
120 dirs += '/'
121 res = 'dh/' + dirs + digest + ext
122 space_left = MAX_PATH_LEN_IN_HGSTORE - len(res)
123 if space_left > 0:
124 filler = basename[:space_left]
125 res = 'dh/' + dirs + filler + digest + ext
126 return res
127
38 128 def _calcmode(path):
39 129 try:
40 130 # files in .hg/ will be created using this mode
@@ -120,8 +210,83 b' class encodedstore(basicstore):'
120 210 return (['requires', '00changelog.i'] +
121 211 [self.pathjoiner('store', f) for f in _data.split()])
122 212
213 def fncache(opener):
214 '''yields the entries in the fncache file'''
215 try:
216 fp = opener('fncache', mode='rb')
217 except IOError:
218 # skip nonexistent file
219 return
220 for n, line in enumerate(fp):
221 if (len(line) < 2) or (line[-1] != '\n'):
222 t = _('invalid entry in fncache, line %s') % (n + 1)
223 raise util.Abort(t)
224 yield line[:-1]
225 fp.close()
226
227 class fncacheopener(object):
228 def __init__(self, opener):
229 self.opener = opener
230 self.entries = None
231
232 def loadfncache(self):
233 self.entries = {}
234 for f in fncache(self.opener):
235 self.entries[f] = True
236
237 def __call__(self, path, mode='r', *args, **kw):
238 if mode not in ('r', 'rb') and path.startswith('data/'):
239 if self.entries is None:
240 self.loadfncache()
241 if path not in self.entries:
242 self.opener('fncache', 'ab').write(path + '\n')
243 # fncache may contain non-existent files after rollback / strip
244 self.entries[path] = True
245 return self.opener(hybridencode(path), mode, *args, **kw)
246
247 class fncachestore(basicstore):
248 def __init__(self, path, opener, pathjoiner):
249 self.pathjoiner = pathjoiner
250 self.path = self.pathjoiner(path, 'store')
251 self.createmode = _calcmode(self.path)
252 self._op = opener(self.path)
253 self._op.createmode = self.createmode
254 self.opener = fncacheopener(self._op)
255
256 def join(self, f):
257 return self.pathjoiner(self.path, hybridencode(f))
258
259 def datafiles(self):
260 rewrite = False
261 existing = []
262 pjoin = self.pathjoiner
263 spath = self.path
264 for f in fncache(self._op):
265 ef = hybridencode(f)
266 try:
267 st = os.stat(pjoin(spath, ef))
268 yield f, ef, st.st_size
269 existing.append(f)
270 except OSError:
271 # nonexistent entry
272 rewrite = True
273 if rewrite:
274 # rewrite fncache to remove nonexistent entries
275 # (may be caused by rollback / strip)
276 fp = self._op('fncache', mode='wb')
277 for p in existing:
278 fp.write(p + '\n')
279 fp.close()
280
281 def copylist(self):
282 d = _data + ' dh fncache'
283 return (['requires', '00changelog.i'] +
284 [self.pathjoiner('store', f) for f in d.split()])
285
123 286 def store(requirements, path, opener, pathjoiner=None):
124 287 pathjoiner = pathjoiner or os.path.join
125 288 if 'store' in requirements:
289 if 'fncache' in requirements:
290 return fncachestore(path, opener, pathjoiner)
126 291 return encodedstore(path, opener, pathjoiner)
127 292 return basicstore(path, opener, pathjoiner)
@@ -2,6 +2,7 b''
2 2
3 3 CONTRIBDIR=$TESTDIR/../contrib
4 4
5 echo % prepare repo-a
5 6 mkdir repo-a
6 7 cd repo-a
7 8 hg init
@@ -18,11 +19,13 b" hg commit -m third -d '0 0'"
18 19
19 20 hg verify
20 21
21 echo dumping revlog of file a to stdout:
22 echo
23 echo % dumping revlog of file a to stdout
22 24 python $CONTRIBDIR/dumprevlog .hg/store/data/a.i
23 echo dumprevlog done
25 echo % dumprevlog done
24 26
25 # dump all revlogs to file repo.dump
27 echo
28 echo % dump all revlogs to file repo.dump
26 29 find .hg/store -name "*.i" | sort | xargs python $CONTRIBDIR/dumprevlog > ../repo.dump
27 30
28 31 cd ..
@@ -31,17 +34,28 b' mkdir repo-b'
31 34 cd repo-b
32 35 hg init
33 36
34 echo undumping:
37 echo
38 echo % undumping into repo-b
35 39 python $CONTRIBDIR/undumprevlog < ../repo.dump
36 echo undumping done
40 echo % undumping done
41
42 cd ..
37 43
44 echo
45 echo % clone --pull repo-b repo-c to rebuild fncache
46 hg clone --pull -U repo-b repo-c
47
48 cd repo-c
49
50 echo
51 echo % verify repo-c
38 52 hg verify
39 53
40 54 cd ..
41 55
42 echo comparing repos:
43 hg -R repo-b incoming repo-a
44 hg -R repo-a incoming repo-b
45 echo comparing done
56 echo
57 echo % comparing repos
58 hg -R repo-c incoming repo-a
59 hg -R repo-a incoming repo-c
46 60
47 61 exit 0
@@ -1,9 +1,11 b''
1 % prepare repo-a
1 2 checking changesets
2 3 checking manifests
3 4 crosschecking files in changesets and manifests
4 5 checking files
5 6 1 files, 3 changesets, 3 total revisions
6 dumping revlog of file a to stdout:
7
8 % dumping revlog of file a to stdout
7 9 file: .hg/store/data/a.i
8 10 node: 183d2312b35066fb6b3b449b84efc370d50993d0
9 11 linkrev: 0
@@ -32,22 +34,34 b' adding to file a'
32 34 adding more to file a
33 35
34 36 -end-
35 dumprevlog done
36 undumping:
37 % dumprevlog done
38
39 % dump all revlogs to file repo.dump
40
41 % undumping into repo-b
37 42 .hg/store/00changelog.i
38 43 .hg/store/00manifest.i
39 44 .hg/store/data/a.i
40 undumping done
45 % undumping done
46
47 % clone --pull repo-b repo-c to rebuild fncache
48 requesting all changes
49 adding changesets
50 adding manifests
51 adding file changes
52 added 3 changesets with 3 changes to 1 files
53
54 % verify repo-c
41 55 checking changesets
42 56 checking manifests
43 57 crosschecking files in changesets and manifests
44 58 checking files
45 59 1 files, 3 changesets, 3 total revisions
46 comparing repos:
60
61 % comparing repos
47 62 comparing with repo-a
48 63 searching for changes
49 64 no changes found
50 comparing with repo-b
65 comparing with repo-c
51 66 searching for changes
52 67 no changes found
53 comparing done
@@ -22,6 +22,7 b' 00770 ./.hg/store/data/'
22 22 00770 ./.hg/store/data/dir/
23 23 00660 ./.hg/store/data/dir/bar.i
24 24 00660 ./.hg/store/data/foo.i
25 00660 ./.hg/store/fncache
25 26 00660 ./.hg/store/undo
26 27 00660 ./.hg/undo.branch
27 28 00660 ./.hg/undo.dirstate
@@ -49,6 +50,7 b' 00770 ../push/.hg/store/data/'
49 50 00770 ../push/.hg/store/data/dir/
50 51 00660 ../push/.hg/store/data/dir/bar.i
51 52 00660 ../push/.hg/store/data/foo.i
53 00660 ../push/.hg/store/fncache
52 54 00660 ../push/.hg/store/undo
53 55 00660 ../push/.hg/undo.branch
54 56 00660 ../push/.hg/undo.dirstate
@@ -3,6 +3,7 b' store created'
3 3 00changelog.i created
4 4 revlogv1
5 5 store
6 fncache
6 7 adding foo
7 8 # creating repo with old format
8 9 revlogv1
@@ -17,7 +17,6 b' checking changesets'
17 17 checking manifests
18 18 crosschecking files in changesets and manifests
19 19 checking files
20 ?: cannot decode filename 'data/X_f_o_o.txt.i'
21 20 data/FOO.txt.i@0: missing revlog!
22 21 0: empty or missing FOO.txt
23 22 FOO.txt@0: f62022d3d590 in manifests not found
@@ -27,8 +26,6 b' checking files'
27 26 data/bar.txt.i@0: missing revlog!
28 27 0: empty or missing bar.txt
29 28 bar.txt@0: 256559129457 in manifests not found
30 warning: orphan revlog 'data/xbar.txt.i'
31 29 3 files, 1 changesets, 0 total revisions
32 1 warnings encountered!
33 10 integrity errors encountered!
30 9 integrity errors encountered!
34 31 (first damaged changeset appears to be 0)
General Comments 0
You need to be logged in to leave comments. Login now